Merge branch 'stable-3.1' * stable-3.1: ElasticContainer: Upgrade V7_5 to elasticsearch 7.5.1 ElasticContainer: Upgrade V6_8 to elasticsearch 6.8.6 Documentation: Update elasticsearch tests on macOS Add a right margin to the search bar Upgrade JGit to v5.6.0.201912101111-r polygerrit: Fix 'Unexpected trailing comma' lint errors Change-Id: I10d1f9f58f8d27bb23e960f36f49f899cb879a1b
diff --git a/.bazelignore b/.bazelignore index 30f1613..69c04b1 100644 --- a/.bazelignore +++ b/.bazelignore
@@ -1 +1,2 @@ eclipse-out +node_modules
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt index d333347..23ab39b 100644 --- a/Documentation/access-control.txt +++ b/Documentation/access-control.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - Access Controls Access controls in Gerrit are group based. Every user account is a @@ -7,10 +8,10 @@ To view/edit the access controls for a specific project, first navigate to the projects page: for example, -https://gerrit-review.googlesource.com/admin/repos/ . Then click on +https://gerrit-review.googlesource.com/admin/repos/[role=external,window=_blank]. Then click on the individual project, and then click Access. This will bring you to a url that looks like -https://gerrit-review.googlesource.com/admin/repos/gerrit,access +https://gerrit-review.googlesource.com/admin/repos/gerrit,access[role=external,window=_blank] [[system_groups]] == System Groups @@ -218,7 +219,7 @@ `^refs/heads/[a-z]{1,8}` matches all lower case branch names between 1 and 8 characters long. Within a regular expression `.` is a wildcard matching any character, but may be escaped as `\.`. -The link:http://www.brics.dk/automaton/[dk.brics.automaton library] +The link:http://www.brics.dk/automaton/[dk.brics.automaton library,role=external,window=_blank] is used for evaluation of regular expression access control rules. See the library documentation for details on this particular regular expression flavor. One quirk is that the @@ -979,7 +980,7 @@ to build and then leave a verdict somehow. As an example, the popular -link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger[gerrit-trigger plugin] +link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger[gerrit-trigger plugin,role=external,window=_blank] for Jenkins/Hudson can set labels at: * The start of a build
diff --git a/Documentation/backup.txt b/Documentation/backup.txt index 7220c74..dd47035 100644 --- a/Documentation/backup.txt +++ b/Documentation/backup.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - Backup A Gerrit Code Review site contains data that needs to be backed up regularly. @@ -47,7 +48,7 @@ + If you have chosen to use _Elastic Search_ for indexing, refer to its -link:https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshots.html[backup documentation]. +link:https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshots.html[backup documentation,role=external,window=_blank]. [#optional-backup-cache] Caches:: @@ -70,7 +71,7 @@ * public and private SSH host keys + You may consider to use the -link:https://gerrit.googlesource.com/plugins/secure-config/[secure-config plugin] +link:https://gerrit.googlesource.com/plugins/secure-config/[secure-config plugin,role=external,window=_blank] to encrypt these secrets. [#optional-backup-plugin-data] @@ -145,7 +146,7 @@ backup. This means read-access is still available from replica servers during backup, because only write operations have to be stopped to ensure consistency. This can be implemented using the -link:https://gerrit.googlesource.com/plugins/readonly/[_readonly_] plugin. +link:https://gerrit.googlesource.com/plugins/readonly/[_readonly_,role=external,window=_blank] plugin. [#cons-backup-replicate] === Replicate data for backup @@ -157,9 +158,9 @@ Replicate all git repositories to another file system using `git clone --mirror`, or the -link:https://gerrit.googlesource.com/plugins/replication[replication plugin] +link:https://gerrit.googlesource.com/plugins/replication[replication plugin,role=external,window=_blank] or the -link:https://gerrit.googlesource.com/plugins/pull-replication[pull-replication plugin]. +link:https://gerrit.googlesource.com/plugins/pull-replication[pull-replication plugin,role=external,window=_blank]. Best you use a filesystem supporting snapshots to create a backup archive of such a replica. @@ -174,7 +175,7 @@ Do not skip backing up the replica, the replica alone IS NOT a backup. Imagine someone deleted a project by mistake and this deletion got replicated. Replication of repository deletions can be switched off using the -link:https://gerrit.googlesource.com/plugins/replication/+/refs/heads/master/src/main/resources/Documentation/config.md[server option] +link:https://gerrit.googlesource.com/plugins/replication/+/refs/heads/master/src/main/resources/Documentation/config.md[server option,role=external,window=_blank] `remote.NAME.replicateProjectDeletions`. If you are using Gerrit replica to offload read traffic you can use one of these @@ -197,13 +198,13 @@ Filesystems supporting copy on write snapshots:: + Use a file system supporting copy-on-write snapshots like -link:https://btrfs.wiki.kernel.org/index.php/SysadminGuide#Snapshots[btrfs] +link:https://btrfs.wiki.kernel.org/index.php/SysadminGuide#Snapshots[btrfs,role=external,window=_blank] or -https://wiki.debian.org/ZFS#Snapshots[zfs]. +https://wiki.debian.org/ZFS#Snapshots[zfs,role=external,window=_blank]. Other filesystems supporting snapshots:: -https://wiki.archlinux.org/index.php/LVM#Snapshots[lvm] or nfs. +https://wiki.archlinux.org/index.php/LVM#Snapshots[lvm,role=external,window=_blank] or nfs. + Create a snapshot and then archive the snapshot to another storage. + @@ -259,7 +260,7 @@ [#backup-dr-multi-site] === Multi-site setup -Use the https://gerrit.googlesource.com/plugins/multi-site[multi-site plugin] +Use the https://gerrit.googlesource.com/plugins/multi-site[multi-site plugin,role=external,window=_blank] to install Gerrit with multiple sites installed in different datacenters across different regions. This ensures that in case of a severe problem with one of the sites, the other sites can still serve your repositories.
diff --git a/Documentation/cmd-hook-commit-msg.txt b/Documentation/cmd-hook-commit-msg.txt index 49d5c17..2b6d7af 100644 --- a/Documentation/cmd-hook-commit-msg.txt +++ b/Documentation/cmd-hook-commit-msg.txt
@@ -1,3 +1,4 @@ +:linkattrs: = commit-msg Hook @@ -86,8 +87,8 @@ * link:user-changeid.html[Change-Id Lines] -* link:http://www.kernel.org/pub/software/scm/git/docs/git-commit.html[git-commit(1)] -* link:http://www.kernel.org/pub/software/scm/git/docs/githooks.html[githooks(5)] +* link:http://www.kernel.org/pub/software/scm/git/docs/git-commit.html[git-commit(1),role=external,window=_blank] +* link:http://www.kernel.org/pub/software/scm/git/docs/githooks.html[githooks(5),role=external,window=_blank] == IMPLEMENTATION
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt index edb54b5..c991a2a 100644 --- a/Documentation/cmd-index.txt +++ b/Documentation/cmd-index.txt
@@ -178,6 +178,9 @@ link:cmd-set-account.html[gerrit set-account]:: Change an account's settings. +link:cmd-sequence-show.html[gerrit sequence show]:: + Display current sequence value. + link:cmd-set-members.html[gerrit set-members]:: Set group members.
diff --git a/Documentation/cmd-sequence-set.txt b/Documentation/cmd-sequence-set.txt new file mode 100644 index 0000000..9023ceb --- /dev/null +++ b/Documentation/cmd-sequence-set.txt
@@ -0,0 +1,54 @@ += gerrit sequence set + +== NAME +gerrit sequence set - Set new sequence value. + +== SYNOPSIS +[verse] +-- +_ssh_ -p <port> <host> _gerrit sequence set_ <NAME> <VALUE> +-- + +== DESCRIPTION +Gerrit maintains the generation of the next available sequence numbers for +account, group and change entities. The sequences are stored as UTF-8 text in +a blob pointed to by the `refs/sequences/accounts`, `refs/sequences/groups` +and `refs/sequences/changes` refs. Those refs are stored in `All-Users` and +`All-Projects` git repositories correspondingly. + +This command allows to set a new sequence value for those sequences. + +The link:cmd-sequence-show.html[sequence-show] command displays current +sequence value. + +== ACCESS +Caller must be a member of the privileged 'Administrators' group. + +== SCRIPTING +This command is intended to be used in scripts. + +== OPTIONS +<NAME>:: + Sequence name to set a new value for. + Currently supported values: + * accounts + * groups + * changes + +<VALUE>:: + New value for the sequence. + +== EXAMPLES +Set a new value for the 'changes' sequence: + +---- +$ ssh -p 29418 review.example.com gerrit sequence set changes 42 +The value for the changes sequence was set to 42. +---- + +GERRIT +------ +Part of link:index.html[Gerrit Code Review] + +SEARCHBOX +---------
diff --git a/Documentation/cmd-sequence-show.txt b/Documentation/cmd-sequence-show.txt new file mode 100644 index 0000000..6b9371f --- /dev/null +++ b/Documentation/cmd-sequence-show.txt
@@ -0,0 +1,51 @@ += gerrit sequence show + +== NAME +gerrit sequence show - Display current sequence value. + +== SYNOPSIS +[verse] +-- +_ssh_ -p <port> <host> _gerrit sequence show_ <NAME> +-- + +== DESCRIPTION +Gerrit maintains the generation of the next available sequence numbers for +account, group and change entities. The sequences are stored as UTF-8 text in +a blob pointed to by the `refs/sequences/accounts`, `refs/sequences/groups` +and `refs/sequences/changes` refs. Those refs are stored in `All-Users` and +`All-Projects` git repositories correspondingly. + +This command allows to display the current sequence value for those sequences. + +The link:cmd-sequence-set.html[sequence-set] command allows to set a new +sequence value. + +== ACCESS +Caller must be a member of the privileged 'Administrators' group. + +== SCRIPTING +This command is intended to be used in scripts. + +== OPTIONS +<NAME>:: + Sequence name to show the current value for. + Currently supported values: + * accounts + * groups + * changes + +== EXAMPLES +Display the current value for the 'changes' sequence: + +---- +$ ssh -p 29418 review.example.com gerrit sequence show changes +42 +---- + +GERRIT +------ +Part of link:index.html[Gerrit Code Review] + +SEARCHBOX +---------
diff --git a/Documentation/concept-refs-for-namespace.txt b/Documentation/concept-refs-for-namespace.txt index c8776ae..003a106 100644 --- a/Documentation/concept-refs-for-namespace.txt +++ b/Documentation/concept-refs-for-namespace.txt
@@ -1,7 +1,8 @@ +:linkattrs: = The refs/for namespace When pushing a new or updated commit to Gerrit, you push that commit using a -link:https://www.kernel.org/pub/software/scm/git/docs/gitglossary.html#def_ref[reference], +link:https://www.kernel.org/pub/software/scm/git/docs/gitglossary.html#def_ref[reference,role=external,window=_blank], in the `refs/for` namespace. This reference must also define the target branch, such as `refs/for/[BRANCH_NAME]`.
diff --git a/Documentation/config-accounts.txt b/Documentation/config-accounts.txt index 90150b1..b1ce1ce 100644 --- a/Documentation/config-accounts.txt +++ b/Documentation/config-accounts.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - Accounts == Overview @@ -143,7 +144,7 @@ In addition it contains an link:https://en.wikibooks.org/wiki/OpenSSH/Client_Configuration_Files#.7E.2F.ssh.2Fauthorized_keys[ -authorized_keys] file with the link:#ssh-keys[SSH keys] of the account. +authorized_keys,role=external,window=_blank] file with the link:#ssh-keys[SSH keys] of the account. [[account-properties]] === Account Properties @@ -256,7 +257,7 @@ SSH keys are stored in the user branch in an `authorized_keys` file, which is the link:https://en.wikibooks.org/wiki/OpenSSH/Client_Configuration_Files#.7E.2F.ssh.2Fauthorized_keys[ -standard OpenSSH file format] for storing SSH keys: +standard OpenSSH file format,role=external,window=_blank] for storing SSH keys: ---- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCgug5VyMXQGnem2H1KVC4/HcRcD4zzBqSuJBRWVonSSoz3RoAZ7bWXCVVGwchtXwUURD689wFYdiPecOrWOUgeeyRq754YWRhU+W28vf8IZixgjCmiBhaL2gt3wff6pP+NXJpTSA4aeWE5DfNK5tZlxlSxqkKOS8JRSUeNQov5Tw== john.doe@example.com
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt index cf255f3..880626e 100644 --- a/Documentation/config-gerrit.txt +++ b/Documentation/config-gerrit.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - Configuration == File `etc/gerrit.config` @@ -174,7 +175,7 @@ + The default setting. Gerrit uses any valid OpenID provider chosen by the end-user. For more information see -http://openid.net/[openid.net]. +http://openid.net/[openid.net,role=external,window=_blank]. + * `OpenID_SSO` + @@ -286,7 +287,7 @@ + Patterns may be either a link:http://download.oracle.com/javase/6/docs/api/java/util/regex/Pattern.html[standard -Java regular expression (java.util.regex)] (start with `^` and +Java regular expression (java.util.regex),role=external,window=_blank] (start with `^` and end with `$`) or be a simple prefix (any other string). + By default, the list contains two values, `http://` and `https://`, @@ -304,7 +305,7 @@ + Patterns may be either a link:http://download.oracle.com/javase/6/docs/api/java/util/regex/Pattern.html[standard -Java regular expression (java.util.regex)] (start with `^` and +Java regular expression (java.util.regex),role=external,window=_blank] (start with `^` and end with `$`) or be a simple prefix (any other string). + By default, the list contains two values, `http://` and `https://`, @@ -695,7 +696,7 @@ + Technically the H2 cache size is configured using the CACHE_SIZE parameter in the H2 JDBC connection URL, as described -link:http://www.h2database.com/html/features.html#cache_settings[here] +link:http://www.h2database.com/html/features.html#cache_settings[here,role=external,window=_blank] + Default is unset, using up to half of the available memory. + @@ -708,7 +709,7 @@ If set to true, enable H2 autoserver mode for the H2-backed persistent cache databases. + -See link:http://www.h2database.com/html/features.html#auto_mixed_mode[here] +See link:http://www.h2database.com/html/features.html#auto_mixed_mode[here,role=external,window=_blank] for detail. + Default is false. @@ -1392,9 +1393,15 @@ [[changeCleanup.abandonIfMergeable]]changeCleanup.abandonIfMergeable:: + -Whether changes which are mergeable should be auto-abandoned. +Whether changes which are mergeable should be auto-abandoned. When set +to `false`, `-is:mergeable` is appended to the query used to find +the changes to auto-abandon. + -By default `true`. +By default `true`, meaning mergeable changes are auto-abandoned. ++ +If link:#index.change.indexMergeable[`index.change.indexMergeable`] +is disabled, setting this option to `false` has no effect and it +behaves as though it were set to `true`. [[changeCleanup.cleanupAccountPatchReview]]changeCleanup.cleanupAccountPatchReview:: + @@ -1708,7 +1715,7 @@ + As explained in this link:http://codicesoftware.blogspot.com/2011/09/merge-recursive-strategy.html[ -blog], the recursive merge produces better results if the two commits +blog,role=external,window=_blank], the recursive merge produces better results if the two commits that are merged have more than one common predecessor. + Default is true. @@ -2241,9 +2248,9 @@ branch names. Some web servers, such as Tomcat, reject this hexadecimal encoding in the URL. + -Some alternative gitweb services, such as link:http://gitblit.com[Gitblit], +Some alternative gitweb services, such as link:http://gitblit.com[Gitblit,role=external,window=_blank], allow using an alternative path separator character. In Gitblit, this can be -configured through the property link:http://gitblit.com/properties.html[web.forwardSlashCharacter]. +configured through the property link:http://gitblit.com/properties.html[web.forwardSlashCharacter,role=external,window=_blank]. In Gerrit, the alternative path separator can be configured correspondingly using the property `gitweb.pathSeparator`. + @@ -2393,7 +2400,7 @@ Like `http://`, but additional header parsing features are enabled to honor `X-Forwarded-For`, `X-Forwarded-Host` and `X-Forwarded-Server`. These headers are typically set by Apache's -link:https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers[mod_proxy]. +link:https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers[mod_proxy,role=external,window=_blank]. + [NOTE] -- @@ -2668,6 +2675,28 @@ filterClass = org.anyorg.MySecureIPFilter ---- +[[filterClass.className.initParam]]filterClass.<className>.initParam:: ++ +Gerrit supports customized pluggable HTTP filters as `filterClass`. This +option allows to pass extra initialization parameters to the filter. It +allows for multiple key/value pairs to be passed in this pattern: ++ +---- +initParam = <key>=<value> +---- +For a comprehensive example: ++ +---- +[httpd] + filterClass = org.anyorg.AFilter + filterClass = org.anyorg.BFilter +[filterClass "org.anyorg.AFilter"] + key1 = value1 + key2 = value2 +[filterClass "org.anyorg.BFilter"] + key3 = value3 +---- + [[httpd.idleTimeout]]httpd.idleTimeout:: + Maximum idle time for a connection, which roughly translates to the @@ -2722,7 +2751,7 @@ + * `ELASTICSEARCH` look into link:#elasticsearch[Elasticsearch section] + -An link:https://www.elastic.co/products/elasticsearch[Elasticsearch] index is +An link:https://www.elastic.co/products/elasticsearch[Elasticsearch,role=external,window=_blank] index is used. Refer to the link:#elasticsearch[Elasticsearch section] for further configuration details. @@ -2746,6 +2775,22 @@ If not set or set to a zero, defaults to the number of logical CPUs as returned by the JVM. If set to a negative value, defaults to a direct executor. +[[index.change.indexMergeable]]index.change.indexMergeable:: ++ +Specifies if `mergeable` should be index or not. Indexing this field enables +queries that contain the mergeability operator (`is:mergeable`). If enabled, +Gerrit will check if the change is mergeable into the target branch when +reindexing a change. This is an expensive operation. ++ +If true, Gerrit will reindex all open changes when the target ref advances. +Depending on the frequency of updates to the ref and the number of open changes, +this can be very expensive. ++ +When this setting is changed from `false` to `true`, all changes need to be +reindexed. ++ +Defaults to true. + [[index.onlineUpgrade]]index.onlineUpgrade:: + Whether to upgrade to new index schema versions while the server is @@ -2793,19 +2838,6 @@ + Defaults to 1024. -[[index.reindexAfterRefUpdate]]index.reindexAfterRefUpdate:: -+ -Whether to reindex all affected open changes after a ref is updated. This -includes reindexing all open changes to recompute the "mergeable" bit every time -the destination branch moves, as well as reindexing changes to take into account -new project configuration (e.g. label definitions). -+ -Leaving this enabled may result in fresher results, but may cause performance -problems if there are lots of open changes on a project whose branches advance -frequently. -+ -Defaults to true. - [[index.autoReindexIfStale]]index.autoReindexIfStale:: + Whether to automatically check if a document became stale in the index @@ -2883,7 +2915,7 @@ Determines the amount of RAM that may be used for buffering added documents and deletions before they are flushed to the index. See the link:http://lucene.apache.org/core/4_6_0/core/org/apache/lucene/index/LiveIndexWriterConfig.html#setRAMBufferSizeMB(double)[ -Lucene documentation] for further details. +Lucene documentation,role=external,window=_blank] for further details. + Defaults to 16M. @@ -2893,7 +2925,7 @@ in-memory documents are flushed to the index. Large values generally give faster indexing. See the link:http://lucene.apache.org/core/4_6_0/core/org/apache/lucene/index/LiveIndexWriterConfig.html#setMaxBufferedDocs(int)[ -Lucene documentation] for further details. +Lucene documentation,role=external,window=_blank] for further details. + Defaults to -1, meaning no maximum is set and the writer will flush according to RAM usage. @@ -2926,7 +2958,7 @@ completed. Note that Lucene will only run the smallest maxThreadCount merges at a time. See the link:https://lucene.apache.org/core/5_5_0/core/org/apache/lucene/index/ConcurrentMergeScheduler.html#setDefaultMaxMergesAndThreads(boolean)[ -Lucene documentation] for further details. +Lucene documentation,role=external,window=_blank] for further details. + Defaults to -1 for (auto detection). @@ -2936,13 +2968,13 @@ Determines the max number of simultaneous Lucene merge threads that should be running at once. This must be less than or equal to maxMergeCount. See the link:https://lucene.apache.org/core/5_5_0/core/org/apache/lucene/index/ConcurrentMergeScheduler.html#setDefaultMaxMergesAndThreads(boolean)[ -Lucene documentation] for further details. +Lucene documentation,role=external,window=_blank] for further details. + For further details on Lucene index configuration (auto detection) which affects maxThreadCount and maxMergeCount settings. See the link:https://lucene.apache.org/core/5_5_0/core/org/apache/lucene/index/ConcurrentMergeScheduler.html#AUTO_DETECT_MERGES_AND_THREADS[ -Lucene documentation] +Lucene documentation,role=external,window=_blank] + Defaults to -1 for (auto detection). @@ -2953,7 +2985,7 @@ on is used to adaptively rate limit writes bytes/sec to the minimal rate necessary so merges do not fall behind. See the link:https://lucene.apache.org/core/5_5_0/core/org/apache/lucene/index/ConcurrentMergeScheduler.html#enableAutoIOThrottle()[ -Lucene documentation] for further details. +Lucene documentation,role=external,window=_blank] for further details. + Defaults to true (throttling enabled). @@ -2983,7 +3015,7 @@ WARNING: Support for Elasticsearch is still experimental and is not recommended for production use. For compatibility information, please refer to the -link:https://www.gerritcodereview.com/elasticsearch.html[project homepage]. +link:https://www.gerritcodereview.com/elasticsearch.html[project homepage,role=external,window=_blank]. When using Elasticsearch version 5.6, the open and closed changes are indexed in a single index, separated into types `open_changes` and `closed_changes` @@ -3018,7 +3050,7 @@ + Sets the number of shards to use per index. Refer to the link:https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started-concepts.html#getting-started-shards-and-replicas[ -Elasticsearch documentation] for details. +Elasticsearch documentation,role=external,window=_blank] for details. + Defaults to 5 for Elasticsearch versions 5 and 6, and to 1 starting with Elasticsearch 7. @@ -3026,7 +3058,7 @@ + Sets the number of replicas to use per index. Refer to the link:https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started-concepts.html#getting-started-shards-and-replicas[ -Elasticsearch documentation] for details. +Elasticsearch documentation,role=external,window=_blank] for details. + Defaults to 1. @@ -3037,12 +3069,12 @@ For further information about Elasticsearch security, please refer to the documentation: -* link:https://www.elastic.co/guide/en/x-pack/5.6/security-getting-started.html[Elasticsearch 5.6] -* link:https://www.elastic.co/guide/en/x-pack/6.2/security-getting-started.html[Elasticsearch 6.2] -* link:https://www.elastic.co/guide/en/elastic-stack-overview/6.3/security-getting-started.html[Elasticsearch 6.3] -* link:https://www.elastic.co/guide/en/elastic-stack-overview/6.4/security-getting-started.html[Elasticsearch 6.4] -* link:https://www.elastic.co/guide/en/elastic-stack-overview/6.5/security-getting-started.html[Elasticsearch 6.5] -* link:https://www.elastic.co/guide/en/elastic-stack-overview/6.6/security-getting-started.html[Elasticsearch 6.6] +* link:https://www.elastic.co/guide/en/x-pack/5.6/security-getting-started.html[Elasticsearch 5.6,role=external,window=_blank] +* link:https://www.elastic.co/guide/en/x-pack/6.2/security-getting-started.html[Elasticsearch 6.2,role=external,window=_blank] +* link:https://www.elastic.co/guide/en/elastic-stack-overview/6.3/security-getting-started.html[Elasticsearch 6.3,role=external,window=_blank] +* link:https://www.elastic.co/guide/en/elastic-stack-overview/6.4/security-getting-started.html[Elasticsearch 6.4,role=external,window=_blank] +* link:https://www.elastic.co/guide/en/elastic-stack-overview/6.5/security-getting-started.html[Elasticsearch 6.5,role=external,window=_blank] +* link:https://www.elastic.co/guide/en/elastic-stack-overview/6.6/security-getting-started.html[Elasticsearch 6.6,role=external,window=_blank] [[elasticsearch.username]]elasticsearch.username:: + @@ -3093,8 +3125,8 @@ the parameters introduced here. Suitable defaults for most parameters are automatically guessed based on the type of server detected during startup. The guessed defaults support -link:http://www.ietf.org/rfc/rfc2307.txt[RFC 2307], Active -Directory and link:https://www.freeipa.org[FreeIPA]. +link:http://www.ietf.org/rfc/rfc2307.txt[RFC 2307,role=external,window=_blank], Active +Directory and link:https://www.freeipa.org[FreeIPA,role=external,window=_blank]. ---- [ldap] @@ -3448,8 +3480,8 @@ garbage collected), the connection is returned to the pool for future use. + For details, see link:http://docs.oracle.com/javase/tutorial/jndi/ldap/pool.html[ -LDAP connection management (Pool)] and link:http://docs.oracle.com/javase/tutorial/jndi/ldap/config.html[ -LDAP connection management (Configuration)] +LDAP connection management (Pool),role=external,window=_blank] and link:http://docs.oracle.com/javase/tutorial/jndi/ldap/config.html[ +LDAP connection management (Configuration),role=external,window=_blank] + By default, false. @@ -3468,7 +3500,7 @@ ldap.useConnectionPooling] configuration property to `true`, the connection pool can be configured using JVM system properties as explained in the link:http://docs.oracle.com/javase/7/docs/technotes/guides/jndi/jndi-ldap.html#POOL[ -Java SE Documentation]. +Java SE Documentation,role=external,window=_blank]. For standalone Gerrit (running with the embedded Jetty), JVM system properties are specified in the link:#container[container section]: @@ -3486,7 +3518,7 @@ + The name of a plugin which serves the link:https://github.com/github/git-lfs/blob/master/docs/api/v1/http-v1-batch.md[ -LFS protocol] on the `<project-name>/info/lfs/objects/batch` endpoint. When +LFS protocol,role=external,window=_blank] on the `<project-name>/info/lfs/objects/batch` endpoint. When not configured Gerrit will respond with `501 Not Implemented` on LFS protocol requests. + @@ -3596,6 +3628,39 @@ + Default is false. +[[operator-alias]] +=== Section operator alias + +Operator aliasing allows global aliases to be defined for query operators. +Currently only change queries are supported. The alias name is the git +config key name, and the operator being aliased is the git config value. + +For example: + +---- +[operator-alias "change"] + oldage = age + number = change +---- + +This section is particularly useful to alias operator names which may be +long and clunky because they include a plugin name in them to a shorter +name without the plugin name. + +Aliases are resolved dynamically at invocation time to any currently +loaded versions of plugins. If the alias points to an operator provided +by a plugin which is not currently loaded, or the plugin does not define +the operator, then "unsupported operator" is returned to the user. + +Aliases will override existing operators. In the case of multiple aliases +with the same name, the last one defined will be used. + +When the target of an alias doesn't exist, the operator with the name +of the alias will be used (if present). This enables an admin to config +the system to override a core operator with an operator provided by a +plugin when present and otherwise fall back to the operator provided by +core. + [[pack]] === Section pack @@ -3750,6 +3815,19 @@ + Default is true. +[[receive.enableInMemoryRefCache]]receive.enableInMemoryRefCache:: ++ +If true, Gerrit will cache all refs advertised during push in memory and +base later receive operations on that cache. ++ +Turning this cache off is considered experimental. ++ +This cache provides value when the ref database is slow and/or does not +offer an inverse lookup of object ID to ref name. When RefTable is used, +this cache can be turned off (experimental) to get speed improvements. ++ +Default is true. + [[receive.enableSignedPush]]receive.enableSignedPush:: + If true, server-side signed push validation is enabled. @@ -3851,7 +3929,7 @@ + Trust signatures can be added to a key using the `tsign` command to link:https://www.gnupg.org/documentation/manuals/gnupg/OpenPGP-Key-Management.html[ -`gpg --edit-key`], after which the signed key should be re-uploaded. +`gpg --edit-key`,role=external,window=_blank], after which the signed key should be re-uploaded. + If no keys are specified, web-of-trust checks are disabled. This is the default behavior. @@ -4304,7 +4382,7 @@ [[sendemail.allowTLD]]sendemail.allowTLD:: + List of custom TLDs to allow sending emails to in addition to those specified -in the link:http://data.iana.org/TLD/[IANA list]. +in the link:http://data.iana.org/TLD/[IANA list,role=external,window=_blank]. + Defaults to an empty list, meaning no additional TLDs are allowed. @@ -4814,7 +4892,7 @@ [[trackingid.name.match]]trackingid.<name>.match:: + A link:http://download.oracle.com/javase/6/docs/api/java/util/regex/Pattern.html[standard -Java regular expression (java.util.regex)] used to match the +Java regular expression (java.util.regex),role=external,window=_blank] used to match the external tracking id part of the footer line. The match can result in several entries in the DB. If grouping is used in the regex the first group will be interpreted as the tracking id.
diff --git a/Documentation/config-gitweb.txt b/Documentation/config-gitweb.txt index d49acfee..46c9ced 100644 --- a/Documentation/config-gitweb.txt +++ b/Documentation/config-gitweb.txt
@@ -1,3 +1,4 @@ +:linkattrs: == Gitweb Integration Gerrit Code Review can manage and generate hyperlinks to gitweb, @@ -167,21 +168,21 @@ Instructions are available for installing the gitweb module distributed with MsysGit: -link:https://github.com/msysgit/msysgit/wiki/GitWeb[GitWeb] +link:https://github.com/msysgit/msysgit/wiki/GitWeb[GitWeb,role=external,window=_blank] If you don't have Apache installed, you can download the appropriate build for Windows from link:http://www.apachelounge.com/download[apachelounge.org]. After you have installed Apache, you will want to create a link:http://httpd.apache.org/docs/2.0/platform/windows.html#winsvc[new service user -account] to use with Apache. +account,role=external,window=_blank] to use with Apache. If you're still having difficulty setting up permissions, you may find this tech note useful for configuring Apache Service to run under another account. -You must grant the new account link:http://technet.microsoft.com/en-us/library/cc794944(WS.10).aspx["run as service"] permission: +You must grant the new account link:http://technet.microsoft.com/en-us/library/cc794944(WS.10).aspx["run as service",role=external,window=_blank] permission: The gitweb version in msysgit is missing several important and required perl modules, including CGI.pm. The perl included with the msysgit distro 1.7.8 -is broken.. The link:http://groups.google.com/group/msysgit/browse_thread/thread/ba3501f1f0ed95af[unicore folder is missing along with utf8_heavy.pl and CGI.pm]. You can +is broken.. The link:http://groups.google.com/group/msysgit/browse_thread/thread/ba3501f1f0ed95af[unicore folder is missing along with utf8_heavy.pl and CGI.pm,role=external,window=_blank]. You can verify by checking for perl modules. From an msys console, execute the following to check: @@ -202,7 +203,7 @@ If you're missing CGI.pm, you'll have to deploy the module to the msys environment: You will have to retrieve them from the 5.8.8 distro on : -http://strawberryperl.com/releases.html +http://strawberryperl.com/releases.html[role=external,window=_blank] File: strawberry-perl-5.8.8.3.zip @@ -272,7 +273,7 @@ === SEE ALSO * link:config-gerrit.html#gitweb[Section gitweb] -* link:http://git.zx2c4.com/cgit/about/[cgit] +* link:http://git.zx2c4.com/cgit/about/[cgit,role=external,window=_blank] GERRIT ------
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt index 835ec11..7d8112c 100644 --- a/Documentation/config-hooks.txt +++ b/Documentation/config-hooks.txt
@@ -1,9 +1,10 @@ +:linkattrs: = Gerrit Code Review - Hooks Gerrit does not run any of the standard git hooks in the repositories it works with, but it does have its own hook mechanism included via the link:https://gerrit-review.googlesource.com/admin/repos/plugins/hooks[ -hooks plugin]. +hooks plugin,role=external,window=_blank]. GERRIT ------
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt index 7d46e26..5d22c0b 100644 --- a/Documentation/config-mail.txt +++ b/Documentation/config-mail.txt
@@ -1,6 +1,7 @@ +:linkattrs: = Gerrit Code Review - Mail Templates -Gerrit uses link:https://developers.google.com/closure/templates/[Closure Templates] +Gerrit uses link:https://developers.google.com/closure/templates/[Closure Templates,role=external,window=_blank] (Soy) for the bulk of the standard mails it sends out. There are builtin default templates which are used if they are not overridden. These defaults are also provided as examples so that administrators may copy
diff --git a/Documentation/config-plugins.txt b/Documentation/config-plugins.txt index 464611d..36276ef 100644 --- a/Documentation/config-plugins.txt +++ b/Documentation/config-plugins.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Plugins The Gerrit server functionality can be extended by installing plugins. @@ -25,10 +26,10 @@ If you want to share your plugin under the link:licenses.html#Apache2_0[ Apache License 2.0] you can host your plugin development on the -link:https://gerrit-review.googlesource.com[gerrit-review] Gerrit +link:https://gerrit-review.googlesource.com[gerrit-review,role=external,window=_blank] Gerrit Server. You can request the creation of a new Project by email to the link:https://groups.google.com/forum/#!forum/repo-discuss[Gerrit -mailing list]. You would be assigned as project owner of the new plugin +mailing list,role=external,window=_blank]. You would be assigned as project owner of the new plugin project so that you can submit changes on your own. It is the responsibility of the project owner to maintain the plugin, e.g. to make sure that it works with new Gerrit versions and to create stable @@ -37,11 +38,9 @@ [[core-plugins]] == Core Plugins -Core plugins are packaged within the Gerrit war file and can easily be -installed during the link:pgm-init.html[Gerrit initialization]. - -The core plugins are developed and maintained by the Gerrit maintainers -and the Gerrit community. +link:dev-core-plugins.html[Core plugins] are packaged within the Gerrit +war file and can easily be installed during the link:pgm-init.html[ +Gerrit initialization]. Note that the documentation and configuration links in the list below are to the plugins' master branch. Please refer to the appropriate branch or @@ -53,7 +52,7 @@ CodeMirror plugin for polygerrit. link:https://gerrit-review.googlesource.com/admin/repos/plugins/codemirror-editor[ -Project] | +Project,role=external,window=_blank] | [[commit-message-length-validator]] === commit-message-length-validator @@ -63,11 +62,11 @@ lengths are exceeded. link:https://gerrit-review.googlesource.com/admin/repos/plugins/commit-message-length-validator[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/commit-message-length-validator/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/commit-message-length-validator/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[delete-project]] === delete-project @@ -75,11 +74,11 @@ Provides the ability to delete a project. link:https://gerrit-review.googlesource.com/admin/repos/plugins/delete-project[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/delete-project/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/delete-project/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[download-commands]] === download-commands @@ -88,11 +87,11 @@ download schemes (for downloading via different network protocols). link:https://gerrit-review.googlesource.com/admin/repos/plugins/download-commands[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/download-commands/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/download-commands/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[gitiles]] === gitiles @@ -100,7 +99,7 @@ Plugin running Gitiles alongside a Gerrit server. link:https://gerrit-review.googlesource.com/admin/repos/plugins/gitiles[ -Project] +Project,role=external,window=_blank] [[hooks]] === hooks @@ -108,11 +107,11 @@ This plugin runs server-side hooks on events. link:https://gerrit-review.googlesource.com/admin/repos/plugins/hooks[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/hooks/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/hooks/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[plugin-manager]] === plugin-manager @@ -122,11 +121,11 @@ this can be changed per plugin configuration. link:https://gerrit-review.googlesource.com/admin/repos/plugins/plugin-manager[ -Project] +Project,role=external,window=_blank] link:https://gerrit.googlesource.com/plugins/plugin-manager/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] +Documentation,role=external,window=_blank] link:https://gerrit.googlesource.com/plugins/plugin-manager/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[replication]] === replication @@ -137,11 +136,11 @@ backups, or a load-balanced public mirror farm. link:https://gerrit-review.googlesource.com/admin/repos/plugins/replication[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/replication/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/replication/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[reviewnotes]] === reviewnotes @@ -150,9 +149,9 @@ branch. link:https://gerrit-review.googlesource.com/admin/repos/plugins/reviewnotes[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/reviewnotes/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] +Documentation,role=external,window=_blank] [[singleusergroup]] === singleusergroup @@ -167,11 +166,11 @@ This plugin allows to propagate Gerrit events to remote http endpoints. link:https://gerrit-review.googlesource.com/admin/repos/plugins/webhooks[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/webhooks/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/webhooks/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[other-plugins]] == Other Plugins @@ -185,12 +184,12 @@ there is one public service that offers the download of pre-built plugin jars: -* link:https://gerrit-ci.gerritforge.com[CI Server from GerritForge] +* link:https://gerrit-ci.gerritforge.com[CI Server from GerritForge,role=external,window=_blank] The following list gives an overview of available plugins, but the list may not be complete. You may discover more plugins on link:https://gerrit-review.googlesource.com/admin/repos/?filter=plugins%252F[ -gerrit-review]. +gerrit-review,role=external,window=_blank]. Note that the documentation and configuration links in the list below are to the plugins' master branch. Please refer to the appropriate branch for @@ -207,9 +206,9 @@ project/account. link:https://gerrit-review.googlesource.com/admin/repos/plugins/admin-console[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/admin-console/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] +Documentation,role=external,window=_blank] [[analytics]] === analytics @@ -220,8 +219,8 @@ archived and processed with popular BigData transformation tools such Apache Spark or published and visualized in dashboards. -link:https://gerrit-review.googlesource.com/admin/repos/plugins/analytics[Project] | -link:https://gerrit.googlesource.com/plugins/analytics/+doc/master/README.md[Documentation] +link:https://gerrit-review.googlesource.com/admin/repos/plugins/analytics[Project,role=external,window=_blank] | +link:https://gerrit.googlesource.com/plugins/analytics/+doc/master/README.md[Documentation,role=external,window=_blank] [[avatars-external]] === avatars-external @@ -230,11 +229,11 @@ from. link:https://gerrit-review.googlesource.com/admin/repos/plugins/avatars-external[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/avatars-external/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/avatars-external/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[avatars-gravatar]] === avatars-gravatar @@ -242,7 +241,7 @@ Plugin to display user icons from Gravatar. link:https://gerrit-review.googlesource.com/admin/repos/plugins/avatars-gravatar[ -Project] +Project,role=external,window=_blank] [[branch-network]] === branch-network @@ -252,11 +251,11 @@ "project link" in a gitweb configuration. link:https://gerrit-review.googlesource.com/admin/repos/plugins/branch-network[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/branch-network/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/branch-network/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[changemessage]] === changemessage @@ -264,11 +263,11 @@ This plugin allows to display a static info message on the change screen. link:https://gerrit-review.googlesource.com/admin/repos/plugins/changemessage[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/changemessage/+doc/master/src/main/resources/Documentation/about.md[ -Plugin Documentation] | +Plugin Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/changemessage/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[checks]] === checks @@ -277,9 +276,9 @@ CI systems with Gerrit. link:https://gerrit-review.googlesource.com/admin/repos/plugins/checks[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/checks/+doc/master/README.md[ -Plugin Documentation] +Plugin Documentation,role=external,window=_blank] [[egit]] === egit @@ -291,9 +290,9 @@ downloading a Gerrit change from within EGit. link:https://gerrit-review.googlesource.com/admin/repos/plugins/egit[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/egit/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] +Documentation,role=external,window=_blank] [[emoticons]] === emoticons @@ -301,11 +300,11 @@ This plugin allows users to see emoticons in comments as images. link:https://gerrit-review.googlesource.com/admin/repos/plugins/emoticons[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/emoticons/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/emoticons/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[find-owners]] === find-owners @@ -314,9 +313,9 @@ (2) Prolog predicates to make sure that a CL is submittable only with owner Code-Review +1 votes. -link:https://gerrit-review.googlesource.com/admin/repos/plugins/find-owners[Project] | -link:https://gerrit.googlesource.com/plugins/find-owners/+doc/master/src/main/resources/Documentation/about.md[Documentation] | -link:https://gerrit.googlesource.com/plugins/find-owners/+doc/master/src/main/resources/Documentation/config.md[Configuration] +link:https://gerrit-review.googlesource.com/admin/repos/plugins/find-owners[Project,role=external,window=_blank] | +link:https://gerrit.googlesource.com/plugins/find-owners/+doc/master/src/main/resources/Documentation/about.md[Documentation,role=external,window=_blank] | +link:https://gerrit.googlesource.com/plugins/find-owners/+doc/master/src/main/resources/Documentation/config.md[Configuration,role=external,window=_blank] [[gitblit]] === gitblit @@ -324,7 +323,7 @@ GitBlit code-viewer plugin with SSO and Security Access Control. link:https://gerrit-review.googlesource.com/admin/repos/plugins/gitblit[ -Project] +Project,role=external,window=_blank] [[github]] === github @@ -332,7 +331,7 @@ Plugin to integrate with GitHub: replication, pull-request to Change-Sets link:https://gerrit-review.googlesource.com/admin/repos/plugins/github[ -Project] +Project,role=external,window=_blank] [[healthcheck]] === healthcheck @@ -348,11 +347,11 @@ Gerrit internal metrics and can be published to dashboards. link:https://gerrit-review.googlesource.com/admin/repos/plugins/healthcheck[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/healthcheck/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/healthcheck/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[imagare]] === imagare @@ -360,11 +359,11 @@ The imagare plugin allows Gerrit users to upload and share images. link:https://gerrit-review.googlesource.com/admin/repos/plugins/imagare[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/imagare/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/imagare/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[importer]] === importer @@ -388,9 +387,9 @@ plugin it can be used to rename a project. link:https://gerrit-review.googlesource.com/admin/repos/plugins/importer[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/importer/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] +Documentation,role=external,window=_blank] [[its-plugins]] === Issue Tracker System Plugins @@ -404,11 +403,11 @@ framework for the ITS plugins which is packaged within each ITS plugin. link:https://gerrit-review.googlesource.com/admin/repos/plugins/its-base[ -its-base Project] | +its-base Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/its-base/+doc/master/src/main/resources/Documentation/about.md[ -its-base Documentation] | +its-base Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/its-base/+doc/master/src/main/resources/Documentation/config.md[ -its-base Configuration] +its-base Configuration,role=external,window=_blank] [[its-bugzilla]] ==== its-bugzilla @@ -416,9 +415,9 @@ Plugin to integrate with Bugzilla. link:https://gerrit-review.googlesource.com/admin/repos/plugins/its-bugzilla[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/its-bugzilla/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] +Documentation,role=external,window=_blank] [[its-jira]] ==== its-jira @@ -426,9 +425,9 @@ Plugin to integrate with Jira. link:https://gerrit-review.googlesource.com/admin/repos/plugins/its-jira[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/its-jira/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[its-phabricator]] ==== its-phabricator @@ -436,9 +435,9 @@ Plugin to integrate with Phabricator. link:https://gerrit-review.googlesource.com/admin/repos/plugins/its-phabricator[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/its-phabricator/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[its-rtc]] ==== its-rtc @@ -446,9 +445,9 @@ Plugin to integrate with IBM Rational Team Concert (RTC). link:https://gerrit-review.googlesource.com/admin/repos/plugins/its-rtc[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/its-rtc/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[its-storyboard]] ==== its-storyboard @@ -456,9 +455,9 @@ Plugin to integrate with Storyboard task tracking system. link:https://gerrit-review.googlesource.com/admin/repos/plugins/its-storyboard[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/its-storyboard/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] +Documentation,role=external,window=_blank] [[javamelody]] === javamelody @@ -469,11 +468,11 @@ instrumentation data from Gerrit. link:https://gerrit-review.googlesource.com/admin/repos/plugins/javamelody[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/javamelody/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | https://gerrit.googlesource.com/plugins/javamelody/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[labelui]] === labelui @@ -484,9 +483,9 @@ screen). link:https://gerrit-review.googlesource.com/admin/repos/plugins/labelui[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/labelui/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] +Documentation,role=external,window=_blank] [[menuextender]] === menuextender @@ -495,11 +494,11 @@ additional menu entries from the WebUI. link:https://gerrit-review.googlesource.com/admin/repos/plugins/menuextender[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/menuextender/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/menuextender/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[metrics-reporter-elasticsearch]] === metrics-reporter-elasticsearch @@ -507,7 +506,7 @@ This plugin reports Gerrit metrics to Elasticsearch. link:https://gerrit-review.googlesource.com/admin/repos/plugins/metrics-reporter-elasticsearch[ -Project]. +Project,role=external,window=_blank]. [[metrics-reporter-graphite]] === metrics-reporter-graphite @@ -515,7 +514,7 @@ This plugin reports Gerrit metrics to Graphite. link:https://gerrit-review.googlesource.com/admin/repos/plugins/metrics-reporter-graphite[ -Project]. +Project,role=external,window=_blank]. [[metrics-reporter-jmx]] === metrics-reporter-jmx @@ -523,7 +522,7 @@ This plugin reports Gerrit metrics to JMX. link:https://gerrit-review.googlesource.com/admin/repos/plugins/metrics-reporter-jmx[ -Project]. +Project,role=external,window=_blank]. [[metrics-reporter-prometheus]] === metrics-reporter-prometheus @@ -531,7 +530,7 @@ This plugin exposes Gerrit metrics for consumption by Prometheus. link:https://gerrit-review.googlesource.com/admin/repos/plugins/metrics-reporter-prometheus[ -Project]. +Project,role=external,window=_blank]. [[motd]] === motd @@ -543,11 +542,11 @@ discarded otherwise. link:https://gerrit-review.googlesource.com/admin/repos/plugins/motd[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/motd/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/motd/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[oauth-authentication-provider]] === OAuth authentication provider @@ -565,16 +564,16 @@ * Keycloak * Office365 -link:https://gerrit-review.googlesource.com/admin/repos/plugins/oauth[Project] | -link:https://gerrit.googlesource.com/plugins/oauth/+doc/master/src/main/resources/Documentation/config.md[Configuration] +link:https://gerrit-review.googlesource.com/admin/repos/plugins/oauth[Project,role=external,window=_blank] | +link:https://gerrit.googlesource.com/plugins/oauth/+doc/master/src/main/resources/Documentation/config.md[Configuration,role=external,window=_blank] [[owners]] === owners This plugin provides a Prolog predicate `add_owner_approval/3` that appends `label('Owner-Approval', need(_))` to a provided list. -link:https://gerrit-review.googlesource.com/admin/repos/plugins/owners[Project] | -link:https://gerrit.googlesource.com/plugins/owners/+doc/master/README.md[Documentation] +link:https://gerrit-review.googlesource.com/admin/repos/plugins/owners[Project,role=external,window=_blank] | +link:https://gerrit.googlesource.com/plugins/owners/+doc/master/README.md[Documentation,role=external,window=_blank] [[project-download-commands]] === project-download-commands @@ -586,11 +585,11 @@ inherited download command or remove it by assigning no value to it. link:https://gerrit-review.googlesource.com/admin/repos/plugins/project-download-commands[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/project-download-commands/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/project-download-commands/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[quota]] === quota @@ -602,21 +601,21 @@ can use this plugin to define quotas on project namespaces. link:https://gerrit-review.googlesource.com/admin/repos/plugins/quota[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/quota/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] +Documentation,role=external,window=_blank] link:https://gerrit.googlesource.com/plugins/quota/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[rabbitmq]] === rabbitmq A plugin that publishes Gerrit events to a -link:https://www.rabbitmq.com/[RabbitMQ] exchange. +link:https://www.rabbitmq.com/[RabbitMQ,role=external,window=_blank] exchange. -link:https://gerrit-review.googlesource.com/admin/repos/plugins/rabbitmq[Project] +link:https://gerrit-review.googlesource.com/admin/repos/plugins/rabbitmq[Project,role=external,window=_blank] link:https://gerrit.googlesource.com/plugins/rabbitmq/+/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[readonly]] === readonly @@ -625,11 +624,11 @@ blocking HTTP PUT/POST/DELETE requests, and disabling SSH commands. link:https://gerrit-review.googlesource.com/admin/repos/plugins/readonly[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/readonly/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/readonly/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[ref-protection]] === ref-protection @@ -640,9 +639,9 @@ `refs/backups/` namespace. link:https://gerrit-review.googlesource.com/admin/repos/plugins/ref-protection[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/ref-protection/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] +Documentation,role=external,window=_blank] [[reparent]] === reparent @@ -650,11 +649,11 @@ A plugin that provides project reparenting as a self-service for project owners. link:https://gerrit-review.googlesource.com/admin/repos/plugins/reparent[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/reparent/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/reparent/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[review-strategy]] === review-strategy @@ -662,9 +661,9 @@ This plugin allows users to configure different review strategies. link:https://gerrit-review.googlesource.com/admin/repos/plugins/review-strategy[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/review-strategy/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] +Documentation,role=external,window=_blank] [[reviewers]] === reviewers @@ -672,11 +671,11 @@ A plugin that allows adding default reviewers to a change. link:https://gerrit-review.googlesource.com/admin/repos/plugins/reviewers[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/reviewers/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/reviewers/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[reviewers-by-blame]] === reviewers-by-blame @@ -688,11 +687,11 @@ change. link:https://gerrit-review.googlesource.com/admin/repos/plugins/reviewers-by-blame[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/reviewers-by-blame/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/reviewers-by-blame/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[groovy-provider]] === scripting/groovy-provider @@ -700,16 +699,16 @@ This plugin provides a Groovy runtime environment for Gerrit plugins in Groovy. link:https://gerrit-review.googlesource.com/admin/repos/plugins/scripting/groovy-provider[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/scripting/groovy-provider/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] +Documentation,role=external,window=_blank] [[saml-authentication-provider]] === SAML2 authentication provider This plugin enables Gerrit to use SAML2 protocol for authentication. -link:https://gerrit-review.googlesource.com/admin/repos/plugins/saml[Project] +link:https://gerrit-review.googlesource.com/admin/repos/plugins/saml[Project,role=external,window=_blank] [[scala-provider]] === scripting/scala-provider @@ -717,9 +716,9 @@ This plugin provides a Scala runtime environment for Gerrit plugins in Scala. link:https://gerrit-review.googlesource.com/admin/repos/plugins/scripting/scala-provider[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/scripting/scala-provider/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] +Documentation,role=external,window=_blank] [[scripts]] === scripts @@ -730,8 +729,8 @@ Groovy and Scala scripts require the installation of the corresponding scripting/*-provider plugin in order to be loaded into Gerrit. -link:https://gerrit-review.googlesource.com/admin/repos/plugins/scripts[Project] -link:https://gerrit.googlesource.com/plugins/scripts/+doc/master/README.md[Documentation] +link:https://gerrit-review.googlesource.com/admin/repos/plugins/scripts[Project,role=external,window=_blank] +link:https://gerrit.googlesource.com/plugins/scripts/+doc/master/README.md[Documentation,role=external,window=_blank] [[server-config]] === server-config @@ -743,7 +742,7 @@ get. link:https://gerrit-review.googlesource.com/admin/repos/plugins/server-config[ -Project] +Project,role=external,window=_blank] [[serviceuser]] === serviceuser @@ -756,11 +755,11 @@ WebUI and it cannot push commits or tags. link:https://gerrit-review.googlesource.com/admin/repos/plugins/serviceuser[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/serviceuser/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/serviceuser/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[uploadvalidator]] === uploadvalidator @@ -772,11 +771,11 @@ settings are rejected by Gerrit. link:https://gerrit-review.googlesource.com/admin/repos/plugins/uploadvalidator[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/uploadvalidator/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/uploadvalidator/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[verify-status]] === verify-status @@ -786,11 +785,11 @@ or in a completely separate datastore. link:https://gerrit-review.googlesource.com/admin/repos/plugins/verify-status[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/verify-status/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/verify-status/+doc/master/src/main/resources/Documentation/database.md[ -Configuration] +Configuration,role=external,window=_blank] [[websession-flatfile]] === websession-flatfile @@ -801,11 +800,11 @@ Gerrit installations having multiple primary Gerrit nodes. link:https://gerrit-review.googlesource.com/admin/repos/plugins/websession-flatfile[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/websession-flatfile/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/websession-flatfile/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] [[x-docs]] === x-docs @@ -813,11 +812,11 @@ This plugin serves project documentation as HTML pages. link:https://gerrit-review.googlesource.com/admin/repos/plugins/x-docs[ -Project] | +Project,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/x-docs/+doc/master/src/main/resources/Documentation/about.md[ -Documentation] | +Documentation,role=external,window=_blank] | link:https://gerrit.googlesource.com/plugins/x-docs/+doc/master/src/main/resources/Documentation/config.md[ -Configuration] +Configuration,role=external,window=_blank] GERRIT
diff --git a/Documentation/config-sso.txt b/Documentation/config-sso.txt index 6f3a32d..14399a3 100644 --- a/Documentation/config-sso.txt +++ b/Documentation/config-sso.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - Single Sign-On Security Gerrit supports integration with some types of single sign-on @@ -17,7 +18,7 @@ As this is the default setting there is nothing required from the site administrator to make use of the OpenID authentication services. -* http://openid.net/[openid.net] +* http://openid.net/[openid.net,,role=external,window=_blank] If Jetty is being used, you may need to increase the header buffer size parameter, due to very long header lines. @@ -34,7 +35,7 @@ `auth.trustedOpenID` list in `gerrit.config`. Patterns may be either a link:http://download.oracle.com/javase/6/docs/api/java/util/regex/Pattern.html[standard -Java regular expression (java.util.regex)] (must start with `^` +Java regular expression (java.util.regex),,role=external,window=_blank] (must start with `^` and end with `$`) or be a simple prefix (any other string). Out of the box Gerrit is configured to trust two patterns, which @@ -65,12 +66,12 @@ subsequent attempts to link that account with the existing account will fail. In cases where this happens, the administrator will need to manually merge the accounts. See link:https://gerrit.googlesource.com/homepage/+/md-pages/docs/SqlMergeUserAccounts.md[ -Merging Gerrit User Accounts] on the Gerrit Wiki for details. +Merging Gerrit User Accounts,,role=external,window=_blank] on the Gerrit Wiki for details. Linking another identity is also useful for users whose primary OpenID provider shuts down. For example Google link:https://developers.google.com/+/api/auth-migration[shut down their OpenID -service on 20th April 2015]. Users who failed to add an alternative identity with +service on 20th April 2015,,role=external,window=_blank]. Users who failed to add an alternative identity with another OpenID provider before that date will end up with their account only having a disabled Google identity. After creating a separate account with an alternative provider, they will need to ask the administrator to merge the accounts using the
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt index 4964568..036dfbf 100644 --- a/Documentation/dev-bazel.txt +++ b/Documentation/dev-bazel.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - Building with Bazel [[installation]] @@ -8,10 +9,10 @@ * A Linux or macOS system (Windows is not supported at this time) * A JDK for Java 8|9|10|11|... * Python 2 or 3 -* link:https://github.com/nodesource/distributions/blob/master/README.md[Node.js (including npm)] +* link:https://github.com/nodesource/distributions/blob/master/README.md[Node.js (including npm),role=external,window=_blank] * Bower (`sudo npm install -g bower`) -* link:https://docs.bazel.build/versions/master/install.html[Bazel] directly -or through link:https://github.com/bazelbuild/bazelisk[Bazelisk] +* link:https://docs.bazel.build/versions/master/install.html[Bazel,role=external,window=_blank] directly +or through link:https://github.com/bazelbuild/bazelisk[Bazelisk,role=external,window=_blank] * Maven * zip, unzip * gcc @@ -32,17 +33,17 @@ `java -version` -[[java-12]] -==== Java 12 support +[[java-13]] +==== Java 13 support -Java 12 (and newer) is supported through vanilla java toolchain -link:https://docs.bazel.build/versions/master/toolchains.html[Bazel option]. -To build Gerrit with Java 12 and newer, specify vanilla java toolchain and +Java 13 (and newer) is supported through vanilla java toolchain +link:https://docs.bazel.build/versions/master/toolchains.html[Bazel option,role=external,window=_blank]. +To build Gerrit with Java 13 and newer, specify vanilla java toolchain and provide the path to JDK home: ``` $ bazel build \ - --define=ABSOLUTE_JAVABASE=<path-to-java-12> \ + --define=ABSOLUTE_JAVABASE=<path-to-java-13> \ --javabase=@bazel_tools//tools/jdk:absolute_javabase \ --host_javabase=@bazel_tools//tools/jdk:absolute_javabase \ --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_vanilla \ @@ -55,7 +56,7 @@ ``` $ bazel test \ - --define=ABSOLUTE_JAVABASE=<path-to-java-12> \ + --define=ABSOLUTE_JAVABASE=<path-to-java-13> \ --javabase=@bazel_tools//tools/jdk:absolute_javabase \ --host_javabase=@bazel_tools//tools/jdk:absolute_javabase \ --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_vanilla \ @@ -68,7 +69,7 @@ ``` $ cat << EOF > ~/.bazelrc -> build --define=ABSOLUTE_JAVABASE=<path-to-java-12> +> build --define=ABSOLUTE_JAVABASE=<path-to-java-13> > build --javabase=@bazel_tools//tools/jdk:absolute_javabase > build --host_javabase=@bazel_tools//tools/jdk:absolute_javabase > build --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_vanilla @@ -96,7 +97,7 @@ ``` === Node.js and npm packages -See link:https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/README.md#installing-node_js-and-npm-packages[Installing Node.js and npm packages]. +See link:https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/README.md#installing-node_js-and-npm-packages[Installing Node.js and npm packages,role=external,window=_blank]. [[build]] == Building on the Command Line @@ -212,7 +213,7 @@ === IntelliJ -The Gerrit build works with Bazel's link:https://ij.bazel.build[IntelliJ plugin]. +The Gerrit build works with Bazel's link:https://ij.bazel.build[IntelliJ plugin,role=external,window=_blank]. Please follow the instructions on <<dev-intellij#,IntelliJ Setup>>. === Eclipse @@ -232,7 +233,7 @@ If an updated classpath is needed, the Eclipse project can be refreshed and missing dependency JARs can be downloaded by running `project.py` again. For IntelliJ, you need to click the `Sync Project -with BUILD Files` button of link:https://ij.bazel.build[Bazel plugin]. +with BUILD Files` button of link:https://ij.bazel.build[Bazel plugin,role=external,window=_blank]. [[documentation]] === Documentation @@ -342,17 +343,17 @@ Successfully running the Elasticsearch tests requires Docker, and may require setting the local virtual memory on -link:https://www.elastic.co/guide/en/elasticsearch/reference/current/vm-max-map-count.html[linux] and -link:https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html#_set_vm_max_map_count_to_at_least_262144[macOS]. +link:https://www.elastic.co/guide/en/elasticsearch/reference/current/vm-max-map-count.html[linux,role=external,window=_blank] and +link:https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html#_set_vm_max_map_count_to_at_least_262144[macOS,role=external,window=_blank]. -On macOS, if using link:https://docs.docker.com/docker-for-mac/[Docker Desktop], +On macOS, if using link:https://docs.docker.com/docker-for-mac/[Docker Desktop,role=external,window=_blank], the effective memory value can be set in the Preferences, under the Advanced tab. The default value usually does not suffice and is causing premature container exits. That default is currently 2 GB and should be set to at least 5 (GB). If Docker is not available, the Elasticsearch tests will be skipped. Note that Bazel currently does not show -link:https://github.com/bazelbuild/bazel/issues/3476[the skipped tests]. +link:https://github.com/bazelbuild/bazel/issues/3476[the skipped tests,role=external,window=_blank]. == Dependencies @@ -465,7 +466,7 @@ * ~/.gerritcodereview/bazel-cache/cas Currently none of these caches have a maximum size limit. See -link:https://github.com/bazelbuild/bazel/issues/5139[this bazel issue] for +link:https://github.com/bazelbuild/bazel/issues/5139[this bazel issue,role=external,window=_blank] for details. Users should watch the cache sizes and clean them manually if necessary. @@ -476,7 +477,7 @@ "binaries". We don't attempt to resolve and download NPM dependencies at build time, but instead use pre-built bundles of the NPM binary along with all its dependencies. Some packages on -link:https://docs.npmjs.com/misc/registry[registry.npmjs.org] come with their +link:https://docs.npmjs.com/misc/registry[registry.npmjs.org,role=external,window=_blank] come with their dependencies bundled, but this is the exception rather than the rule. More commonly, to add a new binary to this list, you will need to bundle the binary yourself. @@ -504,7 +505,7 @@ storage bucket if the licenses allow us to do so. As long as all of the listed license are allowed by link:https://opensource.google.com/docs/thirdparty/licenses/[Google's -standards]. Any `by_exception_only`, commercial, prohibited, or unlisted +standards,role=external,window=_blank]. Any `by_exception_only`, commercial, prohibited, or unlisted licenses are not allowed; otherwise, it is ok to distribute the source. If in doubt, contact a maintainer who is a Googler. @@ -529,7 +530,7 @@ Any project maintainer can upload this file to the link:https://console.cloud.google.com/storage/browser/gerrit-maven/npm-packages[storage -bucket]. +bucket,role=external,window=_blank]. Finally, add the new binary to the build process: ----
diff --git a/Documentation/dev-cla.txt b/Documentation/dev-cla.txt index 267351f..a5a9932 100644 --- a/Documentation/dev-cla.txt +++ b/Documentation/dev-cla.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - Contributor License Agreement In order to link::dev-community.html#how-to-contribute[contribute] to @@ -6,17 +7,17 @@ following: . Click 'Sign In' at the top right corner of - https://gerrit-review.googlesource.com/ + https://gerrit-review.googlesource.com/[role=external,window=_blank] . Sign In with your Google account . After signing in, go to the - link:https://gerrit-review.googlesource.com/#/settings/agreements[Agreements] + link:https://gerrit-review.googlesource.com/#/settings/agreements[Agreements,role=external,window=_blank] tab on the settings page . Click on 'New Contributor Agreement' and follow the instructions For reference, the actual agreements are linked below: -* link:https://cla.developers.google.com/about/google-individual[Individual Agreement] -* link:https://cla.developers.google.com/about/google-corporate[Corporate Agreement] +* link:https://cla.developers.google.com/about/google-individual[Individual Agreement,role=external,window=_blank] +* link:https://cla.developers.google.com/about/google-corporate[Corporate Agreement,role=external,window=_blank] GERRIT ------
diff --git a/Documentation/dev-community.txt b/Documentation/dev-community.txt index a497064..5892253 100644 --- a/Documentation/dev-community.txt +++ b/Documentation/dev-community.txt
@@ -1,19 +1,20 @@ +:linkattrs: = Gerrit Community Gerrit is developed as a -link:https://gerrit-review.googlesource.com/[self-hosting open source project] +link:https://gerrit-review.googlesource.com/[self-hosting open source project,role=external,window=_blank] and very much welcomes contributions from anyone with a link:dev-cla.html[contributor's agreement] on file with the project. [[project-information]] == Project Information -* link:https://www.gerritcodereview.com/[Project Homepage] -* link:https://www.gerritcodereview.com/codeofconduct.html[Code of Conduct] -* link:https://www.gerritcodereview.com/releases-readme.html[Release Versions] -* link:https://gerrit.googlesource.com/gerrit[Source] -* link:https://bugs.chromium.org/p/gerrit/issues/list[Issue Tracking] -* link:https://gerrit-review.googlesource.com/q/status:open+project:gerrit[Change Review] +* link:https://www.gerritcodereview.com/[Project Homepage,role=external,window=_blank] +* link:https://www.gerritcodereview.com/codeofconduct.html[Code of Conduct,role=external,window=_blank] +* link:https://www.gerritcodereview.com/releases-readme.html[Release Versions,role=external,window=_blank] +* link:https://gerrit.googlesource.com/gerrit[Source,role=external,window=_blank] +* link:https://bugs.chromium.org/p/gerrit/issues/list[Issue Tracking,role=external,window=_blank] +* link:https://gerrit-review.googlesource.com/q/status:open+project:gerrit[Change Review,role=external,window=_blank] * link:dev-design.html[System Design] * Processes ** link:dev-processes.html#project-governance[Project Governance / Engineering Steering Committee]
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt index 0bac643..70fbd60 100644 --- a/Documentation/dev-contributing.txt +++ b/Documentation/dev-contributing.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - Contributing [[cla]] @@ -33,7 +34,7 @@ design-driven contribution process instead. If you are in doubt which process is right for you, consult the -link:https://groups.google.com/d/forum/repo-discuss[repo-discuss] +link:https://groups.google.com/d/forum/repo-discuss[repo-discuss,role=external,window=_blank] mailing list. These contribution processes apply to everyone who contributes code to @@ -83,11 +84,11 @@ be reviewed before they will get submitted to the code base. To start your contribution, please make a git commit and upload it for review to the link:https://gerrit-review.googlesource.com/[ -gerrit-review.googlesource.com] Gerrit server. To help speed up the +gerrit-review.googlesource.com,role=external,window=_blank] Gerrit server. To help speed up the review of your change, review these link:dev-crafting-changes.html[ guidelines] before submitting your change. You can view the pending Gerrit contributions and their statuses -link:https://gerrit-review.googlesource.com/#/q/status:open+project:gerrit[here]. +link:https://gerrit-review.googlesource.com/#/q/status:open+project:gerrit[here,role=external,window=_blank]. Depending on the size of that list it might take a while for your change to get reviewed. Naturally there are fewer
diff --git a/Documentation/dev-core-plugins.txt b/Documentation/dev-core-plugins.txt new file mode 100644 index 0000000..12726b7 --- /dev/null +++ b/Documentation/dev-core-plugins.txt
@@ -0,0 +1,95 @@ +:linkattrs: += Gerrit Code Review - Core Plugins + +[[definition]] +== What are core plugins? + +Core plugins are plugins that are packaged within the Gerrit war file. This +means during the link:pgm-init.html[Gerrit initialization] they can be easily +installed without downloading any additional files. + +To make working with core plugins easy, they are linked as +link:https://gerrit.googlesource.com/gerrit/+/refs/heads/master/.gitmodules[Git +submodules,role=external,window=_blank] in the `gerrit` repository. E.g. this means they can be easily +link:dev-readme.html#clone[cloned] together with Gerrit. + +All core plugins are developed and maintained by the +link:dev-roles.html#maintainers[Gerrit maintainers] and everyone can +link:dev-contributing.html[contribute] to them. + +Adding a new core plugin feature that is large or complex requires a +link:dev-design-doc.html[design doc] (also see +link:dev-contributing.html#design-driven-contribution-process[design-driven +contribution process]). The link:dev-processes.html#steering-committee[ +engineering steering committee (ESC)] is the authority that approves the design +docs. The ESC is also in charge of adding and removing core plugins. + +Non-Gerrit maintainers cannot have link:access-control.html#category_owner[ +Owner] permissions for core plugins. + +[[criteria]] +=== Criteria for Core Plugins + +To be considered as a core plugin, a plugin must fulfill the following +criteria: + +1. License: ++ +The plugin code is available under the +link:http://www.apache.org/licenses/LICENSE-2.0[Apache License Version 2.0,role=external,window=_blank]. + +2. Hosting: ++ +The plugin development is hosted on the +link:https://gerrit-review.googlesource.com[gerrit-review,role=external,window=_blank] Gerrit Server. + +3. Scope: ++ +The plugin functionality is Gerrit-related, has a clear scope and does not +conflict with other core plugins or existing and planned Gerrit core features. + +4. Relevance: ++ +The plugin functionality is relevant to a majority of the Gerrit community: ++ +-- +** An out of the box Gerrit installation would seem like it is missing + something if the plugin is not installed. +** It's expected that most sites would use the plugin. +** Multiple parties (different organizations/companies) already use the plugin + and agree that it should be offered as core plugin. +** If the same or similar functionality is provided by multiple plugins, + the plugin is the clear recommended solution by the community. +-- ++ +Whether a plugin is relevant to a majority of the Gerrit community must be +discussed on a case-by-case basis. In case of doubt, it's up to the +link:dev-processes.html#steering-committee[engineering steering committee] to +make a decision. + +5. Code Quality: ++ +The plugin code is mature and has a good test coverage. Maintaining the plugin +code creates only little overhead for the Gerrit maintainers. + +6. Documentation: ++ +The plugin functionality is fully documented. + +7. Ownership: ++ +Existing plugin owners which are not Gerrit maintainers must agree to give up +their ownership. If the current plugin owners disagree, forking the plugin is +possible, but this should happen only in exceptional cases. + +[[list]] +== Which core plugins exist? + +See link:config-plugins.html#core-plugins[here]. + +GERRIT +------ +Part of link:index.html[Gerrit Code Review] + +SEARCHBOX +---------
diff --git a/Documentation/dev-crafting-changes.txt b/Documentation/dev-crafting-changes.txt index bf4453ca..2364fcc 100644 --- a/Documentation/dev-crafting-changes.txt +++ b/Documentation/dev-crafting-changes.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - Crafting Changes Here are some hints as to what approvers may be looking for @@ -86,10 +87,10 @@ The HTTPS access requires proper username and password; this can be obtained by clicking the 'Obtain Password' link on the link:https://gerrit-review.googlesource.com/#/settings/http-password[HTTP -Password tab of the user settings page]. +Password tab of the user settings page,role=external,window=_blank]. Alternately, you may use the -link:https://pypi.org/project/git-review/[git-review] tool to submit changes +link:https://pypi.org/project/git-review/[git-review,role=external,window=_blank] tool to submit changes to Gerrit. If you do, it will set up the Change-Id hook and `gerrit` remote for you. You will still need to do the HTTP access step. @@ -109,12 +110,12 @@ Gerrit generally follows the link:https://google.github.io/styleguide/javaguide.html[Google Java Style -Guide]. +Guide,role=external,window=_blank]. To format Java source code, Gerrit uses the -link:https://github.com/google/google-java-format[`google-java-format`] +link:https://github.com/google/google-java-format[`google-java-format`,role=external,window=_blank] tool (version 1.7), and to format Bazel BUILD, WORKSPACE and .bzl files the -link:https://github.com/bazelbuild/buildtools/tree/master/buildifier[`buildifier`] +link:https://github.com/bazelbuild/buildtools/tree/master/buildifier[`buildifier`,role=external,window=_blank] tool (version 0.29.0). These tools automatically apply format according to the style guides; this streamlines code review by reducing the need for time-consuming, tedious, @@ -230,6 +231,13 @@ * Tests for new code will greatly help your change get approved. +[[javadoc]] +== Javadoc + + * Javadocs for new code (especially public classes and + public/protected methods) will greatly help your change get + approved. + [[change-size]] == Change Size/Number of Files Touched
diff --git a/Documentation/dev-design-docs.txt b/Documentation/dev-design-docs.txt index 5e3f7a9..ca5ff62 100644 --- a/Documentation/dev-design-docs.txt +++ b/Documentation/dev-design-docs.txt
@@ -72,7 +72,7 @@ To propose a new design, upload a change to the link:https://gerrit-review.googlesource.com/admin/repos/homepage[ -homepage] repository that adds a new folder under `pages/design-docs/` +homepage,role=external,window=_blank] repository that adds a new folder under `pages/design-docs/` which contains at least an `index.md` and a `uses-cases.md` file (see link:#structure[design doc structure] above). @@ -87,7 +87,7 @@ Only very few maintainers actively watch out for uploaded design docs. To raise awareness you may want to send a notification to the -link:https://groups.google.com/d/forum/repo-discuss[repo-discuss] +link:https://groups.google.com/d/forum/repo-discuss[repo-discuss,role=external,window=_blank] mailing list about your uploaded design doc. But the discussion should not take place on the mailing list, comments should be made by reviewing the change in Gerrit. @@ -122,7 +122,7 @@ . Go to the link:https://gerrit-review.googlesource.com/settings/#Notifications[ - notification settings] + notification settings,role=external,window=_blank] . Add a project watch for the `homepage` repository with the following query: `dir:pages/design-docs`
diff --git a/Documentation/dev-design.txt b/Documentation/dev-design.txt index fd53cac..c94862e 100644 --- a/Documentation/dev-design.txt +++ b/Documentation/dev-design.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - System Design == Objective @@ -68,10 +69,10 @@ Since Gerrit 3.x link:note-db.html[NoteDb] replaced the SQL database and all metadata is now stored in Git. -* link:http://video.google.com/videoplay?docid=-8502904076440714866[Mondrian Code Review On The Web] -* link:https://github.com/rietveld-codereview/rietveld[Rietveld - Code Review for Subversion] -* link:http://eagain.net/gitweb/?p=gitosis.git;a=blob;f=README.rst;hb=HEAD[Gitosis README] -* link:http://source.android.com/[Android Open Source Project] +* link:http://video.google.com/videoplay?docid=-8502904076440714866[Mondrian Code Review On The Web,role=external,window=_blank] +* link:https://github.com/rietveld-codereview/rietveld[Rietveld - Code Review for Subversion,role=external,window=_blank] +* link:http://eagain.net/gitweb/?p=gitosis.git;a=blob;f=README.rst;hb=HEAD[Gitosis README,role=external,window=_blank] +* link:http://source.android.com/[Android Open Source Project,role=external,window=_blank] == Overview @@ -167,8 +168,8 @@ requires that the OpenID provider selected by a user must be online and operating in order to authenticate that user. -* link:http://www.kernel.org/pub/software/scm/git/docs/gitrepository-layout.html[Git Repository Format] -* link:http://openid.net/developers/specs/[OpenID Specifications] +* link:http://www.kernel.org/pub/software/scm/git/docs/gitrepository-layout.html[Git Repository Format,role=external,window=_blank] +* link:http://openid.net/developers/specs/[OpenID Specifications,role=external,window=_blank] *1 Although an effort is underway to eliminate the use of the database altogether, and to store all the metadata directly in
diff --git a/Documentation/dev-e2e-tests.txt b/Documentation/dev-e2e-tests.txt index 7329a43..5e39687 100644 --- a/Documentation/dev-e2e-tests.txt +++ b/Documentation/dev-e2e-tests.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - End to end load tests This document provides a description of a Gerrit load test scenario implemented using the link:http://gatling.io[`Gatling`] framework. @@ -8,9 +9,9 @@ == What is Gatling? Gatling is a load testing tool which provides out of the box support for the HTTP protocol. Documentation on how to write an -HTTP load test can be found link:https://gatling.io/docs/current/http/http_protocol/[`here`]. +HTTP load test can be found link:https://gatling.io/docs/current/http/http_protocol/[`here`,role=external,window=_blank]. -However, in the scenario we are proposing, we are leveraging the link:https://github.com/GerritForge/gatling-git[`Gatling Git extension`] +However, in the scenario we are proposing, we are leveraging the link:https://github.com/GerritForge/gatling-git[`Gatling Git extension`,role=external,window=_blank] to run tests at Git protocol level. Gatling is written in Scala, but the abstraction provided by the Gatling DSL makes the scenarios implementation easy even without any Scala knowledge. @@ -32,7 +33,7 @@ ==== Setup If you are running SSH commands the private keys of the users used for testing need to go in `/tmp/ssh-keys`. -The keys need to be generated this way (JSch won't validate them [otherwise](https://stackoverflow.com/questions/53134212/invalid-privatekey-when-using-jsch): +The keys need to be generated this way (JSch won't validate them [otherwise,role=external,window=_blank](https://stackoverflow.com/questions/53134212/invalid-privatekey-when-using-jsch): ---- ssh-keygen -m PEM -t rsa -C "test@mail.com" -f /tmp/ssh-keys/id_rsa
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt index dfe6aa5..f7d0c73 100644 --- a/Documentation/dev-eclipse.txt +++ b/Documentation/dev-eclipse.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - Eclipse Setup This document is about configuring Gerrit Code Review into an @@ -8,7 +9,7 @@ [[setup]] == Project Setup -In your Eclipse installation's link:https://wiki.eclipse.org/Eclipse.ini[`eclipse.ini`] file, +In your Eclipse installation's link:https://wiki.eclipse.org/Eclipse.ini[`eclipse.ini`,role=external,window=_blank] file, add the following line in the `vmargs` section: ---- @@ -60,13 +61,13 @@ == Code Formatter Settings To format source code, Gerrit uses the -link:https://github.com/google/google-java-format[`google-java-format`] +link:https://github.com/google/google-java-format[`google-java-format`,role=external,window=_blank] tool (version 1.7), which automatically formats code to follow the style guide. See link:dev-crafting-changes.html#style[Code Style] for the instruction how to set up command line tool that uses this formatter. The Eclipse plugin is provided that allows to format with the same formatter from within the Eclipse IDE. See -link:https://github.com/google/google-java-format#eclipse[Eclipse plugin] +link:https://github.com/google/google-java-format#eclipse[Eclipse plugin,role=external,window=_blank] for details how to install it. It's important to use the same plugin version as the `google-java-format` script.
diff --git a/Documentation/dev-inspector.txt b/Documentation/dev-inspector.txt index 39736d7..da6e3aa 100644 --- a/Documentation/dev-inspector.txt +++ b/Documentation/dev-inspector.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Inspector == NAME @@ -33,7 +34,7 @@ Gerrit Inspector requires Jython library ('jython.jar') to be installed in the '$site_path/lib' directory. Jython, a Python interpreter for -the Java Virtual Machine, can be obtained from the http://www.jython.org/ +the Java Virtual Machine, can be obtained from the http://www.jython.org/[role=external,window=_blank] website. Only 'jython.jar' file is needed, installation of Jython libraries is optional. Gerrit Inspector has been tested with Jython 2.5.2 but might work an earlier version. @@ -87,7 +88,7 @@ For more information on using Jython, especially with regards to its limitations in interfacing to the Java Virtual Machine, please refer to the -http://www.jython.org/[Jython documentation]. +http://www.jython.org/[Jython documentation,role=external,window=_blank]. After successful initialization it is possible to examine components of Java packages, classes and live instances.
diff --git a/Documentation/dev-intellij.txt b/Documentation/dev-intellij.txt index 81790db..b67d546 100644 --- a/Documentation/dev-intellij.txt +++ b/Documentation/dev-intellij.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - IntelliJ IDEA Setup == Prerequisites @@ -14,7 +15,7 @@ === IntelliJ version and Bazel plugin Before downloading IntelliJ, look at the -link:https://plugins.jetbrains.com/plugin/8609-bazel/versions[JetBrains plugin repository page of the Bazel plugin] +link:https://plugins.jetbrains.com/plugin/8609-bazel/versions[JetBrains plugin repository page of the Bazel plugin,role=external,window=_blank] to see what version of the IntelliJ IDEA it is actually compatible with. Also note that the version of the Bazel plugin used in turn may or may not be @@ -30,7 +31,7 @@ === Installation of IntelliJ IDEA Please refer to the -link:https://www.jetbrains.com/help/idea/installation-guide.html[installation guide provided by Jetbrains] +link:https://www.jetbrains.com/help/idea/installation-guide.html[installation guide provided by Jetbrains,role=external,window=_blank] to install it on your platform. Make sure to install a version compatible with the Bazel plugin as mentioned above. @@ -47,7 +48,7 @@ . Search for the plugin `Bazel` (by Google). + TIP: In case the Bazel plugin is not listed, or if it shows an outdated version, -verify the compatibility between the Bazel plugin and IntelliJ IDEA on link:https://plugins.jetbrains.com/plugin/8609-bazel/versions[the JetBrains plugin page]. +verify the compatibility between the Bazel plugin and IntelliJ IDEA on link:https://plugins.jetbrains.com/plugin/8609-bazel/versions[the JetBrains plugin page,role=external,window=_blank]. . Install it. . Restart IntelliJ IDEA. @@ -117,7 +118,7 @@ . Download https://raw.githubusercontent.com/google/styleguide/gh-pages/intellij-java-google-style.xml[ -intellij-java-google-style.xml]. +intellij-java-google-style.xml,role=external,window=_blank]. . Go to *File -> Settings -> Editor -> Code Style*. . Click on the wrench icon with the tooltip _Show Scheme Actions_. . Click on *Import Scheme*. @@ -196,7 +197,7 @@ use the instructions of <<dev-readme#run_daemon,Running the Daemon>> in combination with <<remote-debug,Debugging a remote Gerrit server>>. -(link:https://bugs.chromium.org/p/gerrit/issues/detail?id=11360[Issue 11360]) +(link:https://bugs.chromium.org/p/gerrit/issues/detail?id=11360[Issue 11360,role=external,window=_blank]) ==== Copy `$(gerrit_source_code)/tools/intellij/gerrit_daemon.xml` to
diff --git a/Documentation/dev-plugins-lifecycle.txt b/Documentation/dev-plugins-lifecycle.txt index b552472..d5bd791 100644 --- a/Documentation/dev-plugins-lifecycle.txt +++ b/Documentation/dev-plugins-lifecycle.txt
@@ -1,7 +1,8 @@ +:linkattrs: = Plugin Lifecycle Most of the plugins are hosted on the same instance as the -link:https://gerrit-review.googlesource.com[Gerrit project itself] to make them +link:https://gerrit-review.googlesource.com[Gerrit project itself,role=external,window=_blank] to make them more discoverable and have more chances to be reviewed by the whole community. [[hosting_lifecycle]] @@ -12,7 +13,7 @@ - Ideation and Discussion: + The idea of creating a new plugin is posted and discussed on the -link:https://groups.google.com/d/forum/repo-discuss[repo-discuss] mailing list. +link:https://groups.google.com/d/forum/repo-discuss[repo-discuss,role=external,window=_blank] mailing list. + Also see section link#ideation_discussion[Ideation and discussion] below. @@ -27,14 +28,14 @@ + The author proposes to release the plugin under the link:https://www.apache.org/licenses/LICENSE-2.0.html[Apache 2.0 OpenSource -license] and requests the plugin to be hosted on -link:https://gerrit-review.googlesource.com[the Gerrit project site]. The +license,role=external,window=_blank] and requests the plugin to be hosted on +link:https://gerrit-review.googlesource.com[the Gerrit project site,role=external,window=_blank]. The proposal must be accepted by at least one Gerrit maintainer. In case of disagreement between maintainers, the issue can be escalated to the link:dev-processes.html#steering-committee[Engineering Steering Committee]. If the plugin is accepted, the Gerrit maintainer creates the project under the plugins path on link:https://gerrit-review.googlesource.com[the Gerrit project -site]. +site,role=external,window=_blank]. + Also see section link#plugin_proposal[Plugin Proposal] below. @@ -42,7 +43,7 @@ + To make the consumption of the plugin easy and to notice plugin breakages early the plugin author should setup build jobs on -link:https://gerrit-ci.gerritforge.com[the GerritForge CI] that build the +link:https://gerrit-ci.gerritforge.com[the GerritForge CI,role=external,window=_blank] that build the plugin for each Gerrit version that it supports. + Also see section link#build[Build] below. @@ -58,7 +59,7 @@ - Release: + The author releases the plugin by creating a Git tag and announcing the plugin -on the link:https://groups.google.com/d/forum/repo-discuss[repo-discuss] +on the link:https://groups.google.com/d/forum/repo-discuss[repo-discuss,role=external,window=_blank] mailing list. + Also see section link#plugin_release[Plugin release] below. @@ -84,7 +85,7 @@ contribution of ideas and suggestions by the whole community. The ideator of the plugin starts with an RFC (Request For Comments) post on the -link:https://groups.google.com/d/forum/repo-discuss[repo-discuss] mailing list +link:https://groups.google.com/d/forum/repo-discuss[repo-discuss,role=external,window=_blank] mailing list with a description of the main reasons for starting a new plugin. Example of a post: @@ -138,9 +139,9 @@ The author decides that the plugin prototype makes sense as a general purpose plugin and decides to release the code with the same -link:https://www.apache.org/licenses/LICENSE-2.0.html[Apache 2.0 license] +link:https://www.apache.org/licenses/LICENSE-2.0.html[Apache 2.0 license,role=external,window=_blank] as the Gerrit Code Review project and have it hosted on -link:https://gerrit-review.googlesource.com[the Gerrit project site]. +link:https://gerrit-review.googlesource.com[the Gerrit project site,role=external,window=_blank]. The plugin author formalizes the proposal with a follow-up of the initial RFC post and asks for public opinion on it. @@ -167,7 +168,7 @@ - The plugin's project request is widely appreciated and formally accepted by at least one Gerrit maintainer who creates the repository as child project of 'Public-Projects' on link:https://gerrit-review.googlesource.com[the Gerrit - project site], creates an associated plugin owners group with "Owner" + project site,role=external,window=_blank], creates an associated plugin owners group with "Owner" permissions for the plugin and adds the plugin's author as member of it. - The plugin's project is widely appreciated; however, another existing plugin already partially covers the same use-case and thus it would make more sense @@ -177,15 +178,15 @@ - The plugin's project is found useful; however, it is too specific to the author's use-case and would not make sense outside of it. The plugin remains in a public repository, widely accessible and OpenSource, but not hosted on - link:https://gerrit-review.googlesource.com[the Gerrit project site]. + link:https://gerrit-review.googlesource.com[the Gerrit project site,role=external,window=_blank]. [[build]] == Build The plugin's maintainer creates a job on the -link:https://gerrit-ci.gerritforge.com[GerritForge CI] by creating a new YAML +link:https://gerrit-ci.gerritforge.com[GerritForge CI,role=external,window=_blank] by creating a new YAML definition in the link:https://gerrit.googlesource.com/gerrit-ci-scripts[Gerrit -CI Scripts] repository. +CI Scripts,role=external,window=_blank] repository. Example of a YAML CI job for plugins: @@ -203,7 +204,7 @@ The plugin follows the same lifecycle as Gerrit Code Review and needs to be kept up-to-date with the current active branches, according to the -link:https://www.gerritcodereview.com/#support[current support policy]. +link:https://www.gerritcodereview.com/#support[current support policy,role=external,window=_blank]. During the development, the plugin's maintainer can reward contributors requesting to be more involved and making them maintainers of his plugin, adding them to the list of the project owners.
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt index 11e0666..9714e18 100644 --- a/Documentation/dev-plugins.txt +++ b/Documentation/dev-plugins.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - Plugin Development The Gerrit server functionality can be extended by installing plugins. @@ -36,7 +37,7 @@ To get started with the development of a plugin, take a look at the samples in the -link:https://gerrit.googlesource.com/plugins/examples[examples plugin project]. +link:https://gerrit.googlesource.com/plugins/examples[examples plugin project,role=external,window=_blank]. This is a project that demonstrates the various features of the plugin API. It can be taken as an example to develop an own plugin. @@ -367,7 +368,7 @@ Plugins are loaded from a single JAR file. If a plugin needs additional libraries, it must include those dependencies within its own JAR. Plugins built using Maven may be able to use the -link:http://maven.apache.org/plugins/maven-shade-plugin/[shade plugin] +link:http://maven.apache.org/plugins/maven-shade-plugin/[shade plugin,role=external,window=_blank] to package additional dependencies. Relocating (or renaming) classes should not be necessary due to the ClassLoader isolation. @@ -2189,7 +2190,7 @@ Gerrit provides an extension point that enables development of link:https://github.com/github/git-lfs/blob/master/docs/api/v1/http-v1-batch.md[ -LFS (Large File Storage)] storage plugins. Gerrit core exposes the default LFS +LFS (Large File Storage),role=external,window=_blank] storage plugins. Gerrit core exposes the default LFS protocol endpoint `<project-name>/info/lfs/objects/batch` and forwards the requests to the configured link:config-gerrit.html#lfs[lfs.plugin] plugin which implements the LFS protocol. By exposing the default LFS endpoint, the git-lfs client can be @@ -2247,16 +2248,16 @@ To send Gerrit's metrics data to an external reporting backend, a plugin can get a `MetricRegistry` injected and register an instance of a class that implements the `Reporter` interface from link:http://metrics.dropwizard.io/[ -DropWizard Metrics]. +DropWizard Metrics,role=external,window=_blank]. Metric reporting plugin implementations are provided for -link:https://gerrit.googlesource.com/plugins/metrics-reporter-jmx/[JMX], -link:https://gerrit.googlesource.com/plugins/metrics-reporter-elasticsearch/[Elastic Search], -and link:https://gerrit.googlesource.com/plugins/metrics-reporter-graphite/[Graphite]. +link:https://gerrit.googlesource.com/plugins/metrics-reporter-jmx/[JMX,role=external,window=_blank], +link:https://gerrit.googlesource.com/plugins/metrics-reporter-elasticsearch/[Elastic Search,role=external,window=_blank], +and link:https://gerrit.googlesource.com/plugins/metrics-reporter-graphite/[Graphite,role=external,window=_blank]. There is also a working example of reporting metrics to the console in the link:https://gerrit.googlesource.com/plugins/cookbook-plugin/+/master/src/main/java/com/googlesource/gerrit/plugins/cookbook/ConsoleMetricReporter.java[ -cookbook plugin]. +cookbook plugin,role=external,window=_blank]. === Providing own metrics @@ -2293,7 +2294,7 @@ See the replication metrics in the link:https://gerrit.googlesource.com/plugins/replication/+/master/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationMetrics.java[ -replication plugin] for an example of usage. +replication plugin,role=external,window=_blank] for an example of usage. [[account-patch-review-store]] == AccountPatchReviewStore @@ -2339,7 +2340,7 @@ attribute. Documentation may be written in the Markdown flavor -link:https://github.com/vsch/flexmark-java[flexmark-java] +link:https://github.com/vsch/flexmark-java[flexmark-java,role=external,window=_blank] if the file name ends with `.md`. Gerrit will automatically convert Markdown to HTML if accessed with extension `.html`. @@ -2753,6 +2754,9 @@ this interface can be used to retry the request instead of failing it immediately. +It also allows implementors to group exceptions that have the same +cause into one metric bucket. + [[mail-soy-template-provider]] == MailSoyTemplateProvider
diff --git a/Documentation/dev-processes.txt b/Documentation/dev-processes.txt index f4e77a8..cc6481c 100644 --- a/Documentation/dev-processes.txt +++ b/Documentation/dev-processes.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - Development Processes [[project-governance]] @@ -7,7 +8,8 @@ The Gerrit project has an engineering steering committee (ESC) that is in charge of: -* Gerrit core (the `gerrit` project) and the core plugins +* Gerrit core (the `gerrit` project) and the link:dev-core-plugins.html[core + plugins] * defining the project vision and the project scope * maintaining a roadmap, a release plan and a prioritized backlog * ensuring timely design reviews @@ -29,7 +31,7 @@ period of 1 year (see link:#steering-committee-election[below]) Refer to the project homepage for the link:https://www.gerritcodereview.com/members.html#engineering-steering-committee[ -list of current committee members]. +list of current committee members,role=external,window=_blank]. The steering committee should act in the interest of the Gerrit project and the whole Gerrit community. @@ -83,7 +85,7 @@ [[versioning]] == Semantic versioning -Gerrit follows a light link:https://semver.org/[semantic versioning scheme] MAJOR.MINOR[.PATCH[.HOTFIX]] +Gerrit follows a light link:https://semver.org/[semantic versioning scheme,role=external,window=_blank] MAJOR.MINOR[.PATCH[.HOTFIX]] format: * MAJOR is incremented when there are substantial incompatible changes and/or @@ -181,7 +183,7 @@ To report a security vulnerability file a link:https://bugs.chromium.org/p/gerrit/issues/entry?template=Security+Issue[ -security issue] in the Gerrit issue tracker. The visibility of issues that are +security issue,role=external,window=_blank] in the Gerrit issue tracker. The visibility of issues that are created with the `Security Issue` template is automatically restricted to Gerrit maintainers and a few long-term contributors. This means as a reporter you may not be able to see the issue once it is created. Security issues are @@ -221,7 +223,7 @@ address the security vulnerability immediately (either by upgrading to a fixed release or applying the mitigation). The information about the security vulnerability is disclosed via the -link:https://groups.google.com/d/forum/repo-discuss[repo-discuss] mailing list. +link:https://groups.google.com/d/forum/repo-discuss[repo-discuss,role=external,window=_blank] mailing list. [[handle-security-issue]] === Handling of the Security Vulnerability @@ -252,7 +254,7 @@ + Instead security fixes should be implemented and reviewed in the non-public link:https://gerrit-review.googlesource.com/admin/repos/gerrit-security-fixes[ -gerrit-security-fixes] repository which is only accessible by Gerrit +gerrit-security-fixes,role=external,window=_blank] repository which is only accessible by Gerrit maintainers and Gerrit community members that work on security fixes. + The change that fixes the security vulnerability should contain an integration @@ -283,7 +285,7 @@ Once all releases are ready and tested and the announcement is prepared, the releases should be all published at the same time. Immediately after that, the announcement should be sent out to the -link:https://groups.google.com/d/forum/repo-discuss[repo-discuss] mailing list. +link:https://groups.google.com/d/forum/repo-discuss[repo-discuss,role=external,window=_blank] mailing list. + This ends the embargo and any issue that discusses the security vulnerability should be made public. @@ -292,7 +294,12 @@ + The ESC should discuss if there are any learnings from the security vulnerability and define action items to follow up in the -link:https://bugs.chromium.org/p/gerrit[issue tracker]. +link:https://bugs.chromium.org/p/gerrit[issue tracker,role=external,window=_blank]. + +[[core-plugins]] +== Core Plugins + +See link:dev-core-plugins.html[here]. [[upgrading-libraries]] == Upgrading Libraries
diff --git a/Documentation/dev-readme.txt b/Documentation/dev-readme.txt index 02b1891..5f72f37 100644 --- a/Documentation/dev-readme.txt +++ b/Documentation/dev-readme.txt
@@ -1,10 +1,12 @@ +:linkattrs: = Gerrit Code Review: Developer Setup -To build a developer instance, you'll need link:https://bazel.build/[Bazel] to +To build a developer instance, you'll need link:https://bazel.build/[Bazel,role=external,window=_blank] to compile the code. == Git Setup +[[clone]] === Getting the Source Create a new client workspace: @@ -30,7 +32,7 @@ CAUTION: If you store Eclipse or IntelliJ project files in the Gerrit source directories, do *_not_* run `git clean -fdx`. Doing so may remove untracked files and damage your project. For more information, see -link:https://git-scm.com/docs/git-clean[git-clean]. +link:https://git-scm.com/docs/git-clean[git-clean,role=external,window=_blank]. Run the following:
diff --git a/Documentation/dev-release-deploy-config.txt b/Documentation/dev-release-deploy-config.txt index 98a3df5..a4ccccf 100644 --- a/Documentation/dev-release-deploy-config.txt +++ b/Documentation/dev-release-deploy-config.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Deploy Gerrit Artifacts [[deploy-configuration-setting-maven-central]] @@ -11,7 +12,7 @@ be done: * Create an account on -link:https://issues.sonatype.org/secure/Signup!default.jspa[Sonatype's Jira]. +link:https://issues.sonatype.org/secure/Signup!default.jspa[Sonatype's Jira,role=external,window=_blank]. + Sonatype is the company that runs Maven Central and you need a Sonatype account to be able to upload artifacts to Maven Central. @@ -30,7 +31,7 @@ repository on Maven Central: + Ask for this permission by adding a comment on the -link:https://issues.sonatype.org/browse/OSSRH-7392[OSSRH-7392] Jira +link:https://issues.sonatype.org/browse/OSSRH-7392[OSSRH-7392,role=external,window=_blank] Jira ticket at Sonatype. + The request needs to be approved by someone who already has this @@ -43,7 +44,7 @@ + Generate and publish a PGP key as described in link:http://central.sonatype.org/pages/working-with-pgp-signatures.html[ -Working with PGP Signatures]. In addition to the keyserver mentioned +Working with PGP Signatures,role=external,window=_blank]. In addition to the keyserver mentioned there it is recommended to also publish the key to the link:https://keyserver.ubuntu.com/[Ubuntu key server]. + @@ -51,7 +52,7 @@ while until it is visible to the Sonatype server. + Add an entry for the public key in the -link:https://gerrit.googlesource.com/homepage/+/md-pages/releases/public-keys.md[key list] +link:https://gerrit.googlesource.com/homepage/+/md-pages/releases/public-keys.md[key list,role=external,window=_blank] on the homepage. + The PGP passphrase can be put in `~/.m2/settings.xml`: @@ -80,7 +81,7 @@ Gerrit Subproject Artifacts are stored on link:https://developers.google.com/storage/[Google Cloud Storage]. -Via the link:https://console.developers.google.com/project/164060093628[Developers Console] the +Via the link:https://console.developers.google.com/project/164060093628[Developers Console,role=external,window=_blank] the Gerrit maintainers have access to the `Gerrit Code Review` project. This projects host several buckets for storing Gerrit artifacts: @@ -96,7 +97,7 @@ To upload artifacts to a bucket the user must authenticate with a username and password. The username and password need to be retrieved from the link:https://console.cloud.google.com/storage/settings?project=api-project-164060093628[ -Storage Setting in the Google Cloud Platform Console]: +Storage Setting in the Google Cloud Platform Console,role=external,window=_blank]: Select the `Interoperability` tab, and if no keys are listed under `Interoperable storage access keys`, select 'Create a new key'.
diff --git a/Documentation/dev-release-jgit.txt b/Documentation/dev-release-jgit.txt index 1a8b501..7fbbb95 100644 --- a/Documentation/dev-release-jgit.txt +++ b/Documentation/dev-release-jgit.txt
@@ -1,8 +1,9 @@ +:linkattrs: = Making a Snapshot Release of JGit This step is only necessary if we need to create an unofficial JGit snapshot release and publish it to the -link:https://developers.google.com/storage/[Google Cloud Storage]. +link:https://developers.google.com/storage/[Google Cloud Storage,role=external,window=_blank]. [[prepare-environment]] == Prepare the Maven Environment
diff --git a/Documentation/dev-release-subproject.txt b/Documentation/dev-release-subproject.txt index c9369b9..fad1681 100644 --- a/Documentation/dev-release-subproject.txt +++ b/Documentation/dev-release-subproject.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Making a Release of a Gerrit Subproject [[make-snapshot]]
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt index 9e1744c..eaf9905 100644 --- a/Documentation/dev-release.txt +++ b/Documentation/dev-release.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Making a Gerrit Release [NOTE] @@ -158,14 +159,14 @@ ** SNAPSHOT versions are directly uploaded into the Sonatype snapshots repository and no further action is needed: + -https://oss.sonatype.org/content/repositories/snapshots/com/google/gerrit/ +https://oss.sonatype.org/content/repositories/snapshots/com/google/gerrit/[role=external,window=_blank] ** Release versions are uploaded into a staging repository in the link:https://oss.sonatype.org/[Sonatype Nexus Server]. * Verify the staging repository -** Go to the link:https://oss.sonatype.org/[Sonatype Nexus Server] and +** Go to the link:https://oss.sonatype.org/[Sonatype Nexus Server,role=external,window=_blank] and sign in with your Sonatype credentials. ** Click on 'Build Promotion' in the left navigation bar under @@ -188,7 +189,7 @@ ** Test closed staging repository + Once a repository is closed you can find the URL to it in the `Summary` -section, e.g. https://oss.sonatype.org/content/repositories/comgooglegerrit-1029 +section, e.g. https://oss.sonatype.org/content/repositories/comgooglegerrit-1029[role=external,window=_blank] + Use this URL for further testing of the artifacts in this repository, e.g. to try building a plugin against the plugin API in this repository @@ -207,7 +208,7 @@ + How to release a staging repository is described in the link:https://docs.sonatype.org/display/Repository/Sonatype+OSS+Maven+Repository+Usage+Guide#SonatypeOSSMavenRepositoryUsageGuide-8.a.2.ReleasingaStagingRepository[ -Sonatype OSS Maven Repository Usage Guide]. +Sonatype OSS Maven Repository Usage Guide,role=external,window=_blank]. + [WARNING] Releasing artifacts to Maven Central cannot be undone! @@ -217,17 +218,17 @@ click on `Release`. ** The released artifacts are available in -https://oss.sonatype.org/content/repositories/releases/com/google/gerrit/ +https://oss.sonatype.org/content/repositories/releases/com/google/gerrit/[role=external,window=_blank] ** It may take up to 2 hours until the artifacts appear on Maven Central: + -http://central.maven.org/maven2/com/google/gerrit/ +http://central.maven.org/maven2/com/google/gerrit/[role=external,window=_blank] * [optional]: View download statistics ** Sign in to the -link:https://oss.sonatype.org/[Sonatype Nexus Server]. +link:https://oss.sonatype.org/[Sonatype Nexus Server,role=external,window=_blank]. ** Click on 'Views/Repositories' in the left navigation bar under 'Central Statistics'. @@ -239,7 +240,7 @@ ==== Publish the Gerrit WAR to the Google Cloud Storage * go to the link:https://console.cloud.google.com/storage/browser/gerrit-releases/?project=api-project-164060093628[ -gerrit-releases bucket in the Google cloud storage console] +gerrit-releases bucket in the Google cloud storage console,role=external,window=_blank] * make sure you are signed in with your Gmail account * manually upload the Gerrit WAR file by using the `Upload` button @@ -248,7 +249,7 @@ * Create the stable branch `stable-$version` in the `gerrit` project via the link:https://gerrit-review.googlesource.com/admin/repos/gerrit,branches[ -Gerrit Web UI] or by push. +Gerrit Web UI,role=external,window=_blank] or by push. * Push the commits done on `stable-$version` to `refs/for/stable-$version` and get them merged. @@ -282,7 +283,7 @@ * Upload the files manually via web browser to the appropriate folder in the link:https://console.cloud.google.com/storage/browser/gerrit-documentation/?project=api-project-164060093628[ -gerrit-documentation] storage bucket. +gerrit-documentation,role=external,window=_blank] storage bucket. [[finalize-release-notes]] === Finalize the Release Notes @@ -298,7 +299,7 @@ ==== Update homepage links Upload a change on the link:https://gerrit-review.googlesource.com/admin/repos/homepage[ -homepage project] to change the version numbers to the new version. +homepage project,role=external,window=_blank] to change the version numbers to the new version. [[update-issues]] ==== Update the Issues @@ -364,7 +365,7 @@ Bazlets is used by gerrit plugins to simplify build process. To allow the new released version to be used by gerrit plugins, -link:https://gerrit.googlesource.com/bazlets/+/master/gerrit_api.bzl#8[gerrit_api.bzl] +link:https://gerrit.googlesource.com/bazlets/+/master/gerrit_api.bzl#8[gerrit_api.bzl,role=external,window=_blank] must reference the new version. Upload a change to bazlets repository with api version upgrade.
diff --git a/Documentation/dev-roles.txt b/Documentation/dev-roles.txt index f457667..c16037e 100644 --- a/Documentation/dev-roles.txt +++ b/Documentation/dev-roles.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - Supporting Roles As an open source project Gerrit has a large community of people @@ -14,19 +15,19 @@ There are many possibilities to support the project, e.g.: * get involved in discussions on the - link:https://groups.google.com/d/forum/repo-discuss[repo-discuss] + link:https://groups.google.com/d/forum/repo-discuss[repo-discuss,role=external,window=_blank] mailing list (post your questions, provide feedback, share your experiences, help other users) * attend community events like user summits (see link:https://calendar.google.com/calendar?cid=Z29vZ2xlLmNvbV91YmIxcGxhNmlqNzg1b3FianI2MWg0dmRpc0Bncm91cC5jYWxlbmRhci5nb29nbGUuY29t[ - community calendar]) -* report link:https://bugs.chromium.org/p/gerrit/issues/list[issues] + community calendar,role=external,window=_blank]) +* report link:https://bugs.chromium.org/p/gerrit/issues/list[issues,role=external,window=_blank] and help to clarify existing issues * provide feedback on link:https://www.gerritcodereview.com/releases-readme.html[new - releases and release candidates] + releases and release candidates,role=external,window=_blank] * review - link:https://gerrit-review.googlesource.com/q/status:open[changes] + link:https://gerrit-review.googlesource.com/q/status:open[changes,role=external,window=_blank] and help to verify that they work as advertised, comment if you like or dislike a feature * serve as contact person for a proprietary Gerrit installation and @@ -35,7 +36,7 @@ Supporters can: * post on the - link:https://groups.google.com/d/forum/repo-discuss[repo-discuss] + link:https://groups.google.com/d/forum/repo-discuss[repo-discuss,role=external,window=_blank] mailing list (Please note that the `repo-discuss` mailing list is managed to prevent spam posts. This means posts from new participants must be approved manually before they appear on the mailing list. @@ -43,7 +44,7 @@ participate in mailing list discussions frequently are approved automatically) * comment on - link:https://gerrit-review.googlesource.com/q/status:open[changes] + link:https://gerrit-review.googlesource.com/q/status:open[changes,role=external,window=_blank] and vote from `-1` to `+1` on the `Code-Review` label (these votes are important to understand the interest in a change and to address concerns early, however link:#maintainer[maintainers] can @@ -53,7 +54,7 @@ permissions to vote on the `Verified` label are granted by request, see below) * file issues in the link:https://bugs.chromium.org/p/gerrit/issues/list[ - issue tracker] and comment on existing issues + issue tracker,role=external,window=_blank] and comment on existing issues * support the link:dev-processes.html#design-driven-contribution-process[ design-driven contribution process] by reviewing incoming @@ -62,7 +63,7 @@ Supporters who want to engage further can get additional privileges on request (ask for it on the -link:https://groups.google.com/d/forum/repo-discuss[repo-discuss] +link:https://groups.google.com/d/forum/repo-discuss[repo-discuss,role=external,window=_blank] mailing list): * become member of the `gerrit-verifiers` group, which allows to: @@ -71,10 +72,10 @@ ** edit topics on all open changes ** abandon changes * approve posts to the - link:https://groups.google.com/d/forum/repo-discuss[repo-discuss] + link:https://groups.google.com/d/forum/repo-discuss[repo-discuss,role=external,window=_blank] mailing list * administrate issues in the - link:https://bugs.chromium.org/p/gerrit/issues/list[issue tracker] + link:https://bugs.chromium.org/p/gerrit/issues/list[issue tracker,role=external,window=_blank] Supporters can become link:#contributor[contributors] by signing a contributor license agreement and contributing code to the Gerrit @@ -87,7 +88,7 @@ agreement] and who has link:dev-contributing.html[contributed] at least one change to any project on link:https://gerrit-review.googlesource.com/[ -gerrit-review.googlesource.com] is a contributor. +gerrit-review.googlesource.com,role=external,window=_blank] is a contributor. Contributions can be: @@ -123,10 +124,10 @@ Contributors may also be invited to join the Gerrit hackathons which happen regularly (e.g. twice a year). Hackathons are announced on the -link:https://groups.google.com/d/forum/repo-discuss[repo-discuss] +link:https://groups.google.com/d/forum/repo-discuss[repo-discuss,role=external,window=_blank] mailing list (also see link:https://calendar.google.com/calendar?cid=Z29vZ2xlLmNvbV91YmIxcGxhNmlqNzg1b3FianI2MWg0dmRpc0Bncm91cC5jYWxlbmRhci5nb29nbGUuY29t[ -community calendar]). +community calendar,role=external,window=_blank]). Outstanding contributors that are actively engaged in the community, in activities outlined above, may be nominated as link:#maintainer[ @@ -138,7 +139,7 @@ Maintainers are the gatekeepers of the project and are in charge of approving and submitting changes. Refer to the project homepage for the link:https://www.gerritcodereview.com/members.html#maintainers[ -list of current maintainers]. +list of current maintainers,role=external,window=_blank]. Maintainers should only approve changes that: @@ -185,20 +186,20 @@ link:dev-processes.html#project-governance[Project Governance] * nominate new maintainers and vote on nominations (see below) * administrate the link:https://groups.google.com/d/forum/repo-discuss[ - mailing list], the - link:https://bugs.chromium.org/p/gerrit/issues/list[issue tracker] - and the link:https://www.gerritcodereview.com/[homepage] + mailing list,role=external,window=_blank], the + link:https://bugs.chromium.org/p/gerrit/issues/list[issue tracker,role=external,window=_blank] + and the link:https://www.gerritcodereview.com/[homepage,role=external,window=_blank] * gain permissions to do Gerrit releases and publish release artifacts * create new projects and groups on link:https://gerrit-review.googlesource.com/[ - gerrit-review.googlesource.com] + gerrit-review.googlesource.com,role=external,window=_blank] * administrate the Gerrit projects on link:https://gerrit-review.googlesource.com/[ - gerrit-review.googlesource.com] (e.g. edit ACLs, update project + gerrit-review.googlesource.com,role=external,window=_blank] (e.g. edit ACLs, update project configuration) * create events in the link:https://calendar.google.com/calendar?cid=Z29vZ2xlLmNvbV91YmIxcGxhNmlqNzg1b3FianI2MWg0dmRpc0Bncm91cC5jYWxlbmRhci5nb29nbGUuY29t[ - community calendar] + community calendar,role=external,window=_blank] * discuss with other maintainers on the private maintainers mailing list and Slack channel @@ -243,7 +244,7 @@ Members of the steering committee are expected to act in the interest of the Gerrit project and the whole Gerrit community. Refer to the project homepage for the link:https://www.gerritcodereview.com/members.html#engineering-steering-committee[ -list of current committee members]. +list of current committee members,role=external,window=_blank]. For those that are familiar with scrum, the steering committee member role is similar to the role of an agile product owner. @@ -254,7 +255,7 @@ requests in a timely manner. Community members may submit new items under the -link:https://bugs.chromium.org/p/gerrit/issues/list?q=component:ESC[ESC component] +link:https://bugs.chromium.org/p/gerrit/issues/list?q=component:ESC[ESC component,role=external,window=_blank] in the issue tracker, or add that component to existing items, to raise them to the attention of ESC members. @@ -301,7 +302,7 @@ Community managers should act as stakeholders for the Gerrit community and focus on the health of the community. Refer to the project homepage for the link:https://www.gerritcodereview.com/members.html#community-managers[ -list of current community managers]. +list of current community managers,role=external,window=_blank]. Tasks: @@ -315,7 +316,7 @@ * serve as contact person for community issues Community members may submit new items under the -link:https://bugs.chromium.org/p/gerrit/issues/list?q=component:Community[Community component] +link:https://bugs.chromium.org/p/gerrit/issues/list?q=component:Community[Community component,role=external,window=_blank] backlog, for community managers to refine. Only public topics should be issued through that backlog.
diff --git a/Documentation/dev-starter-projects.txt b/Documentation/dev-starter-projects.txt index ae40ea6..eef4806 100644 --- a/Documentation/dev-starter-projects.txt +++ b/Documentation/dev-starter-projects.txt
@@ -1,10 +1,11 @@ +:linkattrs: = Gerrit Code Review - Starter Projects We have created a -link:https://bugs.chromium.org/p/gerrit/issues/list?can=2&q=label%3AStarterProject[StarterProject] +link:https://bugs.chromium.org/p/gerrit/issues/list?can=2&q=label%3AStarterProject[StarterProject,role=external,window=_blank] category in the issue tracker and try to assign easy hack projects to it. If in doubt, do not hesitate to ask on the developer -link:https://groups.google.com/forum/#!forum/repo-discuss[mailing list]. +link:https://groups.google.com/forum/#!forum/repo-discuss[mailing list,role=external,window=_blank]. GERRIT ------
diff --git a/Documentation/error-change-closed.txt b/Documentation/error-change-closed.txt index a239ef1..ae90d6d 100644 --- a/Documentation/error-change-closed.txt +++ b/Documentation/error-change-closed.txt
@@ -1,3 +1,4 @@ +:linkattrs: = change ... closed With this error message Gerrit rejects to push a commit or submit a @@ -15,7 +16,7 @@ new change. To do this you have to remove the Change-Id from the commit message as explained link:error-push-fails-due-to-commit-message.html[here] and ideally generate a new Change-Id using the link:cmd-hook-commit-msg.html[commit hook] or EGit. Before pushing again it is also -recommended to do a link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[git rebase] to base your commit on the submitted +recommended to do a link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[git rebase,role=external,window=_blank] to base your commit on the submitted change. Pushing again should now create a new change in Gerrit. If the change for which you wanted to upload a new patch set was
diff --git a/Documentation/error-changeid-above-footer.txt b/Documentation/error-changeid-above-footer.txt index abc0186..65d6620 100644 --- a/Documentation/error-changeid-above-footer.txt +++ b/Documentation/error-changeid-above-footer.txt
@@ -1,3 +1,4 @@ +:linkattrs: = commit xxxxxxx: Change-Id must be in message footer With this error message, Gerrit rejects a push of a commit to a project @@ -8,7 +9,7 @@ of a commit message. For details, see link:user-changeid.html[Change-Id Lines]. You can see the commit messages for existing commits in the history -by doing a link:http://www.kernel.org/pub/software/scm/git/docs/git-log.html[git log]. +by doing a link:http://www.kernel.org/pub/software/scm/git/docs/git-log.html[git log,role=external,window=_blank]. == Change-Id is contained in the commit message but not in the last paragraph
diff --git a/Documentation/error-contains-banned-commit.txt b/Documentation/error-contains-banned-commit.txt index 13a0eaa..1ed4992 100644 --- a/Documentation/error-contains-banned-commit.txt +++ b/Documentation/error-contains-banned-commit.txt
@@ -1,3 +1,4 @@ +:linkattrs: = contains banned commit ... With this error message Gerrit rejects to push a commit that is @@ -11,7 +12,7 @@ error message "contains banned commit ...". If you have commits that you want to push that are based on a banned -commit you may want to link:http://www.kernel.org/pub/software/scm/git/docs/git-cherry-pick.html[cherry-pick] them onto a clean base and push +commit you may want to link:http://www.kernel.org/pub/software/scm/git/docs/git-cherry-pick.html[cherry-pick,role=external,window=_blank] them onto a clean base and push them again.
diff --git a/Documentation/error-has-duplicates.txt b/Documentation/error-has-duplicates.txt index a520f5d..9f9c8a8 100644 --- a/Documentation/error-has-duplicates.txt +++ b/Documentation/error-has-duplicates.txt
@@ -1,3 +1,4 @@ +:linkattrs: = ... has duplicates With this error message Gerrit rejects to push a commit if its commit @@ -10,7 +11,7 @@ Since this error should never occur in practice, you should inform your Gerrit administrator if you hit this problem and/or -link:https://bugs.chromium.org/p/gerrit/issues/list[open a Gerrit issue]. +link:https://bugs.chromium.org/p/gerrit/issues/list[open a Gerrit issue,role=external,window=_blank]. In any case to not be blocked with your work, you can simply create a new Change-Id for your commit and then push it as new change to
diff --git a/Documentation/error-invalid-author.txt b/Documentation/error-invalid-author.txt index 5808d4f..d842cb7 100644 --- a/Documentation/error-invalid-author.txt +++ b/Documentation/error-invalid-author.txt
@@ -1,3 +1,4 @@ +:linkattrs: = invalid author For every pushed commit Gerrit verifies that the e-mail address of @@ -121,7 +122,7 @@ ---- For further details about git rebase please check the -link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[Git documentation]. +link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[Git documentation,role=external,window=_blank]. == Missing privileges to push commits of other users
diff --git a/Documentation/error-invalid-changeid-line.txt b/Documentation/error-invalid-changeid-line.txt index 9d3d2fc..a0f4b7b 100644 --- a/Documentation/error-invalid-changeid-line.txt +++ b/Documentation/error-invalid-changeid-line.txt
@@ -1,10 +1,11 @@ +:linkattrs: = invalid Change-Id line format in commit message footer With this error message Gerrit rejects to push a commit if its commit message footer contains an invalid Change-Id line. You can see the commit messages for existing commits in the history -by doing a link:http://www.kernel.org/pub/software/scm/git/docs/git-log.html[git log]. +by doing a link:http://www.kernel.org/pub/software/scm/git/docs/git-log.html[git log,role=external,window=_blank]. If it was the intention to rework a change and to push a new patch set, find the change in the Gerrit Web UI, copy its Change-Id line and
diff --git a/Documentation/error-invalid-committer.txt b/Documentation/error-invalid-committer.txt index a669010..c06eba2 100644 --- a/Documentation/error-invalid-committer.txt +++ b/Documentation/error-invalid-committer.txt
@@ -1,3 +1,4 @@ +:linkattrs: = invalid committer For every pushed commit Gerrit verifies that the e-mail address of @@ -86,7 +87,7 @@ commits and then confirming all the commit messages). Just picking all the changes will not work as in this case the committer is not rewritten. For further details about git rebase please check the -link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[Git documentation]. +link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[Git documentation,role=external,window=_blank]. == Missing privileges to push commits that were committed by other users
diff --git a/Documentation/error-missing-changeid.txt b/Documentation/error-missing-changeid.txt index 27bfea5..3494c3f 100644 --- a/Documentation/error-missing-changeid.txt +++ b/Documentation/error-missing-changeid.txt
@@ -1,3 +1,4 @@ +:linkattrs: = commit xxxxxxx: missing Change-Id in message footer With this error message Gerrit rejects to push a commit to a project @@ -6,7 +7,7 @@ a Change-Id. You can see the commit messages for existing commits in the history -by doing a link:http://www.kernel.org/pub/software/scm/git/docs/git-log.html[git log]. +by doing a link:http://www.kernel.org/pub/software/scm/git/docs/git-log.html[git log,role=external,window=_blank]. To avoid this error you should use the link:cmd-hook-commit-msg.html[commit hook] or EGit to automatically create and insert a unique Change-Id into the commit
diff --git a/Documentation/error-missing-subject.txt b/Documentation/error-missing-subject.txt index 6ef37a4..af628fa 100644 --- a/Documentation/error-missing-subject.txt +++ b/Documentation/error-missing-subject.txt
@@ -1,3 +1,4 @@ +:linkattrs: = commit xxxxxxx: missing subject; Change-Id must be in message footer With this error message Gerrit rejects to push a commit to a project @@ -9,7 +10,7 @@ message. You can see the commit messages for existing commits in the history -by doing a link:http://www.kernel.org/pub/software/scm/git/docs/git-log.html[git log]. +by doing a link:http://www.kernel.org/pub/software/scm/git/docs/git-log.html[git log,role=external,window=_blank]. == Change-Id is the only line in the commit message
diff --git a/Documentation/error-multiple-changeid-lines.txt b/Documentation/error-multiple-changeid-lines.txt index 31567f4..f19ebddd 100644 --- a/Documentation/error-multiple-changeid-lines.txt +++ b/Documentation/error-multiple-changeid-lines.txt
@@ -1,10 +1,11 @@ +:linkattrs: = commit xxxxxxx: multiple Change-Id lines in message footer With this error message Gerrit rejects to push a commit if the commit message footer of the pushed commit contains several Change-Id lines. You can see the commit messages for existing commits in the history -by doing a link:http://www.kernel.org/pub/software/scm/git/docs/git-log.html[git log]. +by doing a link:http://www.kernel.org/pub/software/scm/git/docs/git-log.html[git log,role=external,window=_blank]. If it was the intention to rework a change and to push a new patch set, find the change in the Gerrit Web UI, copy its Change-Id line and
diff --git a/Documentation/error-no-new-changes.txt b/Documentation/error-no-new-changes.txt index 17422ad..10575fa 100644 --- a/Documentation/error-no-new-changes.txt +++ b/Documentation/error-no-new-changes.txt
@@ -1,3 +1,4 @@ +:linkattrs: = no new changes With this error message Gerrit rejects to push a commit if the pushed @@ -38,7 +39,7 @@ in the Gerrit Web UI will not find any change) If you need to re-push a commit you may rewrite this commit by -link:http://www.kernel.org/pub/software/scm/git/docs/git-commit.html[amending] it or doing an interactive link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[git rebase]. By rewriting the +link:http://www.kernel.org/pub/software/scm/git/docs/git-commit.html[amending,role=external,window=_blank] it or doing an interactive link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[git rebase,role=external,window=_blank]. By rewriting the commit you actually create a new commit (with a new commit ID in project scope) which can then be pushed to Gerrit.
diff --git a/Documentation/error-non-fast-forward.txt b/Documentation/error-non-fast-forward.txt index 923132e..1e9a88f3 100644 --- a/Documentation/error-non-fast-forward.txt +++ b/Documentation/error-non-fast-forward.txt
@@ -1,3 +1,4 @@ +:linkattrs: = non-fast forward With this error message Gerrit rejects a push if the remote branch can't @@ -28,8 +29,8 @@ bypassing code review, your push will be rejected with the error message 'non-fast forward'. To solve the problem you have to either -. link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[rebase] your commit on the new tip of the remote branch or -. link:http://www.kernel.org/pub/software/scm/git/docs/git-merge.html[merge] your commit with the new tip of the remote branch. +. link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[rebase,role=external,window=_blank] your commit on the new tip of the remote branch or +. link:http://www.kernel.org/pub/software/scm/git/docs/git-merge.html[merge,role=external,window=_blank] your commit with the new tip of the remote branch. Afterwards the push should be successful. @@ -46,7 +47,7 @@ Although it is considered bad practice, it is possible to allow non-fast forward updates with Git. For this the remote Git repository has to be configured to not deny non-fast forward updates (set the -link:http://www.kernel.org/pub/software/scm/git/docs/git-config.html[Git configuration] parameter 'receive.denyNonFastForwards' to +link:http://www.kernel.org/pub/software/scm/git/docs/git-config.html[Git configuration,role=external,window=_blank] parameter 'receive.denyNonFastForwards' to 'false'). Then it is possible to push a non-fast forward update by using the '--force' option.
diff --git a/Documentation/error-not-allowed-to-upload-merges.txt b/Documentation/error-not-allowed-to-upload-merges.txt index d025bd0..5ba4c69 100644 --- a/Documentation/error-not-allowed-to-upload-merges.txt +++ b/Documentation/error-not-allowed-to-upload-merges.txt
@@ -1,3 +1,4 @@ +:linkattrs: = you are not allowed to upload merges With this error message Gerrit rejects to push a merge commit if the @@ -12,7 +13,7 @@ If one of your changes could not be merged in Gerrit due to conflicts and you created the merge commit to resolve the conflicts, you might want to revert the merge and instead of this do a -link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[rebase]. +link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[rebase,role=external,window=_blank]. GERRIT
diff --git a/Documentation/error-permission-denied.txt b/Documentation/error-permission-denied.txt index 879273d..fba1d44 100644 --- a/Documentation/error-permission-denied.txt +++ b/Documentation/error-permission-denied.txt
@@ -1,10 +1,11 @@ +:linkattrs: = Permission denied (publickey) With this error message an SSH command to Gerrit is rejected if the SSH authentication is not successful. -The link:http://en.wikipedia.org/wiki/Secure_Shell[SSH] protocol can use -link:http://en.wikipedia.org/wiki/Public-key_cryptography[Public-key Cryptography] +The link:http://en.wikipedia.org/wiki/Secure_Shell[SSH,role=external,window=_blank] protocol can use +link:http://en.wikipedia.org/wiki/Public-key_cryptography[Public-key Cryptography,role=external,window=_blank] for authentication. In general configurations, Gerrit will authenticate you by the public keys known to you. Optionally, it can be configured by the administrator to allow
diff --git a/Documentation/error-push-fails-due-to-commit-message.txt b/Documentation/error-push-fails-due-to-commit-message.txt index f6e5c1f..3f992f6 100644 --- a/Documentation/error-push-fails-due-to-commit-message.txt +++ b/Documentation/error-push-fails-due-to-commit-message.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Push fails due to commit message If Gerrit rejects pushing a commit it is often the case that there is @@ -6,7 +7,7 @@ If the commit message of the last commit needs to be fixed you can simply amend the last commit (please find a detailed description in -the link:http://www.kernel.org/pub/software/scm/git/docs/git-commit.html[Git documentation]): +the link:http://www.kernel.org/pub/software/scm/git/docs/git-commit.html[Git documentation,role=external,window=_blank]): ---- $ git commit --amend @@ -17,7 +18,7 @@ rebase for the affected commits. While doing the interactive rebase you can e.g. choose 'reword' for those commits for which you want to fix the commit messages. For a detailed description of git rebase -please check the link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[Git documentation]. +please check the link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[Git documentation,role=external,window=_blank]. Please use interactive git rebase with care as it rewrites existing commits. Generally you should never rewrite commits that have already
diff --git a/Documentation/error-same-change-id-in-multiple-changes.txt b/Documentation/error-same-change-id-in-multiple-changes.txt index b6aad69..4ff623d 100644 --- a/Documentation/error-same-change-id-in-multiple-changes.txt +++ b/Documentation/error-same-change-id-in-multiple-changes.txt
@@ -1,3 +1,4 @@ +:linkattrs: = same Change-Id in multiple changes With this error message Gerrit rejects to push a commit if it @@ -64,7 +65,7 @@ the example above where the last two commits have the same Change-Id, this means an interactive rebase for the last two commits should be done. For further details about the git rebase command please check -the link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[Git documentation for rebase]. +the link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[Git documentation for rebase,role=external,window=_blank]. ---- $ git rebase -i HEAD~2 @@ -100,7 +101,7 @@ by using a link:cmd-hook-commit-msg.html[commit hook] or by using EGit) or the Change-Id could be removed (not recommended since then amending this commit to create subsequent patch sets is more error prone). To change the Change-Id -of an existing commit do an interactive link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[git rebase] and fix the +of an existing commit do an interactive link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[git rebase,role=external,window=_blank] and fix the affected commit messages.
diff --git a/Documentation/error-upload-denied.txt b/Documentation/error-upload-denied.txt index 30c5f2d..6638335 100644 --- a/Documentation/error-upload-denied.txt +++ b/Documentation/error-upload-denied.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Upload denied for project ... With this error message Gerrit rejects to push a commit if the @@ -9,7 +10,7 @@ . contact one of the project owners and request upload permissions for the project (access right link:access-control.html#category_push['Push']) -. export your commit as a patch using the link:http://www.kernel.org/pub/software/scm/git/docs/git-format-patch.html[git format-patch] command +. export your commit as a patch using the link:http://www.kernel.org/pub/software/scm/git/docs/git-format-patch.html[git format-patch,role=external,window=_blank] command and provide the patch file to one of the project owners
diff --git a/Documentation/images/user-review-ui-change-complex-reply-dialogue.png b/Documentation/images/user-review-ui-change-complex-reply-dialogue.png new file mode 100644 index 0000000..1286852 --- /dev/null +++ b/Documentation/images/user-review-ui-change-complex-reply-dialogue.png Binary files differ
diff --git a/Documentation/images/user-review-ui-change-page-download.png b/Documentation/images/user-review-ui-change-page-download.png new file mode 100644 index 0000000..63c4ee3 --- /dev/null +++ b/Documentation/images/user-review-ui-change-page-download.png Binary files differ
diff --git a/Documentation/images/user-review-ui-change-page-patchset-dropdown.png b/Documentation/images/user-review-ui-change-page-patchset-dropdown.png new file mode 100644 index 0000000..f71473e --- /dev/null +++ b/Documentation/images/user-review-ui-change-page-patchset-dropdown.png Binary files differ
diff --git a/Documentation/images/user-review-ui-change-relation-chain.png b/Documentation/images/user-review-ui-change-relation-chain.png new file mode 100644 index 0000000..19942f1 --- /dev/null +++ b/Documentation/images/user-review-ui-change-relation-chain.png Binary files differ
diff --git a/Documentation/images/user-review-ui-change-reply-dialogue.png b/Documentation/images/user-review-ui-change-reply-dialogue.png new file mode 100644 index 0000000..3c95852 --- /dev/null +++ b/Documentation/images/user-review-ui-change-reply-dialogue.png Binary files differ
diff --git a/Documentation/index.txt b/Documentation/index.txt index 77e0ed4..d270f97 100644 --- a/Documentation/index.txt +++ b/Documentation/index.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review for Git == Quickstarts @@ -9,13 +10,14 @@ . link:intro-quick.html[Product Overview] . link:intro-how-gerrit-works.html[How Gerrit Works] . link:intro-gerrit-walkthrough.html[Basic Gerrit Walkthrough] +.. link:intro-gerrit-walkthrough-github.html[Basic Gerrit Walkthrough -- For GitHub Users] . link:dev-community.html[Gerrit Community] .. link:dev-contributing.html[Contributor Guide] == Guides . link:intro-user.html[User Guide] . link:intro-project-owner.html[Project Owner Guide] -. link:https://source.android.com/source/developing[Default Android Workflow] (external) +. link:https://source.android.com/source/developing[Default Android Workflow,role=external,window=_blank] (external) == Tutorials . Web @@ -42,7 +44,7 @@ . link:access-control.html[Access Controls] . Multi-project management .. link:user-submodules.html[Submodules] -.. link:https://source.android.com/source/using-repo.html[Repo] (external) +.. link:https://source.android.com/source/using-repo.html[Repo,role=external,window=_blank] (external) . Prolog rules .. link:prolog-cookbook.html[Prolog Cookbook] .. link:prolog-change-facts.html[Prolog Facts for Gerrit Changes] @@ -83,11 +85,11 @@ == Resources * link:licenses.html[Licenses and Notices] -* link:https://www.gerritcodereview.com/[Homepage] -* link:https://gerrit-releases.storage.googleapis.com/index.html[Downloads] -* link:https://bugs.chromium.org/p/gerrit/issues/list[Issue Tracking] -* link:https://gerrit.googlesource.com/gerrit[Source Code] -* link:https://www.gerritcodereview.com/about.md[A History of Gerrit Code Review] +* link:https://www.gerritcodereview.com/[Homepage,role=external,window=_blank] +* link:https://gerrit-releases.storage.googleapis.com/index.html[Downloads,role=external,window=_blank] +* link:https://bugs.chromium.org/p/gerrit/issues/list[Issue Tracking,role=external,window=_blank] +* link:https://gerrit.googlesource.com/gerrit[Source Code,role=external,window=_blank] +* link:https://www.gerritcodereview.com/about.md[A History of Gerrit Code Review,role=external,window=_blank] GERRIT ------
diff --git a/Documentation/install-j2ee.txt b/Documentation/install-j2ee.txt index 48751b7..fb78a87 100644 --- a/Documentation/install-j2ee.txt +++ b/Documentation/install-j2ee.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - J2EE Installation == Description @@ -47,7 +48,7 @@ Download and unzip a release version of Jetty. From here on we call the unpacked directory `$JETTY_HOME`. -* link:http://www.eclipse.org/jetty/downloads.php[Jetty Downloads] +* link:http://www.eclipse.org/jetty/downloads.php[Jetty Downloads,role=external,window=_blank] If this is a fresh installation of Jetty, move into the installation directory and do some cleanup to remove the sample webapps: @@ -99,7 +100,7 @@ Excerpt from the link:https://tomcat.apache.org/tomcat-7.0-doc/config/systemprops.html[ -documentation]: +documentation,role=external,window=_blank]: ---- Property org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH:
diff --git a/Documentation/install.txt b/Documentation/install.txt index fe32029..dd305d8 100644 --- a/Documentation/install.txt +++ b/Documentation/install.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - Standalone Daemon Installation Guide [[prerequisites]] @@ -5,7 +6,7 @@ To run the Gerrit service, the following requirement must be met on the host: -* JRE, version 1.8 http://www.oracle.com/technetwork/java/javase/downloads/index.html[Download] +* JRE, version 1.8 http://www.oracle.com/technetwork/java/javase/downloads/index.html[Download,role=external,window=_blank] + Gerrit is not yet compatible with Java 9 or newer at this time. @@ -22,8 +23,8 @@ . Download the unlimited strength JCE policy files. + -- link:http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html[JDK7 JCE policy files] -- link:http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html[JDK8 JCE policy files] +- link:http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html[JDK7 JCE policy files,role=external,window=_blank] +- link:http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html[JDK8 JCE policy files,role=external,window=_blank] . Uncompress and extract the downloaded file. + The downloaded file contains the following files: @@ -47,7 +48,7 @@ Current and past binary releases of Gerrit can be obtained from the link:https://gerrit-releases.storage.googleapis.com/index.html[ -Gerrit Releases site]. +Gerrit Releases site,role=external,window=_blank]. Download any current `*.war` package. The war will be referred to as `gerrit.war` from this point forward, so you may find it easier to @@ -173,7 +174,7 @@ The `ssh-keygen` command must be available during the init phase to generate SSH host keys. If you have -link:https://git-for-windows.github.io/[Git for Windows] installed, +link:https://git-for-windows.github.io/[Git for Windows,role=external,window=_blank] installed, start Command Prompt and temporary add directory with ssh-keygen to the PATH environment variable just before running init command: @@ -197,7 +198,7 @@ To install Gerrit as Windows Service use the link:http://commons.apache.org/proper/commons-daemon/procrun.html[Apache -Commons Daemon Procrun]. +Commons Daemon Procrun,role=external,window=_blank]. Sample install command: @@ -235,7 +236,7 @@ Gerrit's internal SSH daemon. See the `git-daemon` documentation for details on how to configure this if anonymous access is desired. -* http://www.kernel.org/pub/software/scm/git/docs/git-daemon.html[man git-daemon] +* http://www.kernel.org/pub/software/scm/git/docs/git-daemon.html[man git-daemon,role=external,window=_blank] [[plugins]] @@ -246,7 +247,7 @@ == External Documentation Links -* http://www.kernel.org/pub/software/scm/git/docs/git-daemon.html[git-daemon] +* http://www.kernel.org/pub/software/scm/git/docs/git-daemon.html[git-daemon,role=external,window=_blank] [[backup]]
diff --git a/Documentation/intro-gerrit-walkthrough-github.txt b/Documentation/intro-gerrit-walkthrough-github.txt new file mode 100644 index 0000000..f16155b --- /dev/null +++ b/Documentation/intro-gerrit-walkthrough-github.txt
@@ -0,0 +1,264 @@ +:linkattrs: += Basic Gerrit Walkthrough -- For GitHub Users + + +[NOTE] +==== +This document aims to provide a concise description of the core principles of +code review in Gerrit for people that were previously using Pull Requests on +Github or similar concepts. Nothing in this document is meant to state that +one or the other might be better, but only aims to help new users understand +Gerrit more readily. We use Github as the point of comparison since it seems +to be the most popular service. +==== + +To illustrate the differences in a meaningful order, we will walk you through +the process of cloning a repo, making a change, asking for code review, +iterating on the code and finally having it submitted to the code base. This +document also does not aim to describe all features of Gerrit. Please refer to +the link:intro-gerrit-walkthrough.html[Basic Gerrit Walkthrough] or +link:index.html[the rest of the documentation] for a more complete overview and additional pointers. + +[[tldr]] +== tl;dr + +Here’s how getting code reviewed and submitted with Gerrit is different from +doing the same with GitHub: + +* You need the add a commit-msg hook script when you clone a repo for the first +time using a snippet you can find e.g. https://gerrit-review.googlesource.com/admin/repos/gerrit[here,role=external,window=_blank]; +* Your review will be on a single commit instead of a branch. You use +`git commit --amend` to modify a code change. +* Instead of using the Web UI to create a pull request, you use +`git push origin HEAD:refs/for/master` to upload new local commits that are +ready for review to Gerrit. You will find the URL to the review in the output of +the push command. +* As a reviewer, Gerrit offers a number of so-called labels to vote on, one of +which is Code-Review. You indicate a negative, neutral or positive review using +a -1, 0 or +1 vote. +* To be able to submit (== merge) a change, you usually need a +2 Code-Review +vote and possibly additional positive votes, depending on the configuration of +the project you are contributing to. + +[[clone]] +== 1. Cloning a Repository + +[NOTE] +==== +Both GitHub and Gerrit provide simple Git repository hosting (of course both can +do much more). In the simplest setup, you could just use both as such without +any code review to push code. We will assume that this is not what you want to +do and focus on the use case where your change requires a review. +==== + +The first step to working with the code is to clone the repo. For both, Gerrit +and GitHub, you can simply use the `git clone` command. + +For Gerrit, there is an additional step before you can start making changes. For +reasons we explain below, you’ll have to add a https://gerrit-review.googlesource.com/Documentation/user-changeid.html[commit-msg hook,role=external,window=_blank] script. This will +append the Gerrit Change-Id to every commit message such that Gerrit can track +commits through the review process. To make this process a little easier in +Gerrit, you can find a command snippet for cloning and adding the commit-msg +hook on the repository page (e.g. https://gerrit-review.googlesource.com/admin/repos/gerrit[here,role=external,window=_blank]). + +[[create-change]] +== 2. Making a Change + +*Branches* + +Now that you have the code in the git repo on your machine, you can start making +changes. With GitHub, you would usually create a new branch and then start +committing to it. This branch would then contain all the changes you share with +your code reviewers in the next step. Your local branch will usually also be +pushed to the remote server. This can be handy to back up your work or hand-off +work to another device or developer. + +With Gerrit, you can also create a new local branch to develop in. While not +required, it can be considered a best practice to sandbox this change from other +changes you might be making. In contrast to the GitHub model, your local branch +will not have to be pushed to the remote in Gerrit, at least not for the +purposes of code review. + +*Commits* +In Gerrit, a single commit is the unit of code that will be reviewed. With +GitHub, you can commit to your branch as much as you like and the sum of all +your commits on that branch will get reviewed. As a single commit gets reviewed +in Gerrit, you need to `git commit --amend` when you iterate on the same change as +opposed to only using `git commit` with GitHub (see Section 5 for more). You can, +however, also add another commit on top of your existing commit in Gerrit, which +will create a second change (and thus another review) that is based on your +first change. Gerrit will show the relationship between these two changes as a +so-called relation chain. This also means that your second change can only be +submitted after the first was successfully merged. In many basic use cases, this +situation is however not what you want. + +image::images/user-review-ui-change-relation-chain.png[Relation chain display on the change page.] + +With GitHub, you may be pushing your branch to the remote for non-code-review +purposes, as mentioned above. You usually do not do this with Gerrit, as +Gerrit-managed repos often only have one or a few branches on the server that +can only be merged into via code review. + +[[request-review]] +== 3. Asking for Code Review + +After you are satisfied with the changes you made, you’ll usually want/need to +get your code reviewed. In GitHub, you would push your branch to the remote, go +to the Web UI and create a pull request. In Gerrit, you need to push your commit +(or the series of changes/commits) to the remote first, since you usually +develop in a local branch only. While you can often just use git push with +GitHub, you need to do a slightly different thing for Gerrit. Gerrit uses a +“magic” branch that tells the server that this code is supposed to be reviewed. +To send the changes you made on your local branch to review and being eventually +merged into the remote’s master branch, you use +`git push origin HEAD:refs/for/master`. There are also link:user-upload.html#_git_push[a number of Gerrit change +options] you can trigger from the CLI this way. + +After successfully pushing your change to Gerrit, you will already find the URL +for viewing your change in Gerrit’s Web UI in the response you get from the +server. The description of the Gerrit code review that was just created is equal +to the commit message of that one commit the change is based on. In GitHub, you +might have described your change in the message you can create when creating the +pull request in the GitHub Web UI. + +Next, you would go and visit your Gerrit change in the Web UI to get your change +ready for review (choose reviewers, cc people, check for failing CI builds or +tests, etc.), very similar to what you do on Github. Reviewers will be notified +via email once you add them. By default, anyone can add reviewers to a Gerrit +change. In GitHub, this ability is reserved for certain users, so you may have +relied on others adding reviewers for you before. This can be the case in a +Gerrit project, but it is also often expected that the change owner (usually the +creator of the change) adds reviewers to get the review process started. + +[[reviewing]] +== 4. Reviewing a Change + +Switching perspectives briefly, reviewing a change is fairly similar between +GitHub and Gerrit. You, as a reviewer, will be notified of a change you have +been added to via email or see an “incoming” change on your Gerrit dashboard. +The dashboard is the central overview of changes going on within a Gerrit +instance. By default, the dashboard shows changes that you are involved in, in +any way. You can also see all changes on a Gerrit server by using the top menu +(“Changes” -> “Open”). This view is more similar to what you see on Github, when +you navigate to the Pull Requests tab of the project/repository you are working +on. Note, however, that a single Gerrit instance can host multiple projects +(also referred to as repositories; a list can be found, for example, https://gerrit-review.googlesource.com/admin/repos[here,role=external,window=_blank]). Your +dashboard and other lists of changes will show all changes across the +projects/repositories by default. + +Back to your dashboard, you can click on the change you want to review. You can +also access this from the email you received. You will see the same view that +you saw as an author. In the middle of the change page, you can find the list of +files that have been modified, just like what you find in the “Files changed” +tab of GitHub. Also similarly, you can leave comments by highlighting a piece of +the code and pressing ‘c’. All comments you make are in a draft state and thus +only visible to you, like on GitHub. When you are done with your review, you +need to click the “Reply” button at the top of the change page to send your +assessment to the change owner alongside a “change message” summarizing your +findings and/or adding higher level comments. Replying to a change makes your +draft comments and the change message visible on the change page for everyone +that has view access to this change. This again is fairly similar to GitHub, +except for Gerrit’s voting labels. + +image::images/user-review-ui-change-reply-dialogue.png[Reply dialogue for a Gerrit change.] + +As you can see in the screenshot of the reply dialogue, the voting labels are in +the bottom part of the dialogue. They can be fairly simple as in this case, but +there can also be a larger number of labels you might be able to vote on. Labels +can be used to distinguish different aspects of a review (e.g. whether or not +the licensing of included libraries is okay), outcome of CI systems (e.g. +whether or not a format checker passed, a build completed successfully, etc.) or +as a flag that is read by bots to do something with a change. An example of a +more complex label setup can be seen in this screenshot from the Android Gerrit +instance. + +image::images/user-review-ui-change-complex-reply-dialogue.png[Reply dialogue for a change on the Android project.] + +In the simplest case shown above, voting -1 on the Code-Review label equals +requesting changes on a GitHub pull request, 0 equals just having comments and ++1 means that you think this change looks good. Usually, Gerrit changes require +a +2 vote on the Code-Review label to be submitted (merged in GitHub terms, see +Section 6 below). Being able to vote +2 on Code-Review is often restricted to +maintainers of a given project, so they can have a final say on a change. These +practices can however vary between projects, as labels and voting permissions +are configurable. + +[[iterate]] +== 5. Iterating on the Change + +After your reviewers got back to you as a change owner, you realize that you +need to make a few updates to the code in your change. As mentioned in Section 2 +(Making a Change), you’ll have to amend the commit that this review was based +on. To do that, you might have to checkout the respective commit first if it is +not at the tip of your local branch, for example if you stacked multiple changes +on top of each other. Another common use case is to not have a local branch but +to work in the so-called https://www.git-tower.com/learn/git/faq/detached-head-when-checkout-commit["detached HEAD",role=external,window=_blank] mode. In that case you can use the +“Download” button on the files tab to copy a command that fetches and checks out +the commit underlying your change. Make sure to select the latest patchset, +though! + +image::images/user-review-ui-change-page-download.png[Using the “Download” button to copy a command that checks out a given patchset for a change.] + +After checking out the commit, you then make the changes as usual. When you +think you are done, you can commit with the `--amend` flag to change the commit +you currently have checked out. + +When you `git commit --amend` to iterate on your change, you might be worried that +you are changing your previous commit and may thus lose that state of your work. +However, here the Change-Id appended to your commit message comes into play. +While the SHA1 hash of your change (the commit ID used by Git) might change, the +Change-Id stays the same (in fact it is the SHA1 hash of the very first version +of that commit). When this amended commit is uploaded to the Gerrit server, +Gerrit knows that this commit is really an iteration of that previous commit +(and the associated review) and will preserve both, the old and the new state. +All previous states of your commit will be visible in the Gerrit UI as so-called +patchsets (and link:intro-user.html#change-ref[from the Git repo]). + +image::images/user-review-ui-change-page-patchset-dropdown.png[Screenshot of the patchset dropdown above the file list, showing all iterations a commit went through.] + +After iterating as much as needed, your reviewers will finally be satisfied. +With GitHub, you would have a string of additional commits in the branch you +used for opening the pull request. In Gerrit, you still only have that one +commit in your local branch. All the iterations are available as patchsets in +the Web UI as well as from the special branch mentioned above. + +[[submit]] +== 6. Submitting a Change + +Finally, it is time to submit your change. As mentioned above, the precondition +for this in Gerrit is usually at least a +2 vote on the Code-Review label. With +GitHub, an authorized person must have given an “Approve” vote. Once this +precondition has been met, anyone with submit permission can submit the change +in Gerrit. To do that, you click the “Submit” button in the Gerrit Web UI just +as you would click the “Merge Pull Request” button in GitHub. Both, Gerrit and +GitHub, allow different merge strategies, that can be enabled by project +administrators. In Gerrit, a merge strategy is configured for each project and +cannot be changed at submit time while this may be possible with GitHub, +depending on project configuration. + +A merge can fail due to conflicts with competing edits on the target branch. +With GitHub, you may be able to resolve some simple conflicts directly from the +Web UI. In Gerrit, you can attempt to rebase a change from the Web UI. If there +are no conflicts, a new patchset will automatically appear. Otherwise, similar +to GitHub, you need to resolve conflicts on the command line with your local +clone of the repository. While you resolve conflicts that arise from a +`git merge` for GitHub, you will need to link:intro-user.html#rebase[use `git rebase` with your change] on +Gerrit. + +After resolving locally, with GitHub, you end up with another commit on your +pull request branch and push it to the server, which should then allow you to +finish merging the change. With Gerrit, resolving the conflict through rebasing +your commit/change results in another amended version of that same commit and +you upload it to Gerrit, resulting in a new patchset just like your previous +iterations addressing reviewer comments. This new patchset will usually require +another round of reviewer votes, as Gerrit will not copy votes from a previous +patchset by default. + + +GERRIT +------ + +Part of link:index.html[Gerrit Code Review] + +SEARCHBOX +---------
diff --git a/Documentation/intro-gerrit-walkthrough.txt b/Documentation/intro-gerrit-walkthrough.txt index b4f799c2..92732d0 100644 --- a/Documentation/intro-gerrit-walkthrough.txt +++ b/Documentation/intro-gerrit-walkthrough.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Working with Gerrit: An example To understand how Gerrit works, let's follow a change through its entire @@ -130,7 +131,7 @@ while the *Verified* check is done by an automated build server, through a mechanism such as the link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger[Gerrit Trigger -Jenkins Plugin]. +Jenkins Plugin,role=external,window=_blank]. IMPORTANT: The Code-Review and Verified checks require different permissions in Gerrit. This requirement allows teams to separate these tasks. For example, @@ -186,6 +187,7 @@ * Check out the commit * Amend the commit +* Rebase the commit if needed * Push the commit to Gerrit ---- @@ -195,6 +197,20 @@ [master 30a6f44] Change to a proper, yeast based pizza dough. Date: Fri Jun 8 16:28:23 2018 +0200 1 file changed, 10 insertions(+), 5 deletions(-) +---- + +At this point Max wants to make sure that his change is on top of the branch. + +---- +$ git fetch +$ +---- + +Max got no output from the fetch command, which is good news. +This means that the master branch has not progressed and there is no need for link:intro-user.html#rebase[*rebase*]. +Max is now ready to push his change: + +---- $ git push origin HEAD:refs/for/master Counting objects: 3, done. Delta compression using up to 8 threads. @@ -237,7 +253,7 @@ can add custom checks or even remove the Verified check entirely. Verification is typically an automated process using the -link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger[Gerrit Trigger Jenkins Plugin] +link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger[Gerrit Trigger Jenkins Plugin,role=external,window=_blank] or a similar mechanism. However, there are still times when a change requires manual verification, or a reviewer needs to check how or if a change works. To accommodate these and other similar circumstances, Gerrit exposes each change
diff --git a/Documentation/intro-project-owner.txt b/Documentation/intro-project-owner.txt index 1f98291..7f1e82b 100644 --- a/Documentation/intro-project-owner.txt +++ b/Documentation/intro-project-owner.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Project Owner Guide This is a Gerrit guide that is dedicated to project owners. It @@ -181,7 +182,7 @@ to be prefixed with `ldap/`. If the link:https://gerrit-review.googlesource.com/admin/repos/plugins/singleusergroup[ -singleusergroup] plugin is installed you can also directly assign +singleusergroup,role=external,window=_blank] plugin is installed you can also directly assign access rights to users, by prefixing the username with `user/` or the user's account ID by `userid/`. @@ -374,10 +375,10 @@ systems. The most commonly used are: - link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger[ - Gerrit Trigger] plugin for link:http://jenkins-ci.org/[Jenkins] + Gerrit Trigger,role=external,window=_blank] plugin for link:http://jenkins-ci.org/[Jenkins,role=external,window=_blank] - link:http://www.mediawiki.org/wiki/Continuous_integration/Zuul[ - Zuul] for link:http://jenkins-ci.org/[Jenkins] + Zuul,role=external,window=_blank] for link:http://jenkins-ci.org/[Jenkins,role=external,window=_blank] For the integration with the continuous integration system you must have a service user that is able to access Gerrit. To create a service @@ -387,7 +388,7 @@ a Gerrit administrator to create the service user. If the link:https://gerrit-review.googlesource.com/admin/repos/plugins/serviceuser[ -serviceuser] plugin is installed you can also create new service users +serviceuser,role=external,window=_blank] plugin is installed you can also create new service users in the Gerrit Web UI under `People` > `Create Service User`. For this the `Create Service User` global capability must be assigned. @@ -407,7 +408,7 @@ Gerrit provides an link:https://gerrit-review.googlesource.com/Documentation/config-validation.html#new-commit-validation[ -extension point to do validation of new commits]. A Gerrit plugin +extension point to do validation of new commits,role=external,window=_blank]. A Gerrit plugin implementing this extension point can perform validation checks when new commits are pushed to Gerrit. The plugin can either provide a message to the client or reject the commit and cause the push to fail. @@ -415,13 +416,13 @@ There are some plugins available that provide commit validation: - link:https://gerrit-review.googlesource.com/admin/repos/plugins/uploadvalidator[ - uploadvalidator]: + uploadvalidator,role=external,window=_blank]: + The `uploadvalidator` plugin allows project owners to configure blocked file extensions, required footers and a maximum allowed path length. - link:https://gerrit-review.googlesource.com/admin/repos/plugins/commit-message-length-validator[ - commit-message-length-validator] + commit-message-length-validator,role=external,window=_blank] + The `commit-message-length-validator` core plugin validates that commit messages conform to line length limits. @@ -501,9 +502,9 @@ - Issue Tracker System Plugins + There are Gerrit plugins for a tight integration with -link:https://gerrit-review.googlesource.com//admin/repos/plugins/its-jira[Jira], -link:https://gerrit-review.googlesource.com/admin/repos/plugins/its-bugzilla[Bugzilla] and -link:https://gerrit-review.googlesource.com/admin/repos/plugins/its-rtc[IBM Rational Team Concert]. +link:https://gerrit-review.googlesource.com//admin/repos/plugins/its-jira[Jira,role=external,window=_blank], +link:https://gerrit-review.googlesource.com/admin/repos/plugins/its-bugzilla[Bugzilla,role=external,window=_blank] and +link:https://gerrit-review.googlesource.com/admin/repos/plugins/its-rtc[IBM Rational Team Concert,role=external,window=_blank]. If installed, these plugins can e.g. be used to automatically add links to Gerrit changes to the issues in the issue tracker system or to automatically close an issue if the corresponding change is merged. @@ -551,15 +552,15 @@ Gerrit will then notify this person by email about the review request. With the link:https://gerrit-review.googlesource.com/admin/repos/plugins/reviewers[ -reviewers] plugin it is possible to configure default reviewers who +reviewers,role=external,window=_blank] plugin it is possible to configure default reviewers who will be automatically added to each change. The default reviewers can be configured in the Gerrit Web UI under `Projects` > `List` > <your project> > `General` in the `reviewers Plugin` section. The link:https://gerrit-review.googlesource.com/admin/repos/plugins/reviewers-by-blame[ -reviewers-by-blame] plugin can automatically add reviewers to changes +reviewers-by-blame,role=external,window=_blank] plugin can automatically add reviewers to changes based on the link:https://www.kernel.org/pub/software/scm/git/docs/git-blame.html[ -git blame] computation on the changed files. This means that the plugin +git blame,role=external,window=_blank] computation on the changed files. This means that the plugin will add those users as reviewer that authored most of the lines touched by the change, since these users should be familiar with the code and can most likely review the change. How many reviewers the @@ -578,7 +579,7 @@ plugins: - link:https://gerrit-review.googlesource.com/admin/repos/plugins/download-commands[ - download-commands] plugin: + download-commands,role=external,window=_blank] plugin: + The `download-commands` plugin provides the default download commands (`Checkout`, `Cherry Pick`, `Format Patch` and `Pull`). @@ -587,14 +588,14 @@ the change screen. - link:https://gerrit-review.googlesource.com/admin/repos/plugins/egit[ - egit] plugin: + egit,role=external,window=_blank] plugin: + The `egit` plugin provides the change ref as a download command, which is needed for downloading a change from within link:https://www.eclipse.org/egit/[EGit]. - link:https://gerrit-review.googlesource.com/admin/repos/plugins/project-download-commands[ - project-download-commands] plugin: + project-download-commands,role=external,window=_blank] plugin: + The `project-download-commands` plugin enables project owners to configure project-specific download commands. For example, a @@ -678,14 +679,14 @@ contains this history. If your existing codebase is in another VCS you must migrate it to Git first. For Subversion you can use the link:http://git-scm.com/book/en/Git-and-Other-Systems-Git-and-Subversion[ -git svn] command as described in the +git svn,role=external,window=_blank] command as described in the link:http://git-scm.com/book/en/Git-and-Other-Systems-Migrating-to-Git#Subversion[ -Subversion migration guide]. An importer for Perforce is available in +Subversion migration guide,role=external,window=_blank]. An importer for Perforce is available in the `contrib` section of the Git source code; how to use -link:http://git-scm.com/docs/git-p4[git p4] to do the import from +link:http://git-scm.com/docs/git-p4[git p4,role=external,window=_blank] to do the import from Perforce is described in the link:http://git-scm.com/book/en/Git-and-Other-Systems-Migrating-to-Git#Perforce[ -Perforce migration guide]. +Perforce migration guide,role=external,window=_blank]. To import an existing history into a Gerrit project you bypass code review and push it directly to `refs/heads/<branch>`. For this you must @@ -699,7 +700,7 @@ link:access-control.html#category_forge_committer[Forge Committer] access right globally. In this case you must use the link:https://www.kernel.org/pub/software/scm/git/docs/git-filter-branch.html[ -git filter-branch] command to rewrite the committer information for all +git filter-branch,role=external,window=_blank] command to rewrite the committer information for all commits (the author information that records who was writing the code stays intact; signed tags will lose their signature): @@ -741,7 +742,7 @@ Gerrit core does not support the deletion of projects. If the link:https://gerrit-review.googlesource.com/admin/repos/plugins/delete-project[ -delete-project] plugin is installed, projects can be deleted from the +delete-project,role=external,window=_blank] plugin is installed, projects can be deleted from the Gerrit Web UI under `Projects` > `List` > <project> > `General` by clicking on the `Delete` command under `Project Commands`. The `Delete` command is only available if you have the `Delete Projects` global @@ -769,7 +770,7 @@ history (changes, review comments) is lost. Alternatively, you can use the -link:https://gerrit.googlesource.com/plugins/importer/[importer] plugin +link:https://gerrit.googlesource.com/plugins/importer/[importer,role=external,window=_blank] plugin to copy the project _including the review history_, and then link:#project-deletion[delete the old project].
diff --git a/Documentation/intro-quick.txt b/Documentation/intro-quick.txt index 11d5052..b8670ed 100644 --- a/Documentation/intro-quick.txt +++ b/Documentation/intro-quick.txt
@@ -1,7 +1,8 @@ +:linkattrs: = Gerrit Code Review Product Overview Gerrit Code Review is a web-based code review tool built on -https://git-scm.com/[Git version control]. +https://git-scm.com/[Git version control,role=external,window=_blank]. == What is Gerrit Code Review? @@ -45,7 +46,7 @@ . link:intro-user.html[User Guide] . link:intro-project-owner.html[Project Owner Guide] -. link:https://source.android.com/source/life-of-a-patch[Default Android Workflow] (external) +. link:https://source.android.com/source/life-of-a-patch[Default Android Workflow,role=external,window=_blank] (external) GERRIT ------
diff --git a/Documentation/intro-rockstar.txt b/Documentation/intro-rockstar.txt index 0b67950..4a7167b 100644 --- a/Documentation/intro-rockstar.txt +++ b/Documentation/intro-rockstar.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Use Gerrit to Be a Rockstar Programmer == Overview @@ -42,7 +43,7 @@ * git cherry-pick -* link:https://www.kernel.org/pub/software/scm/git/docs/git-bisect-lk2009.html[git bisect] +* link:https://www.kernel.org/pub/software/scm/git/docs/git-bisect-lk2009.html[git bisect,role=external,window=_blank] [[amending]] @@ -59,8 +60,8 @@ At least two well-known open source projects insist on these practices: -* link:http://git-scm.com/[Git] -* link:http://www.kernel.org/category/about.html[Linux Kernel] +* link:http://git-scm.com/[Git,role=external,window=_blank] +* link:http://www.kernel.org/category/about.html[Linux Kernel,role=external,window=_blank] However, contributors to these projects don’t refine and polish their changes in private until they’re perfect. Instead, polishing code is part of a review
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt index 16929ae..6f55398 100644 --- a/Documentation/intro-user.txt +++ b/Documentation/intro-user.txt
@@ -1,10 +1,11 @@ +:linkattrs: = User Guide This is a Gerrit guide that is dedicated to Gerrit end-users. It explains the standard Gerrit workflows and how a user can adapt Gerrit to personal preferences. -It is expected that readers know about link:http://git-scm.com/[Git] +It is expected that readers know about link:http://git-scm.com/[Git,role=external,window=_blank] and that they are familiar with basic git commands and workflows. [[gerrit]] @@ -21,20 +22,20 @@ Gerrit uses the git protocol. This means in order to work with Gerrit you do *not* need to install any Gerrit client, but having a regular -git client, such as the link:http://git-scm.com/[git command line] or -link:http://eclipse.org/egit/[EGit] in Eclipse, is sufficient. +git client, such as the link:http://git-scm.com/[git command line,role=external,window=_blank] or +link:http://eclipse.org/egit/[EGit,role=external,window=_blank] in Eclipse, is sufficient. Still there are some client-side tools for Gerrit, which can be used optionally: -* link:http://eclipse.org/mylyn/[Mylyn Gerrit Connector]: Gerrit +* link:http://eclipse.org/mylyn/[Mylyn Gerrit Connector,role=external,window=_blank]: Gerrit integration with Mylyn * link:https://github.com/uwolfer/gerrit-intellij-plugin[Gerrit - IntelliJ Plugin]: Gerrit integration with the - link:http://www.jetbrains.com/idea/[IntelliJ Platform] + IntelliJ Plugin,role=external,window=_blank]: Gerrit integration with the + link:http://www.jetbrains.com/idea/[IntelliJ Platform,role=external,window=_blank] * link:https://play.google.com/store/apps/details?id=com.jbirdvegas.mgerrit[ - mGerrit]: Android client for Gerrit -* link:https://github.com/stackforge/gertty[Gertty]: Console-based + mGerrit,role=external,window=_blank]: Android client for Gerrit +* link:https://github.com/stackforge/gertty[Gertty,role=external,window=_blank]: Console-based interface for Gerrit [[clone]] @@ -209,7 +210,7 @@ Instead of manually installing the `commit-msg` hook for each git repository, you can copy it into the link:http://git-scm.com/docs/git-init#_template_directory[git template -directory]. Then it is automatically copied to every newly cloned +directory,role=external,window=_blank]. Then it is automatically copied to every newly cloned repository. [[review-change]]
diff --git a/Documentation/js-api.txt b/Documentation/js-api.txt index 030541d..893ab36 100644 --- a/Documentation/js-api.txt +++ b/Documentation/js-api.txt
@@ -150,6 +150,13 @@ parameter the URL of the plugin is returned. If passed a string the argument is appended to the plugin URL. +A plugin's URL is where this plugin is loaded, it doesn't +necessary to be the same as the Gerrit host. Use `window.location` +if you need to access the Gerrit host info. + +For preloaded plugins, the plugin url is based on a global +configuration of where to load all plugins, default to current host. + [source,javascript] ---- self.url(); // "https://gerrit-review.googlesource.com/plugins/demo/"
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt index 66d4ff5..949298f 100644 --- a/Documentation/licenses.txt +++ b/Documentation/licenses.txt
@@ -65,7 +65,6 @@ * httpcomponents:httpcore * httpcomponents:httpcore-nio * jackson:jackson-core -* jetty:continuation * jetty:http * jetty:io * jetty:jmx
diff --git a/Documentation/linux-quickstart.txt b/Documentation/linux-quickstart.txt index 0d8848e..aafaf58 100644 --- a/Documentation/linux-quickstart.txt +++ b/Documentation/linux-quickstart.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Quickstart for Installing Gerrit on Linux This content explains how to install a basic instance of Gerrit on a Linux @@ -29,7 +30,7 @@ . Download the desired Gerrit archive. To view previous archives, see -link:https://gerrit-releases.storage.googleapis.com/index.html[Gerrit Code Review: Releases]. The steps below install Gerrit 3.0.3: +link:https://gerrit-releases.storage.googleapis.com/index.html[Gerrit Code Review: Releases,role=external,window=_blank]. The steps below install Gerrit 3.0.3: .... wget https://gerrit-releases.storage.googleapis.com/gerrit-3.0.3.war
diff --git a/Documentation/note-db.txt b/Documentation/note-db.txt index 8725cee..0505dd2 100644 --- a/Documentation/note-db.txt +++ b/Documentation/note-db.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - NoteDb Backend NoteDb is the next generation of Gerrit storage backend, which replaces the @@ -36,7 +37,7 @@ the upgrade process by running `gerrit.war init` - Account, group and change metadata on the servers behind `googlesource.com` is fully migrated to NoteDb. In other words, if you use - link:https://gerrit-review.googlesource.com/[gerrit-review], you're already + link:https://gerrit-review.googlesource.com/[gerrit-review,role=external,window=_blank], you're already using NoteDb. - NoteDb is the only database format supported by Gerrit 3.0. The change data migration tools are only included in Gerrit 2.15 and 2.16; they are not @@ -197,5 +198,5 @@ In case of rollback from NoteDB to ReviewDB, all the meta refs and the sequence ref need to be removed. -The [remove-notedb-refs.sh](https://gerrit.googlesource.com/gerrit/+/refs/heads/master/contrib/remove-notedb-refs.sh) +The [remove-notedb-refs.sh,role=external,window=_blank](https://gerrit.googlesource.com/gerrit/+/refs/heads/master/contrib/remove-notedb-refs.sh) script has been written to automate this process.
diff --git a/Documentation/pg-plugin-dev.txt b/Documentation/pg-plugin-dev.txt index d901851..fadf5d4 100644 --- a/Documentation/pg-plugin-dev.txt +++ b/Documentation/pg-plugin-dev.txt
@@ -1,8 +1,9 @@ +:linkattrs: = Gerrit Code Review - PolyGerrit Plugin Development CAUTION: Work in progress. Hard hat area. Please link:https://bugs.chromium.org/p/gerrit/issues/entry?template=PolyGerrit%20plugins[send -feedback] if something's not right. +feedback,role=external,window=_blank] if something's not right. For migrating existing GWT UI plugins, please check out the link:pg-plugin-migration.html#migration[migration guide]. @@ -11,7 +12,7 @@ == Plugin loading and initialization link:js-api.html#_entry_point[Entry point] for the plugin and the loading method -is based on link:http://w3c.github.io/webcomponents/spec/imports/[HTML Imports] +is based on link:http://w3c.github.io/webcomponents/spec/imports/[HTML Imports,role=external,window=_blank] spec. * The plugin provides pluginname.html, and can be a standalone file or a static @@ -103,7 +104,7 @@ A plugin may provide Polymer's https://www.polymer-project.org/2.0/docs/devguide/style-shadow-dom#style-modules[style -modules] to style individual endpoints using +modules,role=external,window=_blank] to style individual endpoints using `plugin.registerStyleModule(endpointName, moduleName)`. A style must be defined as a standalone `<dom-module>` defined in the same .html file. @@ -152,7 +153,7 @@ Alternative for link:https://www.polymer-project.org/1.0/docs/devguide/data-binding[Polymer data -binding] for plugins that don't use Polymer. Can be used to bind element +binding,role=external,window=_blank] for plugins that don't use Polymer. Can be used to bind element attribute changes to callbacks. See `samples/bind-parameters.html` for examples on both Polymer data bindings
diff --git a/Documentation/pg-plugin-migration.txt b/Documentation/pg-plugin-migration.txt index 3ddceed..bca4b7a 100644 --- a/Documentation/pg-plugin-migration.txt +++ b/Documentation/pg-plugin-migration.txt
@@ -1,8 +1,9 @@ +:linkattrs: = Gerrit Code Review - PolyGerrit Plugin Development CAUTION: Work in progress. Hard hat area. Please link:https://bugs.chromium.org/p/gerrit/issues/entry?template=PolyGerrit%20plugins[send -feedback] if something's not right. +feedback,role=external,window=_blank] if something's not right. [[migration]] == Incremental migration of existing GWT UI plugins
diff --git a/Documentation/pg-plugin-styling.txt b/Documentation/pg-plugin-styling.txt index 2453bad..f600376 100644 --- a/Documentation/pg-plugin-styling.txt +++ b/Documentation/pg-plugin-styling.txt
@@ -1,20 +1,21 @@ +:linkattrs: = Gerrit Code Review - PolyGerrit Plugin Styling == Plugin styles Plugins may provide link:https://www.polymer-project.org/2.0/docs/devguide/style-shadow-dom#style-modules[Polymer -style modules] for UI CSS-based customization. +style modules,role=external,window=_blank] for UI CSS-based customization. PolyGerrit UI implements number of styling endpoints, which apply CSS mixins -link:https://tabatkins.github.io/specs/css-apply-rule/[using @apply] to its +link:https://tabatkins.github.io/specs/css-apply-rule/[using @apply,role=external,window=_blank] to its direct contents. NOTE: Only items (i.e. CSS properties and mixin targets) documented here are guaranteed to work in the long term, since they are covered by integration tests. + When there is a need to add new property or endpoint, please link:https://bugs.chromium.org/p/gerrit/issues/entry?template=PolyGerrit%20Issue[file -a bug] stating your use case to track and maintain for future releases. +a bug,role=external,window=_blank] stating your use case to track and maintain for future releases. Plugins should be html-based and imported following PolyGerrit's link:pg-plugin-dev.html#loading[dev guide]. @@ -64,7 +65,7 @@ Following CSS properties have link:https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html[long-term -support via integration test]: +support via integration test,role=external,window=_blank]: * `display` +
diff --git a/Documentation/pgm-MigrateAccountPatchReviewDb.txt b/Documentation/pgm-MigrateAccountPatchReviewDb.txt index c8ab193..64a1008 100644 --- a/Documentation/pgm-MigrateAccountPatchReviewDb.txt +++ b/Documentation/pgm-MigrateAccountPatchReviewDb.txt
@@ -1,3 +1,4 @@ +:linkattrs: = MigrateAccountPatchReviewDb == NAME @@ -30,7 +31,7 @@ [NOTE] When using MySQL, the file_name column length in the account_patch_reviews table will be shortened from the standard 4096 characters down to 255 characters. This is due to a -link:https://dev.mysql.com/doc/refman/5.7/en/innodb-restrictions.html[MySQL limitation] +link:https://dev.mysql.com/doc/refman/5.7/en/innodb-restrictions.html[MySQL limitation,role=external,window=_blank] on the max size of 767 bytes for each column in an index. == OPTIONS
diff --git a/Documentation/prolog-cookbook.txt b/Documentation/prolog-cookbook.txt index f291920..21b8c9f 100644 --- a/Documentation/prolog-cookbook.txt +++ b/Documentation/prolog-cookbook.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - Prolog Submit Rules Cookbook [[SubmitRule]] @@ -23,10 +24,10 @@ link:config-gerrit.html#_a_id_rules_a_section_rules[rules section]) link:https://groups.google.com/d/topic/repo-discuss/wJxTGhlHZMM/discussion[This -discussion thread] explains why Prolog was chosen for the purpose of writing +discussion thread,role=external,window=_blank] explains why Prolog was chosen for the purpose of writing project specific submit rules. link:http://gerrit-documentation.googlecode.com/svn/ReleaseNotes/ReleaseNotes-2.2.2.html[Gerrit -2.2.2 ReleaseNotes] introduces Prolog support in Gerrit. +2.2.2 ReleaseNotes,role=external,window=_blank] introduces Prolog support in Gerrit. [[SubmitType]] == Submit Type @@ -58,14 +59,14 @@ == Prolog Language This document is not a complete Prolog tutorial. -link:http://en.wikipedia.org/wiki/Prolog[This Wikipedia page on Prolog] is a +link:http://en.wikipedia.org/wiki/Prolog[This Wikipedia page on Prolog,role=external,window=_blank] is a good starting point for learning the Prolog language. This document will only explain some elements of Prolog that are necessary to understand the provided examples. == Prolog in Gerrit -Gerrit uses its own link:https://gerrit.googlesource.com/prolog-cafe/[fork] of the -original link:http://kaminari.istc.kobe-u.ac.jp/PrologCafe/[prolog-cafe] +Gerrit uses its own link:https://gerrit.googlesource.com/prolog-cafe/[fork,role=external,window=_blank] of the +original link:http://kaminari.istc.kobe-u.ac.jp/PrologCafe/[prolog-cafe,role=external,window=_blank] project. Gerrit embeds the prolog-cafe library and can interpret Prolog programs at runtime. @@ -75,7 +76,7 @@ Prolog interpreter shell. For batch or unit tests, see the examples in Gerrit source directory -link:https://gerrit.googlesource.com/gerrit/+/refs/heads/master/prologtests/examples/[prologtests/examples]. +link:https://gerrit.googlesource.com/gerrit/+/refs/heads/master/prologtests/examples/[prologtests/examples,role=external,window=_blank]. [NOTE] The interactive shell is just a prolog shell, it does not load @@ -84,7 +85,7 @@ == SWI-Prolog Instead of using the link:pgm-prolog-shell.html[prolog-shell] program one can -also use the link:http://www.swi-prolog.org/[SWI-Prolog] environment. It +also use the link:http://www.swi-prolog.org/[SWI-Prolog,role=external,window=_blank] environment. It provides a better shell interface and a graphical source-level debugger. [[RulesFile]]
diff --git a/Documentation/quota.txt b/Documentation/quota.txt index a647e33..475ae72 100644 --- a/Documentation/quota.txt +++ b/Documentation/quota.txt
@@ -1,9 +1,10 @@ +:linkattrs: = Gerrit Code Review - Quota Gerrit does not provide out of the box quota enforcement. However, it does support an extension mechanism for plugins to hook into to provide this functionality. The most prominent plugin is the -link:https://gerrit.googlesource.com/plugins/quota/[Quota Plugin]. +link:https://gerrit.googlesource.com/plugins/quota/[Quota Plugin,role=external,window=_blank]. This documentation is intended to be read by plugin developers. It contains all quota requests implemented in Gerrit-core as well as the metadata that they have
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt index 85cdace..27a31b1 100644 --- a/Documentation/rest-api-accounts.txt +++ b/Documentation/rest-api-accounts.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - /accounts/ REST API This page describes the account related REST endpoints. @@ -2661,7 +2662,7 @@ |`id` |Not set in map context|The 8-char hex GPG key ID. |`fingerprint`|Not set for deleted keys|The 40-char (plus spaces) hex GPG key fingerprint. |`user_ids` |Not set for deleted keys| -link:https://tools.ietf.org/html/rfc4880#section-5.11[OpenPGP User IDs] +link:https://tools.ietf.org/html/rfc4880#section-5.11[OpenPGP User IDs,role=external,window=_blank] associated with the public key. |`key` |Not set for deleted keys|ASCII armored public key material. |`status` |Not set for deleted keys|
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt index 09ec415..d5abbe3 100644 --- a/Documentation/rest-api-changes.txt +++ b/Documentation/rest-api-changes.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - /changes/ REST API This page describes the change related REST endpoints. @@ -315,6 +316,11 @@ the `mergeable` field will always be omitted and `SKIP_MERGEABLE` has no effect. + +When link:config-gerrit.html#index.change.indexMergeable[ +`index.change.indexMergeable`] is set to `false` in the `gerrit.config`, +the `mergeable` field will always be omitted when querying changes and +`SKIP_MERGEABLE` has no effect. ++ A change's mergeability can be requested separately by calling the link:#get-mergeable[get-mergeable] endpoint. -- @@ -1451,6 +1457,10 @@ Reverts a change. +The subject of the newly created change will be +'Revert "<subject-of-reverted-change>"'. If the subject of the change reverted is +above 63 characters, it will be cut down to 59 characters with "..." in the end. + The request body does not need to include a link:#revert-input[ RevertInput] entity if no review comment is added. @@ -1501,6 +1511,109 @@ change is new ---- +[[revert-submission]] +=== Revert Submission +-- +'POST /changes/link:#change-id[\{change-id\}]/revert_submission' +-- + +Creates open revert changes for all of the changes of a certain submission. + +The subject of each revert change will be 'Revert "<subject-of-reverted-change"'. +If the subject is above 63 characters, the subject will be cut to 59 characters +with "..." in the end. + +Details for the revert can be specified in the request body inside a link:#revert-input[ +RevertInput] The topic of all created revert changes will be +`revert-{submission_id}-{random_string_of_size_10}`. + +The changes will not be rebased on onto the destination branch so the users may still +have to manually rebase them to resolve conflicts and make them submittable. + +However, the changes that have the same project and branch will be rebased on top +of each other. E.g, the first revert change will have the original change as a +parent, and the second revert change will have the first revert change as a +parent. + +There is one special case that involves merge commits; if a user has multiple +changes in the same project and branch, but not in the same change series, those +changes can still get submitted together if they have the same topic and +link:config-gerrit.html#change.submitWholeTopic[`change.submitWholeTopic`] in +gerrit.config is set to true. In the case, Gerrit may create merge commits on +submit (depending on the link:config-project-config.html#submit-type[submit types] +of the project). +The first parent for the reverts will be the most recent merge commit that was +created by Gerrit to merge the different change series into the target branch. + +.Request +---- + POST /changes/myProject~master~I1ffe09a505e25f15ce1521bcfb222e51e62c2a14/revert_submission HTTP/1.0 +---- + +As response link:#revert-submission-info[RevertSubmissionInfo] entity +is returned. That entity describes the revert changes. + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + "revert_changes": + [ + { + "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940", + "project": "myProject", + "branch": "master", + "topic": "revert--1571043962462-3640749-ABCEEZGHIJ", + "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940", + "subject": "Revert \"Implementing Feature X\"", + "status": "NEW", + "created": "2013-02-01 09:59:32.126000000", + "updated": "2013-02-21 11:16:36.775000000", + "mergeable": true, + "insertions": 6, + "deletions": 4, + "_number": 3965, + "owner": { + "name": "John Doe" + } + }, + { + "id": "anyProject~master~1eee2c9d8f352483781e772f35dc586a69ff5646", + "project": "anyProject", + "branch": "master", + "topic": "revert--1571043962462-3640749-ABCEEZGHIJ", + "change_id": "I1eee2c9d8f352483781e772f35dc586a69ff5646", + "subject": "Revert \"Implementing Feature Y\"", + "status": "NEW", + "created": "2013-02-04 09:59:33.126000000", + "updated": "2013-02-21 11:16:37.775000000", + "mergeable": true, + "insertions": 62, + "deletions": 11, + "_number": 3966, + "owner": { + "name": "Jane Doe" + } + } + ] +---- + +If any of the changes cannot be reverted because the change state doesn't +allow reverting the change, 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 is new +---- + [[submit-change]] === Submit Change -- @@ -2267,7 +2380,7 @@ A message can be specified in the request body inside a link:#private-input[PrivateInput] entity. Historically, this method allowed a body in the DELETE, but that behavior is -link:https://www.gerritcodereview.com/releases/2.16.md[deprecated]. +link:https://www.gerritcodereview.com/releases/2.16.md[deprecated,role=external,window=_blank]. In this case, use a POST request instead: .Request @@ -3253,7 +3366,7 @@ Options can be provided in the request body as a link:#delete-reviewer-input[DeleteReviewerInput] entity. Historically, this method allowed a body in the DELETE, but that behavior is -link:https://www.gerritcodereview.com/releases/2.16.md[deprecated]. +link:https://www.gerritcodereview.com/releases/2.16.md[deprecated,role=external,window=_blank]. In this case, use a POST request instead: .Request @@ -3333,7 +3446,7 @@ Options can be provided in the request body as a link:#delete-vote-input[DeleteVoteInput] entity. Historically, this method allowed a body in the DELETE, but that behavior is -link:https://www.gerritcodereview.com/releases/2.16.md[deprecated]. +link:https://www.gerritcodereview.com/releases/2.16.md[deprecated,role=external,window=_blank]. In this case, use a POST request instead: .Request @@ -3797,6 +3910,10 @@ The review must be provided in the request body as a link:#review-input[ReviewInput] entity. +If the labels are set, the user sending the request will automatically be +added as a reviewer, otherwise (if they only commented) they are added to +the CC list. + A review cannot be set on a change edit. Trying to post a review for a change edit fails with `409 Conflict`. @@ -4706,7 +4823,7 @@ Deletion reason can be provided in the request body as a link:#delete-comment-input[DeleteCommentInput] entity. Historically, this method allowed a body in the DELETE, but that behavior is -link:https://www.gerritcodereview.com/releases/2.16.md[deprecated]. +link:https://www.gerritcodereview.com/releases/2.16.md[deprecated,role=external,window=_blank]. In this case, use a POST request instead: .Request @@ -5266,6 +5383,17 @@ The `context` parameter can be specified to control the number of lines of surrounding context in the diff. Valid values are `ALL` or number of lines. +[[preview-fix]] +=== Preview fix +-- +'GET /changes/<<change-id,\{change-id\}>>/revisions/<<revision-id,\{revision-id\}>>/fixes/<<fix-id,\{fix-id\}>>/preview' +-- + +Gets the diffs of all files for a certain <<fix-id,\{fix-id\}>>. +As response, a map of link:#diff-info[DiffInfo] entities is returned that describes the diffs. + +Each link:#diff-info[DiffInfo] is the differences between the patch set indicated by revision-id and a virtual patch set with the applied fix. + [[get-blame]] === Get Blame -- @@ -5526,7 +5654,7 @@ Options can be provided in the request body as a link:#delete-vote-input[DeleteVoteInput] entity. Historically, this method allowed a body in the DELETE, but that behavior is -link:https://www.gerritcodereview.com/releases/2.16.md[deprecated]. +link:https://www.gerritcodereview.com/releases/2.16.md[deprecated,role=external,window=_blank]. In this case, use a POST request instead: .Request @@ -5926,6 +6054,12 @@ When present, change has been marked Ready at some point in time. |`revert_of` |optional| The numeric Change-Id of the change that this change reverts. +|`submission_id` |optional| +ID of the submission of this change. Only set if the status is `MERGED`. +This ID is equal to the numeric ID of the change that triggered the submission. +If the change that triggered the submission also has a topic, it will be +"<id>-<topic>" of the change that triggered the submission. +The callers must not rely on the format of the submission ID. |================================== [[change-input]] @@ -6931,10 +7065,24 @@ Additional information about whom to notify about the revert as a map of recipient type to link:#notify-info[NotifyInfo] entity. |`topic` |optional| -Name of the topic for the revert change. If not set, the default is the topic -of the change being reverted. +Name of the topic for the revert change. If not set, the default for Revert +endpoint is the topic of the change being reverted, and the default for the +RevertSubmission endpoint is `revert-{submission_id}-{timestamp.now}`. |============================= +[[revert-submission-info]] +=== RevertSubmissionInfo +The `RevertSubmissionInfo` entity describes the revert changes. + +[options="header",cols="1,6"] +|============================== +|Field Name | Description +|`revert_changes` | +A list of link:#change-info[ChangeInfo] that describes the revert changes. Each +entity in that list is a revert change that was created in that revert +submission. +|============================== + [[review-info]] === ReviewInfo The `ReviewInfo` entity contains information about a review.
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt index 021a1bb..affe1ea 100644 --- a/Documentation/rest-api-config.txt +++ b/Documentation/rest-api-config.txt
@@ -1572,6 +1572,21 @@ link:rest-api-changes.html#change-info[ChangeInfo] will never be set. |============================= +[[change-index-config-info]] +=== ChangeIndexConfigInfo +The `ChangeIndexConfigInfo` entity contains information about Gerrit +configuration from the link:config-gerrit.html#index.change[index.change] +section. + +[options="header",cols="1,^1,5"] +|============================= +|Field Name ||Description +|`index_mergeable` |not set if `false`| +Value of the link:config-gerrit.html#index.change.indexMergeable[ +configuration parameter] that controls whether the mergeability bit is +indexed (hence queryable using `is:mergeable`). +|============================= + [[check-account-external-ids-input]] === CheckAccountExternalIdsInput The `CheckAccountExternalIdsInput` entity contains input for the @@ -1821,6 +1836,21 @@ link:config-gerrit.html#gerrit.reportBugUrl[URL to report bugs]. |================================= +[[index-config-info]] +=== IndexConfigInfo +The `IndexConfigInfo` entity contains information about Gerrit +configuration from the link:config-gerrit.html#index[index] +section. + +[options="header",cols="1,^1,5"] +|============================= +|Field Name ||Description +|`change` || +Information about the configuration from the +link:config-gerrit.html#index.change[index.change] section as +link:#index.change[ChangeIndexConfigInfo] entity. +|============================= + [[hit-ration-info]] === HitRatioInfo The `HitRatioInfo` entity contains information about the hit ratio of a @@ -1947,6 +1977,10 @@ Information about the configuration from the link:config-gerrit.html#gerrit[gerrit] section as link:#gerrit-info[ GerritInfo] entity. +|`index` || +Information about the configuration from the +link:config-gerrit.html#index[index] section as link:#index[ +IndexConfigInfo] entity. |`note_db_enabled` |not set if `false`| Whether the NoteDb storage backend is fully enabled. |`plugin` ||
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt index 00fd81f..a99c3bb 100644 --- a/Documentation/rest-api-groups.txt +++ b/Documentation/rest-api-groups.txt
@@ -341,24 +341,19 @@ [[query-groups]] === Query Groups -- -'GET /groups/?query2=<query>' +'GET /groups/?query=<query>' -- Queries internal groups visible to the caller. The link:user-search-groups.html#_search_operators[query string] must be -provided by the `query2` parameter. The `start` and `limit` parameters +provided by the `query` parameter. The `start` and `limit` parameters can be used to skip/limit results. As result a list of link:#group-info[GroupInfo] entities is returned. -[NOTE] `query2` is a temporary name and in future this option may be -renamed to `query`. `query2` was chosen to maintain backwards -compatibility with the deprecated `query` parameter on the -link:#list-groups[List Groups] endpoint. - .Request ---- - GET /groups/?query2=inname:test HTTP/1.0 + GET /groups/?query=inname:test HTTP/1.0 ---- .Response @@ -398,12 +393,12 @@ [[group-query-limit]] ==== Group Limit -The `/groups/?query2=<query>` URL also accepts a limit integer in the +The `/groups/?query=<query>` URL also accepts a limit integer in the `limit` parameter. This limits the results to `limit` groups. Query the first 25 groups in group list. ---- - GET /groups/?query2=<query>&limit=25 HTTP/1.0 + GET /groups/?query=<query>&limit=25 HTTP/1.0 ---- The `/groups/` URL also accepts a start integer in the `start` @@ -411,7 +406,7 @@ Query 25 groups starting from index 50. ---- - GET /groups/?query2=<query>&limit=25&start=50 HTTP/1.0 + GET /groups/?query=<query>&limit=25&start=50 HTTP/1.0 ---- [[group-query-options]]
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt index c1349aa..6fbb338 100644 --- a/Documentation/rest-api-projects.txt +++ b/Documentation/rest-api-projects.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - /projects/ REST API This page describes the project related REST endpoints. @@ -610,7 +611,7 @@ A commit message can be provided in the request body as a link:#project-description-input[ProjectDescriptionInput] entity. Historically, this method allowed a body in the DELETE, but that behavior is -link:https://www.gerritcodereview.com/releases/2.16.md[deprecated]. +link:https://www.gerritcodereview.com/releases/2.16.md[deprecated,role=external,window=_blank]. In this case, use link:#set-project-description[PUT] instead. .Request @@ -2925,6 +2926,374 @@ HTTP/1.1 204 No Content ---- +[[label-endpoints]] +== Label Endpoints + +[[list-labels]] +=== List Labels +-- +'GET /projects/link:#project-name[\{project-name\}]/labels/' +-- + +Lists the labels that are defined in this project. + +The calling user must have read access to the `refs/meta/config` branch of the +project. + +.Request +---- + GET /projects/All-Projects/labels/ HTTP/1.0 +---- + +As result a list of link:#label-definition-info[LabelDefinitionInfo] entities +is returned that describe the labels that are defined in this project +(inherited labels are not returned unless the `inherited` parameter is set, see +link:#list-with-inherited-labels[below]). The returned labels are sorted by +label name. + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + [ + { + "name": "Code-Review", + "project": "All-Projects", + "function": "MaxWithBlock", + "values": { + " 0": "No score", + "-1": "I would prefer this is not merged as is", + "-2": "This shall not be merged", + "+1": "Looks good to me, but someone else must approve", + "+2": "Looks good to me, approved" + }, + "default_value": 0, + "can_override": true, + "copy_min_score": true, + "copy_all_scores_if_no_change": true, + "copy_all_scores_on_trivial_rebase": true, + "allow_post_submit": true + } + ] +---- + +[[list-with-inherited-labels]] +To include inherited labels from all parent projects the parameter `inherited` +can be set. + +The calling user must have read access to the `refs/meta/config` branch of the +project and all its parent projects. + +.Request +---- + GET /projects/My-Project/labels/?inherited HTTP/1.0 +---- + +As result a list of link:#label-definition-info[LabelDefinitionInfo] entities +is returned that describe the labels that are defined in this project and in +all its parent projects. The returned labels are sorted by parent projects +in-order from `All-Projects` through the project hierarchy to this project. +Labels that belong to the same project are sorted by label name. + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + [ + { + "name": "Code-Review", + "project": "All-Projects", + "function": "MaxWithBlock", + "values": { + " 0": "No score", + "-1": "I would prefer this is not merged as is", + "-2": "This shall not be merged", + "+1": "Looks good to me, but someone else must approve", + "+2": "Looks good to me, approved" + }, + "default_value": 0, + "can_override": true, + "copy_min_score": true, + "copy_all_scores_if_no_change": true, + "copy_all_scores_on_trivial_rebase": true, + "allow_post_submit": true + }, + { + "name": "Foo-Review", + "project": "My-Project", + "function": "MaxWithBlock", + "values": { + " 0": "No score", + "-1": "I would prefer this is not merged as is", + "-2": "This shall not be merged", + "+1": "Looks good to me, but someone else must approve", + "+2": "Looks good to me, approved" + }, + "default_value": 0, + "can_override": true, + "copy_any_score": true, + "allow_post_submit": true + } + ] +---- + +[[get-label]] +=== Get Label +-- +'GET /projects/link:#project-name[\{project-name\}]/labels/link:#label-name[\{label-name\}]' +-- + +Retrieves the definition of a label that is defined in this project. + +The calling user must have read access to the `refs/meta/config` branch of the +project. + +.Request +---- + GET /projects/All-Projects/labels/Code-Review HTTP/1.0 +---- + +As response a link:#label-definition-info[LabelDefinitionInfo] entity is +returned that describes the label. + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + { + "name": "Code-Review", + "project": "All-Projects", + "function": "MaxWithBlock", + "values": { + " 0": "No score", + "-1": "I would prefer this is not merged as is", + "-2": "This shall not be merged", + "+1": "Looks good to me, but someone else must approve", + "+2": "Looks good to me, approved" + }, + "default_value": 0, + "can_override": true, + "copy_min_score": true, + "copy_all_scores_if_no_change": true, + "copy_all_scores_on_trivial_rebase": true, + "allow_post_submit": true + } +---- + +[[create-label]] +=== Create Label +-- +'PUT /projects/link:#project-name[\{project-name\}]/labels/link:#label-name[\{label-name\}]' +-- + +Creates a new label definition in this project. + +The calling user must have write access to the `refs/meta/config` branch of the +project. + +If a label with this name is already defined in this project, this label +definition is updated (see link:#set-label[Set Label]). + +.Request +---- + PUT /projects/My-Project/labels/Foo HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "commit_message": "Create Foo Label", + "values": { + " 0": "No score", + "-1": "I would prefer this is not merged as is", + "-2": "This shall not be merged", + "+1": "Looks good to me, but someone else must approve", + "+2": "Looks good to me, approved" + } + } +---- + +As response a link:#label-definition-info[LabelDefinitionInfo] entity is +returned that describes the created label. + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + { + "name": "Foo", + "project_name": "My-Project", + "function": "MaxWithBlock", + "values": { + " 0": "No score", + "-1": "I would prefer this is not merged as is", + "-2": "This shall not be merged", + "+1": "Looks good to me, but someone else must approve", + "+2": "Looks good to me, approved" + }, + "default_value": 0, + "can_override": true, + "copy_all_scores_if_no_change": true, + "allow_post_submit": true + } +---- + +[[set-label]] +=== Set Label +-- +'PUT /projects/link:#project-name[\{project-name\}]/labels/link:#label-name[\{label-name\}]' +-- + +Updates the definition of a label that is defined in this project. + +The calling user must have write access to the `refs/meta/config` branch of the +project. + +Properties which are not set in the input entity are not modified. + +.Request +---- + PUT /projects/All-Projects/labels/Code-Review HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "commit_message": "Ignore self approvals for Code-Review label", + "ignore_self_approval": true + } +---- + +As response a link:#label-definition-info[LabelDefinitionInfo] entity is +returned that describes the updated label. + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + { + "name": "Code-Review", + "project": "All-Projects", + "function": "MaxWithBlock", + "values": { + " 0": "No score", + "-1": "I would prefer this is not merged as is", + "-2": "This shall not be merged", + "+1": "Looks good to me, but someone else must approve", + "+2": "Looks good to me, approved" + }, + "default_value": 0, + "can_override": true, + "copy_min_score": true, + "copy_all_scores_if_no_change": true, + "copy_all_scores_on_trivial_rebase": true, + "allow_post_submit": true, + "ignore_self_approval": true + } +---- + +[[delete-label]] +=== Delete Label +-- +'DELETE /projects/link:#project-name[\{project-name\}]/labels/link:#label-name[\{label-name\}]' +-- + +Deletes the definition of a label that is defined in this project. + +The calling user must have write access to the `refs/meta/config` branch of the +project. + +The request body does not need to include a link:#delete-label-input[ +DeleteLabelInput] entity if no commit message is specified. + +.Request +---- + DELETE /projects/My-Project/labels/Foo-Review HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "commit_message": "Delete Foo-Review label", + } +---- + +If a label was deleted the response is "`204 No Content`". + +.Response +---- + HTTP/1.1 204 No Content +---- + +[[batch-update-labels]] +=== Batch Update Labels +-- +'POST /projects/link:#project-name[\{project-name\}]/labels/' +-- + +Creates/updates/deletes multiple label definitions in this project at once. + +The calling user must have write access to the `refs/meta/config` branch of the +project. + +The updates must be specified in the request body as +link:#batch-label-input[BatchLabelInput] entity. + +The updates are processed in the following order: + +1. label deletions +2. label creations +3. label updates + +.Request +---- + POST /projects/My-Project/labels/ HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "commit_message": "Update Labels", + "delete": [ + "Old-Review", + "Unused-Review" + ], + "create": [ + { + "name": "Foo-Review", + "values": { + " 0": "No score", + "-1": "I would prefer this is not merged as is", + "-2": "This shall not be merged", + "+1": "Looks good to me, but someone else must approve", + "+2": "Looks good to me, approved" + } + ], + "update:" { + "Bar-Review": { + "function": "MaxWithBlock" + }, + "Baz-Review": { + "copy_min_score": true + } + } + } +---- + +If the label updates were done successfully the response is "`200 OK`". + +.Response +---- + HTTP/1.1 200 OK +---- + [[ids]] == IDs @@ -2949,6 +3318,10 @@ A special dashboard ID is `default` which represents the default dashboard of a project. +[[label-name]] +=== \{label-name\} +The name of a review label. + [[project-name]] === \{project-name\} The name of the project. @@ -3122,6 +3495,25 @@ |`enabled` |optional|Whether the commentlink is enabled, as documented in link:config-gerrit.html#commentlink.name.enabled[ commentlink.name.enabled]. If not set the commentlink is enabled. + +[[commentlink-input]] +=== CommentLinkInput +The `CommentLinkInput` entity describes the input for a +link:config-gerrit.html#commentlink[commentlink]. + +|================================================== +[options="header",cols="1,^2,4"] +|================================================== +|Field Name | |Description +|`match` | |A JavaScript regular expression to match +positions to be replaced with a hyperlink, as documented in +link:config-gerrit.html#commentlink.name.match[commentlink.name.match]. +|`link` | |The URL to direct the user to whenever the +regular expression is matched, as documented in +link:config-gerrit.html#commentlink.name.link[commentlink.name.link]. +|`enabled` |optional|Whether the commentlink is enabled, as documented +in link:config-gerrit.html#commentlink.name.enabled[ +commentlink.name.enabled]. If not set the commentlink is enabled. |================================================== [[config-info]] @@ -3272,6 +3664,11 @@ Whether empty commits should be rejected when a change is merged. Can be `TRUE`, `FALSE` or `INHERIT`. + If not set, this setting is not updated. +|commentlinks |optional| +Map of commentlink names to link:#commentlink-input[CommentLinkInput] +entities to add or update on the project. If the given commentlink +already exists, it will be updated with the given values, otherwise +it will be created. If the value is null, that entry is deleted. |====================================================== [[config-parameter-info]] @@ -3309,8 +3706,6 @@ |`inherited_value` |optional| The inherited value of the configuration parameter, only set if `inheritable` is true. -|`permitted_values` |optional| -The list of permitted values, only set if the `type` is `LIST`. |=============================== [[dashboard-info]] @@ -3377,6 +3772,19 @@ Tokens such as `${project}` are not resolved. |=========================== +[[delete-label-input]] +=== DeleteLabelInput +The `DeleteLabelInput` entity contains information for deleting a label +definition in a project. + +[options="header",cols="1,^2,4"] +|============================= +|Field Name ||Description +|`commit_message`|optional| +Message that should be used to commit the deletion of the label in the +`project.config` file to the `refs/meta/config` branch. +|============================= + [[delete-branches-input]] === DeleteBranchesInput The `DeleteBranchesInput` entity contains information about branches that should @@ -3459,6 +3867,128 @@ Not set if there is no parent. |================================ +[[label-definition-info]] +=== LabelDefinitionInfo +The `LabelDefinitionInfo` entity describes a link:config-labels.html[ +review label]. + +[options="header",cols="1,^2,4"] +|============================= +|Field Name ||Description +|`name` || +The link:config-labels.html#label_name[name] of the label. +|`project_name` || +The name of the project in which this label is defined. +|`function` || +The link:config-labels.html#label_function[function] of the label (can be +`MaxWithBlock`, `AnyWithBlock`, `MaxNoBlock`, `NoBlock`, `NoOp` and `PatchSetLock`. +|`values` || +The link:config-labels.html#label_value[values] of the label as a map of label +value to value description. The label values are formatted strings, e.g. "+1" +instead of "1", " 0" instead of "0". +|`default_value` || +The link:config-labels.html#label_defaultValue[default value] of the label (as +integer). +|`branches` |optional| +A list of link:config-labels.html#label_branch[branches] for which the label +applies. A branch can be a ref, a ref pattern or a regular expression. If not +set, the label applies for all branches. +|`can_override` |`false` if not set| +Whether this label can be link:config-labels.html#label_canOverride[overridden] +by child projects. +|`copy_any_score`|`false` if not set| +Whether link:config-labels.html#label_copyAnyScore[copyAnyScore] is set on the +label. +|`copy_min_score`|`false` if not set| +Whether link:config-labels.html#label_copyMinScore[copyMinScore] is set on the +label. +|`copy_max_score`|`false` if not set| +Whether link:config-labels.html#label_copyMaxScore[copyMaxScore] is set on the +label. +|`copy_all_scores_if_no_change`|`false` if not set| +Whether link:config-labels.html#label_copyAllScoresIfNoChange[ +copyAllScoresIfNoChange] is set on the label. +|`copy_all_scores_if_no_code_change`|`false` if not set| +Whether link:config-labels.html#label_copyAllScoresIfNoCodeChange[ +copyAllScoresIfNoCodeChange] is set on the label. +|`copy_all_scores_on_trivial_rebase`|`false` if not set| +Whether link:config-labels.html#label_copyAllScoresOnTrivialRebase[ +copyAllScoresOnTrivialRebase] is set on the label. +|`copy_all_scores_on_merge_first_parent_update`|`false` if not set| +Whether link:config-labels.html#label_copyAllScoresOnMergeFirstParentUpdate[ +copyAllScoresOnMergeFirstParentUpdate] is set on the label. +|`allow_post_submit`|`false` if not set| +Whether link:config-labels.html#label_allowPostSubmit[allowPostSubmit] is set +on the label. +|`ignore_self_approval`|`false` if not set| +Whether link:config-labels.html#label_ignoreSelfApproval[ignoreSelfApproval] is +set on the label. +|============================= + +[[label-definition-input]] +=== LabelDefinitionInput +The `LabelDefinitionInput` entity describes a link:config-labels.html[ +review label]. + +[options="header",cols="1,^2,4"] +|============================= +|Field Name ||Description +|`commit_message`|optional| +Message that should be used to commit the change of the label in the +`project.config` file to the `refs/meta/config` branch.+ +Must not be set if this `LabelDefinitionInput` entity is contained in a +link:#batch-label-input[BatchLabelInput] entity. +|`name` |optional| +The new link:config-labels.html#label_name[name] of the label.+ +For label creation the name is required if this `LabelDefinitionInput` entity +is contained in a link:#batch-label-input[BatchLabelInput] +entity. +|`function` |optional| +The new link:config-labels.html#label_function[function] of the label (can be +`MaxWithBlock`, `AnyWithBlock`, `MaxNoBlock`, `NoBlock`, `NoOp` and `PatchSetLock`. +|`values` |optional| +The new link:config-labels.html#label_value[values] of the label as a map of +label value to value description. The label values are formatted strings, e.g. +"+1" instead of "1", " 0" instead of "0". +|`default_value` |optional| +The new link:config-labels.html#label_defaultValue[default value] of the label +(as integer). +|`branches` |optional| +The new branches for which the label applies as a list of +link:config-labels.html#label_branch[branches]. A branch can be a ref, a ref +pattern or a regular expression. If not set, the label applies for all +branches. +|`can_override` |optional| +Whether this label can be link:config-labels.html#label_canOverride[overridden] +by child projects. +|`copy_any_score`|optional| +Whether link:config-labels.html#label_copyAnyScore[copyAnyScore] is set on the +label. +|`copy_min_score`|optional| +Whether link:config-labels.html#label_copyMinScore[copyMinScore] is set on the +label. +|`copy_max_score`|optional| +Whether link:config-labels.html#label_copyMaxScore[copyMaxScore] is set on the +label. +|`copy_all_scores_if_no_change`|optional| +Whether link:config-labels.html#label_copyAllScoresIfNoChange[ +copyAllScoresIfNoChange] is set on the label. +|`copy_all_scores_if_no_code_change`|optional| +Whether link:config-labels.html#label_copyAllScoresIfNoCodeChange[ +copyAllScoresIfNoCodeChange] is set on the label. +|`copy_all_scores_on_trivial_rebase`|optional| +Whether link:config-labels.html#label_copyAllScoresOnTrivialRebase[ +copyAllScoresOnTrivialRebase] is set on the label. +|`copy_all_scores_on_merge_first_parent_update`|optional| +Whether link:config-labels.html#label_copyAllScoresOnMergeFirstParentUpdate[ +copyAllScoresOnMergeFirstParentUpdate] is set on the label. +|`allow_post_submit`|optional| +Whether link:config-labels.html#label_allowPostSubmit[allowPostSubmit] is set +on the label. +|`ignore_self_approval`|optional| +Whether link:config-labels.html#label_ignoreSelfApproval[ignoreSelfApproval] is +set on the label. +|============================= [[label-type-info]] === LabelTypeInfo @@ -3495,6 +4025,27 @@ Not set if not inherited or overridden. |=============================== +[[batch-label-input]] +=== BatchLabelInput +The `BatchLabelInput` entity contains information for batch updating label +definitions in a project. + +[options="header",cols="1,^2,4"] +|============================= +|Field Name ||Description +|`commit_message`|optional| +Message that should be used to commit the label updates in the +`project.config` file to the `refs/meta/config` branch. +|`delete` |optional| +List of labels that should be deleted. +|`create` |optional| +List of link:#label-definition-input[LabelDefinitionInput] entities that +describe labels that should be created. +|`update` |optional| +Map of label names to link:#label-definition-input[LabelDefinitionInput] +entities that describe the updates that should be done for the labels. +|============================= + [[project-access-input]] === ProjectAccessInput The `ProjectAccessInput` describes changes that should be applied to a project
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt index a8ab353..eabcaa9 100644 --- a/Documentation/rest-api.txt +++ b/Documentation/rest-api.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - REST API Gerrit Code Review comes with a REST like API available over HTTP. @@ -128,7 +129,7 @@ === Response Codes The Gerrit REST endpoints use HTTP status codes as described in the link:http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html[ -HTTP specification]. +HTTP specification,role=external,window=_blank]. In most cases, the response body of an error response will be a plaintext, human-readable error message.
diff --git a/Documentation/user-inline-edit.txt b/Documentation/user-inline-edit.txt index 1f5e195..aa3c0a8 100644 --- a/Documentation/user-inline-edit.txt +++ b/Documentation/user-inline-edit.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Creating and Editing Changes in the Gerrit Web Interface == Overview @@ -16,7 +17,7 @@ To create a change in the Gerrit web interface: -. From the link:http://gerrit-review.googlesource.com[Gerrit Code Review] +. From the link:http://gerrit-review.googlesource.com[Gerrit Code Review,role=external,window=_blank] dashboard, select Browse > Repositories. . Under Repository Name, click the name of the repository you want to work
diff --git a/Documentation/user-notify.txt b/Documentation/user-notify.txt index 3c922ed..5346b2e 100644 --- a/Documentation/user-notify.txt +++ b/Documentation/user-notify.txt
@@ -125,6 +125,7 @@ should be sent to the emails named in this section. Within a Git-style configuration file double quotes around complex operator values may need to be escaped, e.g. `filter = branch:\"^(maint|stable)-.*\"`. +Single quotes are illegal and must be omitted. When sending email to a bare email address in a notify block, Gerrit Code Review ignores read access controls and assumes the administrator
diff --git a/Documentation/user-request-tracing.txt b/Documentation/user-request-tracing.txt index b26f4c1..1123775 100644 --- a/Documentation/user-request-tracing.txt +++ b/Documentation/user-request-tracing.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Request Tracing [[on-demand]] @@ -75,7 +76,7 @@ [[auto-retry-succeeded]] If an auto-retry succeeds you may consider filing this as link:https://bugs.chromium.org/p/gerrit/issues/entry?template=GoogleSource+Issue[ -Gerrit issue] so that the Gerrit developers can fix this and treat this +Gerrit issue,role=external,window=_blank] so that the Gerrit developers can fix this and treat this exception as recoverable. The trace IDs for auto-retries are generated and start with
diff --git a/Documentation/user-review-ui.txt b/Documentation/user-review-ui.txt index de17c00..06c5ab7 100644 --- a/Documentation/user-review-ui.txt +++ b/Documentation/user-review-ui.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Review UI Reviewing changes is an important task and the Gerrit Web UI provides @@ -432,7 +433,7 @@ The available download commands depend on the installed Gerrit plugins. The most popular plugin for download commands, the link:https://gerrit-review.googlesource.com/admin/repos/plugins/download-commands[ -download-commands] plugin, provides commands to checkout, pull and +download-commands,role=external,window=_blank] plugin, provides commands to checkout, pull and cherry-pick a patch set. Each command has a copy-to-clipboard icon that allows the command to be @@ -943,7 +944,7 @@ contain a match and one navigates to it. For additional possibilities to search please check the -link:http://www.vim.org/docs.php[Vim documentation]. There are other +link:http://www.vim.org/docs.php[Vim documentation,role=external,window=_blank]. There are other useful ways to search, e.g. while the cursor is on a word, pressing `*` or `#` searches for the next or previous occurrence of the word. @@ -962,7 +963,7 @@ - `gg` / `G` moves to cursor to the start / end of the file - `Ctrl-D` / `Ctrl-U` scrolls downwards / upwards -Please check the link:http://www.vim.org/docs.php[Vim documentation] +Please check the link:http://www.vim.org/docs.php[Vim documentation,role=external,window=_blank] for further information. [[diff-preferences]]
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt index 55a9ab7..aa5edf0 100644 --- a/Documentation/user-search.txt +++ b/Documentation/user-search.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - Searching Changes == Default Searches @@ -140,6 +141,11 @@ + Changes that revert the change specified by the numeric 'ID'. +[[submissionid]] +submissionid:'ID':: ++ +Changes that have the specified submission 'ID'. + [[reviewerin]] reviewerin:'GROUP':: + @@ -156,7 +162,7 @@ Changes occurring in 'PROJECT'. If 'PROJECT' starts with `^` it matches project names by regular expression. The link:http://www.brics.dk/automaton/[dk.brics.automaton -library] is used for evaluation of such patterns. +library,role=external,window=_blank] is used for evaluation of such patterns. [[projects]] projects:'PREFIX':: @@ -175,7 +181,7 @@ Changes occurring in 'REPOSITORY'. If 'REPOSITORY' starts with `^` it matches repository names by regular expression. The link:http://www.brics.dk/automaton/[dk.brics.automaton -library] is used for evaluation of such patterns. +library,role=external,window=_blank] is used for evaluation of such patterns. [[repositories]] repositories:'PREFIX', repos:'PREFIX':: @@ -198,7 +204,7 @@ If 'BRANCH' starts with `^` it matches branch names by regular expression patterns. The link:http://www.brics.dk/automaton/[dk.brics.automaton -library] is used for evaluation of such patterns. +library,role=external,window=_blank] is used for evaluation of such patterns. [[intopic]] intopic:'TOPIC':: @@ -208,7 +214,7 @@ If 'TOPIC' starts with `^` it matches topic names by regular expression patterns. The link:http://www.brics.dk/automaton/[dk.brics.automaton -library] is used for evaluation of such patterns. +library,role=external,window=_blank] is used for evaluation of such patterns. [[topic]] topic:'TOPIC':: @@ -233,7 +239,7 @@ If 'REF' starts with `^` it matches reference names by regular expression patterns. The link:http://www.brics.dk/automaton/[dk.brics.automaton -library] is used for evaluation of such patterns. +library,role=external,window=_blank] is used for evaluation of such patterns. [[tr,bug]] tr:'ID', bug:'ID':: @@ -268,7 +274,7 @@ Matches any change touching file at 'PATH'. By default exact path matching is used, but regular expressions can be enabled by starting with `^`. For example, to match all XML files use `file:^.*\.xml$`. -The link:http://www.brics.dk/automaton/[dk.brics.automaton library] +The link:http://www.brics.dk/automaton/[dk.brics.automaton library,role=external,window=_blank] is used for the evaluation of such patterns. + The `^` required at the beginning of the regular expression not only @@ -327,7 +333,7 @@ + If 'DIR' starts with `^` it matches directories and directory segments by regular expression. The link:http://www.brics.dk/automaton/[dk.brics.automaton -library] is used for evaluation of such patterns. +library,role=external,window=_blank] is used for evaluation of such patterns. [[footer-operator]] footer:'FOOTER'::
diff --git a/Documentation/user-signedoffby.txt b/Documentation/user-signedoffby.txt index 507f4f2..2b000f6 100644 --- a/Documentation/user-signedoffby.txt +++ b/Documentation/user-signedoffby.txt
@@ -1,7 +1,8 @@ +:linkattrs: = Gerrit Code Review - Signed-off-by Lines [NOTE] -This document was literally taken from link:http://git.kernel.org/?p=linux/kernel/git/torvalds/linux-2.6.git;a=blob;f=Documentation/SubmittingPatches;hb=4e8a2372f9255a1464ef488ed925455f53fbdaa1[linux-2.6 Documentation/SubmittingPatches] +This document was literally taken from link:http://git.kernel.org/?p=linux/kernel/git/torvalds/linux-2.6.git;a=blob;f=Documentation/SubmittingPatches;hb=4e8a2372f9255a1464ef488ed925455f53fbdaa1[linux-2.6 Documentation/SubmittingPatches,role=external,window=_blank] and is covered by the GPLv2. [[Signed-off-by]]
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt index 6cf5587..86719d2 100644 --- a/Documentation/user-upload.txt +++ b/Documentation/user-upload.txt
@@ -1,3 +1,4 @@ +:linkattrs: = Gerrit Code Review - Uploading Changes Gerrit supports three methods of uploading changes: @@ -618,7 +619,7 @@ repo is a multiple repository management tool, most commonly used by the Android Open Source Project. For more details, see -link:http://source.android.com/source/using-repo.html[using repo]. +link:http://source.android.com/source/using-repo.html[using repo,role=external,window=_blank]. [[repo_create]] === Create Changes
diff --git a/WORKSPACE b/WORKSPACE index f5cd71c..4a7add7 100644 --- a/WORKSPACE +++ b/WORKSPACE
@@ -266,6 +266,31 @@ sha1 = "1dcf1de382a0bf95a3d8b0849546c88bac1292c9", ) +CAFFEINE_VERS = "2.8.0" + +maven_jar( + name = "caffeine", + artifact = "com.github.ben-manes.caffeine:caffeine:" + CAFFEINE_VERS, + sha1 = "6000774d7f8412ced005a704188ced78beeed2bb", +) + +# TODO(davido): Rename guava.jar to caffeine-guava.jar on fetch to prevent potential +# naming collision between caffeine guava adapater and guava library itself. +# Remove this renaming procedure, once this upstream issue is fixed: +# https://github.com/ben-manes/caffeine/issues/364. +http_file( + name = "caffeine-guava-renamed", + downloaded_file_path = "caffeine-guava-" + CAFFEINE_VERS + ".jar", + sha256 = "3a66ee3ec70971dee0bae6e56bda7b8742bc4bedd7489161bfbbaaf7137d89e1", + urls = [ + "https://repo1.maven.org/maven2/com/github/ben-manes/caffeine/guava/" + + CAFFEINE_VERS + + "/guava-" + + CAFFEINE_VERS + + ".jar", + ], +) + maven_jar( name = "jsch", artifact = "com.jcraft:jsch:0.1.54", @@ -613,36 +638,36 @@ sha1 = "5e3bda828a80c7a21dfbe2308d1755759c2fd7b4", ) -OW2_VERS = "7.0" +OW2_VERS = "7.2" maven_jar( name = "ow2-asm", artifact = "org.ow2.asm:asm:" + OW2_VERS, - sha1 = "d74d4ba0dee443f68fb2dcb7fcdb945a2cd89912", + sha1 = "fa637eb67eb7628c915d73762b681ae7ff0b9731", ) maven_jar( name = "ow2-asm-analysis", artifact = "org.ow2.asm:asm-analysis:" + OW2_VERS, - sha1 = "4b310d20d6f1c6b7197a75f1b5d69f169bc8ac1f", + sha1 = "b6e6abe057f23630113f4167c34bda7086691258", ) maven_jar( name = "ow2-asm-commons", artifact = "org.ow2.asm:asm-commons:" + OW2_VERS, - sha1 = "478006d07b7c561ae3a92ddc1829bca81ae0cdd1", + sha1 = "ca2954e8d92a05bacc28ff465b25c70e0f512497", ) maven_jar( name = "ow2-asm-tree", artifact = "org.ow2.asm:asm-tree:" + OW2_VERS, - sha1 = "29bc62dcb85573af6e62e5b2d735ef65966c4180", + sha1 = "3a23cc36edaf8fc5a89cb100182758ccb5991487", ) maven_jar( name = "ow2-asm-util", artifact = "org.ow2.asm:asm-util:" + OW2_VERS, - sha1 = "18d4d07010c24405129a6dbb0e92057f8779fb9d", + sha1 = "a3ae34e57fa8a4040e28247291d0cc3d6b8c7bcf", ) AUTO_VALUE_VERSION = "1.7" @@ -894,54 +919,48 @@ sha1 = "7e060dd5b19431e6d198e91ff670644372f60fbd", ) -JETTY_VERS = "9.4.18.v20190429" +JETTY_VERS = "9.4.24.v20191120" maven_jar( name = "jetty-servlet", artifact = "org.eclipse.jetty:jetty-servlet:" + JETTY_VERS, - sha1 = "290f7a88f351950d51ebc9fb4a794752c62d7de5", + sha1 = "ca1803fde51b795c0a8346ca8bc6277d9d04d01d", ) maven_jar( name = "jetty-security", artifact = "org.eclipse.jetty:jetty-security:" + JETTY_VERS, - sha1 = "01aceff3608ca1b223bfd275a497797cfe675ef4", + sha1 = "9fa640d36c088cf55843900043d28aef830ade4d", ) maven_jar( name = "jetty-server", artifact = "org.eclipse.jetty:jetty-server:" + JETTY_VERS, - sha1 = "b76ef50e04635f11d4d43bc6ccb7c4482a8384f0", + sha1 = "7885cc3d5d7701a444acada7ab97f89846514875", ) maven_jar( name = "jetty-jmx", artifact = "org.eclipse.jetty:jetty-jmx:" + JETTY_VERS, - sha1 = "f4c2654db1a55f0780acdfcee8bb98550f56ca70", -) - -maven_jar( - name = "jetty-continuation", - artifact = "org.eclipse.jetty:jetty-continuation:" + JETTY_VERS, - sha1 = "3c421a3be5be5805e32b1a7f9c6046526524181d", + sha1 = "22be18a055850a6cf3b0efd56c789c3929c87e98", ) maven_jar( name = "jetty-http", artifact = "org.eclipse.jetty:jetty-http:" + JETTY_VERS, - sha1 = "c2e73db2db5c369326b717da71b6587b3da11e0e", + sha1 = "d3f0b0fb016ef8d35ffb199d928ffbcbfa121c86", ) maven_jar( name = "jetty-io", artifact = "org.eclipse.jetty:jetty-io:" + JETTY_VERS, - sha1 = "844af5efe58ab23fd0166a796efef123f4cb06b0", + sha1 = "dcb6d4d505ef74898e3a64a38c40195c01e97119", ) maven_jar( name = "jetty-util", artifact = "org.eclipse.jetty:jetty-util:" + JETTY_VERS, - sha1 = "13e6148bfda7ae511f69ae7e5e3ea898bc9b0e33", + sha1 = "3095acb088f4ff9e3fd9aedf98db73e3c18ea849", ) maven_jar(
diff --git a/contrib/mitm-ui/README.md b/contrib/mitm-ui/README.md deleted file mode 100644 index 1ec8dd4..0000000 --- a/contrib/mitm-ui/README.md +++ /dev/null
@@ -1,61 +0,0 @@ -# Scripts for PolyGerrit local development against prod using MitmProxy. - -## Installation (OSX) - -1. Install Docker from http://docker.com -2. Start the proxy and create a new proxied browser instance - ``` - cd ~/gerrit - ~/mitm-gerrit/mitm-serve-app-dev.sh - ``` -3. Make sure that the browser uses the proxy provided by the command line, - e.g. if you are a Googler check that the BeyondCorp extension uses the - "System/Alternative" proxy. -4. Install MITM certificates - - Open http://mitm.it in the proxied browser window - - Follow the instructions to install MITM certs - -## Usage - -### Add or replace a single plugin containing static content - -To develop unminified plugin that loads multiple files, use this. - -1. Create a new proxied browser window and start mitmproxy via Docker: - ``` - ~/mitm-gerrit/mitm-single-plugin.sh ./path/to/static/plugin.html - ``` -2. Open any *.googlesource.com domain in proxied window -3. plugin.html and ./path/to/static/* will be served - -### Add or replace a minified plugin for *.googlesource.com - -This flow assumes no additional .html/.js are needed, i.e. the plugin is a single file. - -1. Create a new proxied browser window and start mitmproxy via Docker: - ``` - ~/mitm-gerrit/mitm-plugins.sh ./path/to/plugin.html,./maybe/one/more.js - ``` -2. Open any *.googlesource.com domain in proxied window -3. plugin.html and more.js are served - -### Force or replace default site theme for *.googlesource.com - -1. Create a new proxied browser window and start mitmproxy via Docker: - ``` - ~/mitm-gerrit/mitm-theme.sh ./path/to/theme.html - ``` -2. Open any *.googlesource.com domain in proxied window -3. Default site themes are enabled. -4. Local `theme.html` content replaces `/static/gerrit-theme.html` -5. `/static/*` URLs are served from local theme directory, i.e. `./path/to/` - -### Serve uncompiled PolyGerrit - -1. Create a new proxied browser window and start mitmproxy via Docker: - ``` - cd ~/gerrit - ~/mitm-gerrit/mitm-serve-app-dev.sh - ``` -2. Open any *.googlesource.com domain in proxied window -3. Instead of prod UI (gr-app.html, gr-app.js), local source files will be served
diff --git a/contrib/mitm-ui/add-header.py b/contrib/mitm-ui/add-header.py deleted file mode 100644 index f9b2b12..0000000 --- a/contrib/mitm-ui/add-header.py +++ /dev/null
@@ -1,5 +0,0 @@ -# mitmdump -s add-header.py -def response(flow): - if flow.request.host == 'gerrit-review.googlesource.com' and flow.request.path == "/c/92000?1": - #flow.response.headers['any'] = '<meta.rdf>; rel=meta' - flow.response.headers['Link'] = '</changes/98000/detail?O=11640c>;rel="preload";crossorigin;'
diff --git a/contrib/mitm-ui/dev-chrome.sh b/contrib/mitm-ui/dev-chrome.sh deleted file mode 100755 index adcb296..0000000 --- a/contrib/mitm-ui/dev-chrome.sh +++ /dev/null
@@ -1,8 +0,0 @@ -#!/bin/sh - -if [[ "$OSTYPE" != "darwin"* ]]; then - echo Only works on OSX. - exit 1 -fi - -/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir=${HOME}/devchrome --proxy-server="127.0.0.1:8888"
diff --git a/contrib/mitm-ui/force-version.py b/contrib/mitm-ui/force-version.py deleted file mode 100644 index a69c885..0000000 --- a/contrib/mitm-ui/force-version.py +++ /dev/null
@@ -1,22 +0,0 @@ -# mitmdump -q -p 8888 -s "force-version.py --version $1" -# Request URL is not changed, only the response context -from mitmproxy import http -import argparse -import re - -class Server: - def __init__(self, version): - self.version = version - - def request(self, flow: http.HTTPFlow) -> None: - if "gr-app." in flow.request.pretty_url: - flow.request.url = re.sub( - r"polygerrit_ui/([\d.]+)/elements", - "polygerrit_ui/" + self.version + "/elements", - flow.request.url) - -def start(): - parser = argparse.ArgumentParser() - parser.add_argument("--version", type=str, help="Rapid release version, e.g. 432.0") - args = parser.parse_args() - return Server(args.version)
diff --git a/contrib/mitm-ui/mitm-docker.sh b/contrib/mitm-ui/mitm-docker.sh deleted file mode 100755 index a1206f7..0000000 --- a/contrib/mitm-ui/mitm-docker.sh +++ /dev/null
@@ -1,43 +0,0 @@ -#!/bin/sh - -extra_volume='/tmp:/tmp' - -POSITIONAL=() -while [[ $# -gt 0 ]] -do -key="$1" - -case $key in - -v|--volume) - extra_volume="$2" - shift # past argument - shift # past value - ;; - *) # unknown option - POSITIONAL+=("$1") # save it in an array for later - shift # past argument - ;; -esac -done -set -- "${POSITIONAL[@]}" # restore positional parameters - -if [[ -z "$1" ]]; then - echo This is a runner for higher-level scripts, e.g. mitm-serve-app-dev.sh - echo Alternatively, pass mitmproxy script from the same dir as a parameter, e.g. serve-app-dev.py - exit 1 -fi - -gerrit_dir=$(pwd) -mitm_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" - -CMD="${mitm_dir}/$1" - -docker run --rm -it \ - -v ~/.mitmproxy:/home/mitmproxy/.mitmproxy \ - -v ${mitm_dir}:${mitm_dir} \ - -v ${gerrit_dir}:${gerrit_dir} \ - -v ${gerrit_dir}/bazel-out:${gerrit_dir}/bazel-out \ - -v ${extra_volume} \ - -p 8888:8888 \ - mitmproxy/mitmproxy:2.0.2 \ - mitmdump -q -p 8888 -s "${CMD}"
diff --git a/contrib/mitm-ui/mitm-plugins.sh b/contrib/mitm-ui/mitm-plugins.sh deleted file mode 100755 index fc542bb..0000000 --- a/contrib/mitm-ui/mitm-plugins.sh +++ /dev/null
@@ -1,39 +0,0 @@ -#!/bin/sh - -if [[ -z "$1" ]]; then - echo This script injects plugins for *.googlesource.com. - echo Provide plugin paths, comma-separated, as a parameter. - echo This script assumes files do not have dependencies, i.e. minified. - exit 1 -fi - -realpath() { - [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" -} - -join () { - local IFS="$1" - shift - echo "$*" -} - -plugins=$1 -plugin_paths=() -for plugin in $(echo ${plugins} | sed "s/,/ /g") -do - plugin_paths+=($(realpath ${plugin})) -done - -absolute_plugin_paths=$(join , "${plugin_paths[@]}") - -mitm_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" - -${mitm_dir}/dev-chrome.sh & - -bazel build //polygerrit-ui/app:test_components & - -${mitm_dir}/mitm-docker.sh \ - "serve-app-dev.py \ - --plugins ${absolute_plugin_paths} \ - --strip_assets \ - --components $(pwd)/bazel-bin/polygerrit-ui/app/"
diff --git a/contrib/mitm-ui/mitm-serve-app-dev.sh b/contrib/mitm-ui/mitm-serve-app-dev.sh deleted file mode 100755 index d4c72cc..0000000 --- a/contrib/mitm-ui/mitm-serve-app-dev.sh +++ /dev/null
@@ -1,15 +0,0 @@ -#!/bin/sh - -workspace="./WORKSPACE" -if [[ ! -f ${workspace} ]] || [[ ! $(head -n 1 ${workspace}) == *"gerrit"* ]]; then - echo Please change to cloned Gerrit repo from https://gerrit.googlesource.com/gerrit/ - exit 1 -fi - -mitm_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" - -bazel build //polygerrit-ui/app:test_components & - -${mitm_dir}/dev-chrome.sh & - -${mitm_dir}/mitm-docker.sh "serve-app-dev.py --app $(pwd)/polygerrit-ui/app/ --components $(pwd)/bazel-bin/polygerrit-ui/app/"
diff --git a/contrib/mitm-ui/mitm-single-plugin.sh b/contrib/mitm-ui/mitm-single-plugin.sh deleted file mode 100755 index 8958229..0000000 --- a/contrib/mitm-ui/mitm-single-plugin.sh +++ /dev/null
@@ -1,38 +0,0 @@ -#!/bin/sh - -if [[ -z "$1" ]]; then - echo This script serves one plugin with the rest of static content. - echo Provide path to index plugin file, e.g. buildbucket.html for buildbucket plugin - exit 1 -fi - -realpath() { - OURPWD=$PWD - cd "$(dirname "$1")" - LINK=$(basename "$1") - while [ -L "$LINK" ]; do - LINK=$(readlink "$LINK") - cd "$(dirname "$LINK")" - LINK="$(basename "$1")" - done - REAL_DIR=`pwd -P` - RESULT=$REAL_DIR/$LINK - cd "$OURPWD" - echo "$RESULT" -} - -plugin=$(realpath $1) -plugin_root=$(dirname ${plugin}) - -mitm_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" - -${mitm_dir}/dev-chrome.sh & - -bazel build //polygerrit-ui/app:test_components & - -${mitm_dir}/mitm-docker.sh -v ${plugin_root}:${plugin_root} \ - "serve-app-dev.py \ - --plugins ${plugin} \ - --strip_assets \ - --plugin_root ${plugin_root} \ - --components $(pwd)/bazel-bin/polygerrit-ui/app/"
diff --git a/contrib/mitm-ui/mitm-theme.sh b/contrib/mitm-ui/mitm-theme.sh deleted file mode 100755 index 9290235..0000000 --- a/contrib/mitm-ui/mitm-theme.sh +++ /dev/null
@@ -1,31 +0,0 @@ -#!/bin/sh - -if [[ -z "$1" ]]; then - echo This script forces or replaces default site theme on *.googlesource.com - echo Provide path to the theme.html as a parameter. - exit 1 -fi - -realpath() { - OURPWD=$PWD - cd "$(dirname "$1")" - LINK=$(basename "$1") - while [ -L "$LINK" ]; do - LINK=$(readlink "$LINK") - cd "$(dirname "$LINK")" - LINK="$(basename "$1")" - done - REAL_DIR=`pwd -P` - RESULT=$REAL_DIR/$LINK - cd "$OURPWD" - echo "$RESULT" -} - -theme=$(realpath "$1") -theme_dir=$(dirname "${theme}") - -mitm_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" - -"${mitm_dir}"/dev-chrome.sh & - -"${mitm_dir}"/mitm-docker.sh -v "${theme_dir}":"${theme_dir}" "serve-app-dev.py --strip_assets --theme \"${theme}\""
diff --git a/contrib/mitm-ui/serve-app-dev.py b/contrib/mitm-ui/serve-app-dev.py deleted file mode 100644 index cdf7bfc..0000000 --- a/contrib/mitm-ui/serve-app-dev.py +++ /dev/null
@@ -1,169 +0,0 @@ -# 1. install and setup mitmproxy v2.0.2: https://mitmproxy.readthedocs.io/en/v2.0.2/install.html -# (In case of python versions trouble, use https://www.anaconda.com/) -# 2. mitmdump -q -s -p 8888 \ -# "serve-app-dev.py --app /path/to/polygerrit-ui/app/" -# 3. start Chrome with --proxy-server="127.0.0.1:8888" --user-data-dir=/tmp/devchrome -# 4. open, say, gerrit-review.googlesource.com. Or chromium-review.googlesource.com. Any. -# 5. uncompiled source files are served and you can log in, too. -# 6. enjoy! -# -# P.S. For replacing plugins, use --plugins or --plugin_root -# -# --plugin takes comma-separated list of plugins to add or replace. -# -# Example: Adding a new plugin to the server response: -# --plugins ~/gerrit-testsite/plugins/myplugin.html -# -# Example: Replace all matching plugins with local versions: -# --plugins ~/gerrit-testsite/plugins/ -# Following files will be served if they exist for /plugins/tricium/static/tricium.html: -# ~/gerrit-testsite/plugins/tricium.html -# ~/gerrit-testsite/plugins/tricium/static/tricium.html -# -# --assets takes assets bundle.html, expecting rest of the assets files to be in the same folder -# -# Example: -# --assets ~/gerrit-testsite/assets/a3be19f.html -# - -from mitmproxy import http -from mitmproxy.script import concurrent -import argparse -import json -import mimetypes -import os.path -import re -import zipfile - -class Server: - def __init__(self, devpath, components, plugins, pluginroot, assets, strip_assets, theme): - if devpath: - print("Serving app from " + devpath) - if components: - print("Serving components from " + components) - if pluginroot: - print("Serving plugins from " + pluginroot) - if assets: - self.assets_root, self.assets_file = os.path.split(assets) - print("Assets: using " + self.assets_file + " from " + self.assets_root) - else: - self.assets_root = None - if plugins: - self.plugins = {path.split("/")[-1:][0]: path for path in map(expandpath, plugins.split(","))} - for filename, path in self.plugins.items(): - print("Serving " + filename + " from " + path) - else: - self.plugins = {} - self.devpath = devpath - self.components = components - self.pluginroot = pluginroot - self.strip_assets = strip_assets - self.theme = theme - - def readfile(self, path): - with open(path, 'rb') as contentfile: - return contentfile.read() - -@concurrent -def response(flow: http.HTTPFlow) -> None: - if server.strip_assets: - assets_bundle = 'googlesource.com/polygerrit_assets' - assets_pos = flow.response.text.find(assets_bundle) - if assets_pos != -1: - t = flow.response.text - flow.response.text = t[:t.rfind('<', 0, assets_pos)] + t[t.find('>', assets_pos) + 1:] - return - - if server.assets_root: - marker = 'webcomponents-lite.js"></script>' - pos = flow.response.text.find(marker) - if pos != -1: - pos += len(marker) - flow.response.text = ''.join([ - flow.response.text[:pos], - '<link rel="import" href="/gerrit_assets/123.0/' + server.assets_file + '">', - flow.response.text[pos:] - ]) - - assets_prefix = "/gerrit_assets/123.0/" - if flow.request.path.startswith(assets_prefix): - assets_file = flow.request.path[len(assets_prefix):] - flow.response.content = server.readfile(server.assets_root + '/' + assets_file) - flow.response.status_code = 200 - if assets_file.endswith('.js'): - flow.response.headers['Content-type'] = 'text/javascript' - return - m = re.match(".+polygerrit_ui/\d+\.\d+/(.+)", flow.request.path) - pluginmatch = re.match("^/plugins/(.+)", flow.request.path) - localfile = "" - content = "" - if flow.request.path == "/config/server/info": - config = json.loads(flow.response.content[5:].decode('utf8')) - if server.theme: - config['default_theme'] = '/static/gerrit-theme.html' - for filename, path in server.plugins.items(): - pluginname = filename.split(".")[0] - payload = config["plugin"]["js_resource_paths" if filename.endswith(".js") else "html_resource_paths"] - if list(filter(lambda url: filename in url, payload)): - continue - payload.append("plugins/" + pluginname + "/static/" + filename) - flow.response.content = str.encode(")]}'\n" + json.dumps(config)) - if m is not None: - filepath = m.groups()[0] - if (filepath.startswith("bower_components/")): - with zipfile.ZipFile(server.components + "test_components.zip") as bower_zip: - content = bower_zip.read(filepath) - localfile = server.devpath + filepath - elif pluginmatch is not None: - pluginfile = flow.request.path_components[-1] - if server.plugins and pluginfile in server.plugins: - if os.path.isfile(server.plugins[pluginfile]): - localfile = server.plugins[pluginfile] - else: - print("Can't find file " + server.plugins[pluginfile] + " for " + flow.request.path) - elif server.pluginroot: - pluginurl = pluginmatch.groups()[0] - if os.path.isfile(server.pluginroot + pluginfile): - localfile = server.pluginroot + pluginfile - elif os.path.isfile(server.pluginroot + pluginurl): - localfile = server.pluginroot + pluginurl - - if server.theme: - if flow.request.path.endswith('/gerrit-theme.html'): - localfile = server.theme - else: - match = re.match("^/static(/[\w\.]+)$", flow.request.path) - if match is not None: - localfile = os.path.dirname(server.theme) + match.group(1) - - if localfile and os.path.isfile(localfile): - if pluginmatch is not None: - print("Serving " + flow.request.path + " from " + localfile) - content = server.readfile(localfile) - - if content: - flow.response.content = content - flow.response.status_code = 200 - localtype = mimetypes.guess_type(localfile) - if localtype and localtype[0]: - flow.response.headers['Content-type'] = localtype[0] - -def expandpath(path): - return os.path.realpath(os.path.expanduser(path)) - -parser = argparse.ArgumentParser() -parser.add_argument("--app", type=str, default="", help="Path to /polygerrit-ui/app/") -parser.add_argument("--components", type=str, default="", help="Path to test_components.zip") -parser.add_argument("--plugins", type=str, default="", help="Comma-separated list of plugin files to add/replace") -parser.add_argument("--plugin_root", type=str, default="", help="Path containing individual plugin files to replace") -parser.add_argument("--assets", type=str, default="", help="Path containing assets file to import.") -parser.add_argument("--strip_assets", action="store_true", help="Strip plugin bundles from the response.") -parser.add_argument("--theme", default="", type=str, help="Path to the default site theme to be used.") -args = parser.parse_args() -server = Server(expandpath(args.app) + '/', - expandpath(args.components) + '/', - args.plugins, - expandpath(args.plugin_root) + '/', - args.assets and expandpath(args.assets), - args.strip_assets, - expandpath(args.theme))
diff --git a/contrib/mitm-ui/serve-app-locally.py b/contrib/mitm-ui/serve-app-locally.py deleted file mode 100644 index 636c684..0000000 --- a/contrib/mitm-ui/serve-app-locally.py +++ /dev/null
@@ -1,46 +0,0 @@ -# bazel build polygerrit-ui/app:gr-app -# mitmdump -s "serve-app-locally.py ~/gerrit/bazel-bin/polygerrit-ui/app" -from mitmproxy import http -import argparse -import os -import zipfile - -class Server: - def __init__(self, bundle): - self.bundle = bundle - self.bundlemtime = 0 - self.files = { - 'polygerrit_ui/elements/gr-app.js': '', - 'polygerrit_ui/elements/gr-app.html': '', - 'polygerrit_ui/styles/main.css': '', - } - self.read_files() - - def read_files(self): - if not os.path.isfile(self.bundle): - print("bundle not found!") - return - mtime = os.stat(self.bundle).st_mtime - if mtime <= self.bundlemtime: - return - self.bundlemtime = mtime - with zipfile.ZipFile(self.bundle) as z: - for fname in self.files: - print('Reading new content for ' + fname) - with z.open(fname, 'r') as content_file: - self.files[fname] = content_file.read() - - def response(self, flow: http.HTTPFlow) -> None: - self.read_files() - for name in self.files: - if name.rsplit('/', 1)[1] in flow.request.pretty_url: - flow.response.content = self.files[name] - -def expandpath(path): - return os.path.expanduser(path) - -def start(): - parser = argparse.ArgumentParser() - parser.add_argument("bundle", type=str) - args = parser.parse_args() - return Server(expandpath(args.bundle))
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java index 7edb43a..fd61aa5 100644 --- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java +++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -644,6 +644,14 @@ return result; } + protected PushOneCommit.Result createChange(TestRepository<InMemoryRepository> repo) + throws Exception { + PushOneCommit push = pushFactory.create(admin.newIdent(), repo); + PushOneCommit.Result result = push.to("refs/for/master"); + result.assertOkStatus(); + return result; + } + protected PushOneCommit.Result createMergeCommitChange(String ref) throws Exception { return createMergeCommitChange(ref, "foo"); }
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD index 812d089..e4a993c 100644 --- a/java/com/google/gerrit/acceptance/BUILD +++ b/java/com/google/gerrit/acceptance/BUILD
@@ -79,6 +79,7 @@ ), exported_deps = [ ":function", + "//java/com/google/gerrit/acceptance/config", "//java/com/google/gerrit/acceptance/testsuite/project", "//java/com/google/gerrit/exceptions", "//java/com/google/gerrit/gpg", @@ -89,6 +90,7 @@ "//java/com/google/gerrit/pgm:daemon", "//java/com/google/gerrit/pgm/http/jetty", "//java/com/google/gerrit/pgm/util", + "//java/com/google/gerrit/server/fixes/testing", "//java/com/google/gerrit/server/group/testing", "//java/com/google/gerrit/server/project/testing:project-test-util", "//java/com/google/gerrit/testing:gerrit-test-util",
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java index 678bc31..f90df67 100644 --- a/java/com/google/gerrit/acceptance/GerritServer.java +++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -25,6 +25,11 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.gerrit.acceptance.config.ConfigAnnotationParser; +import com.google.gerrit.acceptance.config.GerritConfig; +import com.google.gerrit.acceptance.config.GerritConfigs; +import com.google.gerrit.acceptance.config.GlobalPluginConfig; +import com.google.gerrit.acceptance.config.GlobalPluginConfigs; import com.google.gerrit.acceptance.testsuite.account.AccountOperations; import com.google.gerrit.acceptance.testsuite.account.AccountOperationsImpl; import com.google.gerrit.acceptance.testsuite.group.GroupOperations; @@ -264,7 +269,7 @@ private static final ImmutableMap<String, Level> LOG_LEVELS = ImmutableMap.<String, Level>builder() - .put("com.google.gerrit", Level.DEBUG) + .put("com.google.gerrit", getGerritLogLevel()) // Silence non-critical messages from MINA SSHD. .put("org.apache.mina", Level.WARN) @@ -302,6 +307,14 @@ .put("org.eclipse.jgit.util.FS", Level.WARN) .build(); + private static Level getGerritLogLevel() { + String value = Strings.nullToEmpty(System.getenv("GERRIT_LOG_LEVEL")); + if (value.isEmpty()) { + value = Strings.nullToEmpty(System.getProperty("gerrit.logLevel")); + } + return Level.toLevel(value, Level.INFO); + } + private static boolean forceLocalDisk() { String value = Strings.nullToEmpty(System.getenv("GERRIT_FORCE_LOCAL_DISK")); if (value.isEmpty()) { @@ -558,8 +571,8 @@ cfg.setInt("sshd", null, "commandStartThreads", 1); cfg.setInt("receive", null, "threadPoolSize", 1); cfg.setInt("index", null, "threads", 1); - if (cfg.getString("index", null, "reindexAfterRefUpdate") == null) { - cfg.setBoolean("index", null, "reindexAfterRefUpdate", false); + if (cfg.getString("index", "change", "indexMergeable") == null) { + cfg.setBoolean("index", "change", "indexMergeable", false); } }
diff --git a/java/com/google/gerrit/acceptance/config/BUILD b/java/com/google/gerrit/acceptance/config/BUILD new file mode 100644 index 0000000..a8ccc1f --- /dev/null +++ b/java/com/google/gerrit/acceptance/config/BUILD
@@ -0,0 +1,15 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package(default_testonly = 1) + +java_library( + name = "config", + srcs = glob(["*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//lib:guava", + "//lib:jgit", + "//lib/auto:auto-value", + "//lib/auto:auto-value-annotations", + ], +)
diff --git a/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java b/java/com/google/gerrit/acceptance/config/ConfigAnnotationParser.java similarity index 89% rename from java/com/google/gerrit/acceptance/ConfigAnnotationParser.java rename to java/com/google/gerrit/acceptance/config/ConfigAnnotationParser.java index 0a1d765..24a2117 100644 --- a/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java +++ b/java/com/google/gerrit/acceptance/config/ConfigAnnotationParser.java
@@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.acceptance; +package com.google.gerrit.acceptance.config; import com.google.auto.value.AutoAnnotation; import com.google.common.base.Splitter; @@ -24,10 +24,10 @@ import java.util.Map; import org.eclipse.jgit.lib.Config; -class ConfigAnnotationParser { +public class ConfigAnnotationParser { private static Splitter splitter = Splitter.on(".").trimResults(); - static Config parse(Config base, GerritConfigs annotation) { + public static Config parse(Config base, GerritConfigs annotation) { if (annotation == null) { return null; } @@ -39,33 +39,13 @@ return cfg; } - static Config parse(Config base, GerritConfig annotation) { + public static Config parse(Config base, GerritConfig annotation) { Config cfg = new Config(base); parseAnnotation(cfg, annotation); return cfg; } - private static GerritConfig toGerritConfig(GlobalPluginConfig annotation) { - return newGerritConfig(annotation.name(), annotation.value(), annotation.values()); - } - - @AutoAnnotation - private static GerritConfig newGerritConfig(String name, String value, String[] values) { - return new AutoAnnotation_ConfigAnnotationParser_newGerritConfig(name, value, values); - } - - static Map<String, Config> parse(GlobalPluginConfig annotation) { - if (annotation == null) { - return null; - } - Map<String, Config> result = new HashMap<>(); - Config cfg = new Config(); - parseAnnotation(cfg, toGerritConfig(annotation)); - result.put(annotation.pluginName(), cfg); - return result; - } - - static Map<String, Config> parse(GlobalPluginConfigs annotation) { + public static Map<String, Config> parse(GlobalPluginConfigs annotation) { if (annotation == null || annotation.value().length < 1) { return null; } @@ -87,6 +67,26 @@ return result; } + public static Map<String, Config> parse(GlobalPluginConfig annotation) { + if (annotation == null) { + return null; + } + Map<String, Config> result = new HashMap<>(); + Config cfg = new Config(); + parseAnnotation(cfg, toGerritConfig(annotation)); + result.put(annotation.pluginName(), cfg); + return result; + } + + private static GerritConfig toGerritConfig(GlobalPluginConfig annotation) { + return newGerritConfig(annotation.name(), annotation.value(), annotation.values()); + } + + @AutoAnnotation + private static GerritConfig newGerritConfig(String name, String value, String[] values) { + return new AutoAnnotation_ConfigAnnotationParser_newGerritConfig(name, value, values); + } + private static void parseAnnotation(Config cfg, GerritConfig c) { ArrayList<String> l = Lists.newArrayList(splitter.split(c.name())); if (l.size() == 2) {
diff --git a/java/com/google/gerrit/acceptance/GerritConfig.java b/java/com/google/gerrit/acceptance/config/GerritConfig.java similarity index 96% rename from java/com/google/gerrit/acceptance/GerritConfig.java rename to java/com/google/gerrit/acceptance/config/GerritConfig.java index fe0c628..26be2d4 100644 --- a/java/com/google/gerrit/acceptance/GerritConfig.java +++ b/java/com/google/gerrit/acceptance/config/GerritConfig.java
@@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.acceptance; +package com.google.gerrit.acceptance.config; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME;
diff --git a/java/com/google/gerrit/acceptance/GerritConfigs.java b/java/com/google/gerrit/acceptance/config/GerritConfigs.java similarity index 94% rename from java/com/google/gerrit/acceptance/GerritConfigs.java rename to java/com/google/gerrit/acceptance/config/GerritConfigs.java index e0f9d4a..0127ace 100644 --- a/java/com/google/gerrit/acceptance/GerritConfigs.java +++ b/java/com/google/gerrit/acceptance/config/GerritConfigs.java
@@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.acceptance; +package com.google.gerrit.acceptance.config; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME;
diff --git a/java/com/google/gerrit/acceptance/GlobalPluginConfig.java b/java/com/google/gerrit/acceptance/config/GlobalPluginConfig.java similarity index 96% rename from java/com/google/gerrit/acceptance/GlobalPluginConfig.java rename to java/com/google/gerrit/acceptance/config/GlobalPluginConfig.java index 43477ae..ae88e37 100644 --- a/java/com/google/gerrit/acceptance/GlobalPluginConfig.java +++ b/java/com/google/gerrit/acceptance/config/GlobalPluginConfig.java
@@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.acceptance; +package com.google.gerrit.acceptance.config; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME;
diff --git a/java/com/google/gerrit/acceptance/GlobalPluginConfigs.java b/java/com/google/gerrit/acceptance/config/GlobalPluginConfigs.java similarity index 95% rename from java/com/google/gerrit/acceptance/GlobalPluginConfigs.java rename to java/com/google/gerrit/acceptance/config/GlobalPluginConfigs.java index dfcf955..e53e5ba 100644 --- a/java/com/google/gerrit/acceptance/GlobalPluginConfigs.java +++ b/java/com/google/gerrit/acceptance/config/GlobalPluginConfigs.java
@@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.acceptance; +package com.google.gerrit.acceptance.config; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME;
diff --git a/java/com/google/gerrit/common/data/GarbageCollectionResult.java b/java/com/google/gerrit/common/data/GarbageCollectionResult.java index 5ed0158..5e3601e 100644 --- a/java/com/google/gerrit/common/data/GarbageCollectionResult.java +++ b/java/com/google/gerrit/common/data/GarbageCollectionResult.java
@@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.List; +/** A list of errors occurred during GC. */ public class GarbageCollectionResult { protected List<Error> errors;
diff --git a/java/com/google/gerrit/common/data/GlobalCapability.java b/java/com/google/gerrit/common/data/GlobalCapability.java index fbe1deb..10a66cc 100644 --- a/java/com/google/gerrit/common/data/GlobalCapability.java +++ b/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -20,7 +20,12 @@ import java.util.Collections; import java.util.List; -/** Server wide capabilities. Represented as {@link Permission} objects. */ +/** + * Server wide capabilities. Represented as {@link Permission} objects. + * + * <p>Contrary to {@link Permission}, global capabilities do not need a resource to check + * permissions on. + */ public class GlobalCapability { /** Ability to view code review metadata refs in repositories. */ public static final String ACCESS_DATABASE = "accessDatabase";
diff --git a/java/com/google/gerrit/common/data/LabelType.java b/java/com/google/gerrit/common/data/LabelType.java index f9cd562..14b8310 100644 --- a/java/com/google/gerrit/common/data/LabelType.java +++ b/java/com/google/gerrit/common/data/LabelType.java
@@ -157,6 +157,10 @@ return name; } + public void setName(String name) { + this.name = checkName(name); + } + public boolean matches(PatchSetApproval psa) { return psa.labelId().get().equalsIgnoreCase(name); } @@ -173,6 +177,7 @@ return canOverride; } + @Nullable public List<String> getRefPatterns() { return refPatterns; } @@ -198,7 +203,7 @@ } public void setRefPatterns(List<String> refPatterns) { - if (refPatterns != null) { + if (refPatterns != null && !refPatterns.isEmpty()) { this.refPatterns = refPatterns.stream().collect(collectingAndThen(toList(), Collections::unmodifiableList)); } else { @@ -210,6 +215,10 @@ return values; } + public void setValues(List<LabelValue> values) { + this.values = sortValues(values); + } + public LabelValue getMin() { if (values.isEmpty()) { return null;
diff --git a/java/com/google/gerrit/common/data/PatchScript.java b/java/com/google/gerrit/common/data/PatchScript.java index c177e35..d8ec6a9 100644 --- a/java/com/google/gerrit/common/data/PatchScript.java +++ b/java/com/google/gerrit/common/data/PatchScript.java
@@ -14,6 +14,8 @@ package com.google.gerrit.common.data; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.gerrit.entities.Change; import com.google.gerrit.entities.Patch; import com.google.gerrit.entities.Patch.ChangeType; @@ -37,30 +39,42 @@ GITLINK } - private Change.Key changeId; - private ChangeType changeType; - private String oldName; - private String newName; - private FileMode oldMode; - private FileMode newMode; - private List<String> header; - private DiffPreferencesInfo diffPrefs; - private SparseFileContent a; - private SparseFileContent b; - private List<Edit> edits; - private Set<Edit> editsDueToRebase; - private DisplayMethod displayMethodA; - private DisplayMethod displayMethodB; - private transient String mimeTypeA; - private transient String mimeTypeB; - private CommentDetail comments; - private List<Patch> history; - private boolean hugeFile; - private boolean intralineFailure; - private boolean intralineTimeout; - private boolean binary; - private transient String commitIdA; - private transient String commitIdB; + public static class PatchScriptFileInfo { + public final String name; + public final FileMode mode; + public final SparseFileContent content; + public final DisplayMethod displayMethod; + public final String mimeType; + public final String commitId; + + PatchScriptFileInfo( + String name, + FileMode mode, + SparseFileContent content, + DisplayMethod displayMethod, + String mimeType, + String commitId) { + this.name = name; + this.mode = mode; + this.content = content; + this.displayMethod = displayMethod; + this.mimeType = mimeType; + this.commitId = commitId; + } + } + + private final Change.Key changeId; + private final ChangeType changeType; + private final ImmutableList<String> header; + private final DiffPreferencesInfo diffPrefs; + private final ImmutableList<Edit> edits; + private final ImmutableSet<Edit> editsDueToRebase; + private final ImmutableList<Patch> history; + private final boolean intralineFailure; + private final boolean intralineTimeout; + private final boolean binary; + private final PatchScriptFileInfo fileInfoA; + private final PatchScriptFileInfo fileInfoB; public PatchScript( Change.Key ck, @@ -69,19 +83,17 @@ String nn, FileMode om, FileMode nm, - List<String> h, + ImmutableList<String> h, DiffPreferencesInfo dp, SparseFileContent ca, SparseFileContent cb, - List<Edit> e, - Set<Edit> editsDueToRebase, + ImmutableList<Edit> e, + ImmutableSet<Edit> editsDueToRebase, DisplayMethod ma, DisplayMethod mb, String mta, String mtb, - CommentDetail cd, - List<Patch> hist, - boolean hf, + ImmutableList<Patch> hist, boolean idf, boolean idt, boolean bin, @@ -89,52 +101,23 @@ String cmb) { changeId = ck; changeType = ct; - oldName = on; - newName = nn; - oldMode = om; - newMode = nm; header = h; diffPrefs = dp; - a = ca; - b = cb; edits = e; this.editsDueToRebase = editsDueToRebase; - displayMethodA = ma; - displayMethodB = mb; - mimeTypeA = mta; - mimeTypeB = mtb; - comments = cd; history = hist; - hugeFile = hf; intralineFailure = idf; intralineTimeout = idt; binary = bin; - commitIdA = cma; - commitIdB = cmb; - } - protected PatchScript() {} + fileInfoA = new PatchScriptFileInfo(on, om, ca, ma, mta, cma); + fileInfoB = new PatchScriptFileInfo(nn, nm, cb, mb, mtb, cmb); + } public Change.Key getChangeId() { return changeId; } - public DisplayMethod getDisplayMethodA() { - return displayMethodA; - } - - public DisplayMethod getDisplayMethodB() { - return displayMethodB; - } - - public FileMode getFileModeA() { - return oldMode; - } - - public FileMode getFileModeB() { - return newMode; - } - public List<String> getPatchHeader() { return header; } @@ -144,15 +127,11 @@ } public String getOldName() { - return oldName; + return fileInfoA.name; } public String getNewName() { - return newName; - } - - public CommentDetail getCommentDetail() { - return comments; + return fileInfoB.name; } public List<Patch> getHistory() { @@ -163,14 +142,6 @@ return diffPrefs; } - public void setDiffPrefs(DiffPreferencesInfo dp) { - diffPrefs = dp; - } - - public boolean isHugeFile() { - return hugeFile; - } - public boolean isIgnoreWhitespace() { return diffPrefs.ignoreWhitespace != Whitespace.IGNORE_NONE; } @@ -183,24 +154,12 @@ return intralineTimeout; } - public boolean isExpandAllComments() { - return diffPrefs.expandAllComments; - } - public SparseFileContent getA() { - return a; + return fileInfoA.content; } public SparseFileContent getB() { - return b; - } - - public String getMimeTypeA() { - return mimeTypeA; - } - - public String getMimeTypeB() { - return mimeTypeB; + return fileInfoB.content; } public List<Edit> getEdits() { @@ -215,11 +174,11 @@ return binary; } - public String getCommitIdA() { - return commitIdA; + public PatchScriptFileInfo getFileInfoA() { + return fileInfoA; } - public String getCommitIdB() { - return commitIdB; + public PatchScriptFileInfo getFileInfoB() { + return fileInfoB; } }
diff --git a/java/com/google/gerrit/common/data/SubmitTypeRecord.java b/java/com/google/gerrit/common/data/SubmitTypeRecord.java index d16da96..afb3bac 100644 --- a/java/com/google/gerrit/common/data/SubmitTypeRecord.java +++ b/java/com/google/gerrit/common/data/SubmitTypeRecord.java
@@ -65,7 +65,7 @@ StringBuilder sb = new StringBuilder(); sb.append(status); if (status == Status.RULE_ERROR && errorMessage != null) { - sb.append('(').append(errorMessage).append(")"); + sb.append(" (").append(errorMessage).append(")"); } if (type != null) { sb.append('[');
diff --git a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java index 864839a..5f412ce 100644 --- a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java +++ b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -26,6 +26,7 @@ import com.google.common.collect.Lists; import com.google.common.collect.Streams; import com.google.common.flogger.FluentLogger; +import com.google.common.io.BaseEncoding; import com.google.common.io.CharStreams; import com.google.gerrit.common.Nullable; import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties; @@ -68,7 +69,6 @@ import java.util.Map; import java.util.Set; import java.util.function.Function; -import org.apache.commons.codec.binary.Base64; import org.apache.http.HttpEntity; import org.apache.http.HttpStatus; import org.apache.http.StatusLine; @@ -88,7 +88,7 @@ protected static final String SETTINGS = "settings"; protected static byte[] decodeBase64(String base64String) { - return Base64.decodeBase64(base64String); + return BaseEncoding.base64().decode(base64String); } protected static <T> List<T> decodeProtos( @@ -268,7 +268,7 @@ } else if (type == FieldType.TIMESTAMP) { rawFields.put(element.getKey(), new Timestamp(inner.getAsLong())); } else if (type == FieldType.STORED_ONLY) { - rawFields.put(element.getKey(), Base64.decodeBase64(inner.getAsString())); + rawFields.put(element.getKey(), decodeBase64(inner.getAsString())); } else { throw FieldType.badFieldType(type); }
diff --git a/java/com/google/gerrit/elasticsearch/BUILD b/java/com/google/gerrit/elasticsearch/BUILD index edbd82c..8bab80b 100644 --- a/java/com/google/gerrit/elasticsearch/BUILD +++ b/java/com/google/gerrit/elasticsearch/BUILD
@@ -19,7 +19,6 @@ "//lib:guava", "//lib:jgit", "//lib:protobuf", - "//lib/commons:codec", "//lib/commons:lang", "//lib/elasticsearch-rest-client", "//lib/flogger:api",
diff --git a/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java b/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java index a06f90f..c3e3264 100644 --- a/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java +++ b/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
@@ -14,6 +14,7 @@ package com.google.gerrit.elasticsearch; +import com.google.common.collect.ImmutableSet; import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties; import com.google.gerrit.elasticsearch.bulk.BulkRequest; import com.google.gerrit.elasticsearch.bulk.IndexRequest; @@ -74,7 +75,7 @@ public void replace(AccountState as) { BulkRequest bulk = new IndexRequest(getId(as), indexName, type, client.adapter()) - .add(new UpdateRequest<>(schema, as)); + .add(new UpdateRequest<>(schema, as, ImmutableSet.of())); String uri = getURI(type, BULK); Response response = postRequest(uri, bulk, getRefreshParam());
diff --git a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java index 37184cc..084c2ec 100644 --- a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java +++ b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -22,6 +22,7 @@ import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; @@ -48,6 +49,7 @@ import com.google.gerrit.server.ReviewerByEmailSet; import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.StarredChangesUtil; +import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.index.IndexUtils; import com.google.gerrit.server.index.change.ChangeField; @@ -65,6 +67,7 @@ import java.util.Optional; import java.util.Set; import org.apache.http.HttpStatus; +import org.eclipse.jgit.lib.Config; import org.elasticsearch.client.Response; /** Secondary index implementation using Elasticsearch. */ @@ -91,6 +94,7 @@ private final ChangeData.Factory changeDataFactory; private final Schema<ChangeData> schema; private final FieldDef<ChangeData, ?> idField; + private final ImmutableSet<String> skipFields; @Inject ElasticChangeIndex( @@ -98,6 +102,7 @@ ChangeData.Factory changeDataFactory, SitePaths sitePaths, ElasticRestClientProvider clientBuilder, + @GerritServerConfig Config gerritConfig, @Assisted Schema<ChangeData> schema) { super(cfg, sitePaths, schema, clientBuilder, CHANGES); this.changeDataFactory = changeDataFactory; @@ -105,6 +110,10 @@ this.mapping = new ChangeMapping(schema, client.adapter()); this.idField = this.schema.useLegacyNumericFields() ? ChangeField.LEGACY_ID : ChangeField.LEGACY_ID_STR; + this.skipFields = + gerritConfig.getBoolean("index", "change", "indexMergeable", true) + ? ImmutableSet.of() + : ImmutableSet.of(ChangeField.MERGEABLE.getName()); } @Override @@ -123,7 +132,7 @@ ElasticQueryAdapter adapter = client.adapter(); BulkRequest bulk = new IndexRequest(getId(cd), indexName, adapter.getType(insertIndex), adapter) - .add(new UpdateRequest<>(schema, cd)); + .add(new UpdateRequest<>(schema, cd, skipFields)); if (adapter.deleteToReplace()) { bulk.add(new DeleteRequest(cd.getId().toString(), indexName, deleteIndex, adapter)); } @@ -263,7 +272,7 @@ // Mergeable. JsonElement mergeableElement = source.get(ChangeField.MERGEABLE.getName()); - if (mergeableElement != null) { + if (mergeableElement != null && !skipFields.contains(ChangeField.MERGEABLE.getName())) { String mergeable = mergeableElement.getAsString(); if ("1".equals(mergeable)) { cd.setMergeable(true);
diff --git a/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java b/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java index c215132..ce2025f 100644 --- a/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java +++ b/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
@@ -14,6 +14,7 @@ package com.google.gerrit.elasticsearch; +import com.google.common.collect.ImmutableSet; import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties; import com.google.gerrit.elasticsearch.bulk.BulkRequest; import com.google.gerrit.elasticsearch.bulk.IndexRequest; @@ -74,7 +75,7 @@ public void replace(InternalGroup group) { BulkRequest bulk = new IndexRequest(getId(group), indexName, type, client.adapter()) - .add(new UpdateRequest<>(schema, group)); + .add(new UpdateRequest<>(schema, group, ImmutableSet.of())); String uri = getURI(type, BULK); Response response = postRequest(uri, bulk, getRefreshParam());
diff --git a/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java b/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java index 29f8507..b636706 100644 --- a/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java +++ b/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
@@ -14,6 +14,7 @@ package com.google.gerrit.elasticsearch; +import com.google.common.collect.ImmutableSet; import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties; import com.google.gerrit.elasticsearch.bulk.BulkRequest; import com.google.gerrit.elasticsearch.bulk.IndexRequest; @@ -74,7 +75,7 @@ public void replace(ProjectData projectState) { BulkRequest bulk = new IndexRequest(projectState.getProject().getName(), indexName, type, client.adapter()) - .add(new UpdateRequest<>(schema, projectState)); + .add(new UpdateRequest<>(schema, projectState, ImmutableSet.of())); String uri = getURI(type, BULK); Response response = postRequest(uri, bulk, getRefreshParam());
diff --git a/java/com/google/gerrit/elasticsearch/bulk/UpdateRequest.java b/java/com/google/gerrit/elasticsearch/bulk/UpdateRequest.java index 2f0bd01..196b8d6 100644 --- a/java/com/google/gerrit/elasticsearch/bulk/UpdateRequest.java +++ b/java/com/google/gerrit/elasticsearch/bulk/UpdateRequest.java
@@ -16,6 +16,7 @@ import static java.util.stream.Collectors.toList; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Streams; import com.google.gerrit.elasticsearch.builders.XContentBuilder; @@ -27,17 +28,19 @@ private final Schema<V> schema; private final V v; + private final ImmutableSet<String> skipFields; - public UpdateRequest(Schema<V> schema, V v) { + public UpdateRequest(Schema<V> schema, V v, ImmutableSet<String> skipFields) { this.schema = schema; this.v = v; + this.skipFields = skipFields; } @Override protected String getRequest() { try (XContentBuilder closeable = new XContentBuilder()) { XContentBuilder builder = closeable.startObject(); - for (Values<V> values : schema.buildFields(v)) { + for (Values<V> values : schema.buildFields(v, skipFields)) { String name = values.getField().getName(); if (values.getField().isRepeatable()) { builder.field(name, Streams.stream(values.getValues()).collect(toList()));
diff --git a/java/com/google/gerrit/entities/Change.java b/java/com/google/gerrit/entities/Change.java index 739bd38..04e97dc 100644 --- a/java/com/google/gerrit/entities/Change.java +++ b/java/com/google/gerrit/entities/Change.java
@@ -26,6 +26,7 @@ import java.security.SecureRandom; import java.sql.Timestamp; import java.util.Arrays; +import java.util.Optional; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; @@ -116,13 +117,30 @@ @AutoValue public abstract static class Id { - /** Parse a Change.Id out of a string representation. */ + /** + * Parse a Change.Id out of a string representation. + * + * @deprecated use {@link #tryParse(String)} instead. + */ + @Deprecated public static Id parse(String str) { Integer id = Ints.tryParse(str); checkArgument(id != null, "invalid change ID: %s", str); return Change.id(id); } + /** + * Parse a Change.Id out of a string representation. + * + * @param str the string to parse + * @return Optional containing the Change.Id, or {@code Optional.empty()} if str does not + * represent a valid Change.Id. + */ + public static Optional<Id> tryParse(String str) { + Integer id = Ints.tryParse(str); + return id != null ? Optional.of(Change.id(id)) : Optional.empty(); + } + public static Id fromRef(String ref) { if (RefNames.isRefsEdit(ref)) { return fromEditRefPart(ref);
diff --git a/java/com/google/gerrit/entities/SubmissionId.java b/java/com/google/gerrit/entities/SubmissionId.java new file mode 100644 index 0000000..eb03a5a --- /dev/null +++ b/java/com/google/gerrit/entities/SubmissionId.java
@@ -0,0 +1,34 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.entities; + +import org.eclipse.jgit.annotations.Nullable; + +public class SubmissionId { + private final String submissionId; + + public SubmissionId(Change.Id changeId, @Nullable String topic) { + submissionId = topic != null ? String.format("%s-%s", changeId, topic) : changeId.toString(); + } + + public SubmissionId(Change change) { + this(change.getId(), change.getTopic()); + } + + @Override + public String toString() { + return submissionId; + } +}
diff --git a/java/com/google/gerrit/exceptions/InvalidMergeStrategyException.java b/java/com/google/gerrit/exceptions/InvalidMergeStrategyException.java new file mode 100644 index 0000000..d9c5776 --- /dev/null +++ b/java/com/google/gerrit/exceptions/InvalidMergeStrategyException.java
@@ -0,0 +1,23 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.exceptions; + +public class InvalidMergeStrategyException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public InvalidMergeStrategyException(String strategy) { + super("invalid merge strategy: " + strategy); + } +}
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java index 26a1a27..119d941 100644 --- a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java +++ b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -27,6 +27,7 @@ import com.google.gerrit.extensions.common.EditInfo; import com.google.gerrit.extensions.common.MergePatchSetInput; import com.google.gerrit.extensions.common.PureRevertInfo; +import com.google.gerrit.extensions.common.RevertSubmissionInfo; import com.google.gerrit.extensions.common.RobotCommentInfo; import com.google.gerrit.extensions.common.SuggestedReviewerInfo; import com.google.gerrit.extensions.restapi.NotImplementedException; @@ -161,6 +162,12 @@ */ ChangeApi revert(RevertInput in) throws RestApiException; + default RevertSubmissionInfo revertSubmission() throws RestApiException { + return revertSubmission(new RevertInput()); + } + + RevertSubmissionInfo revertSubmission(RevertInput in) throws RestApiException; + /** Create a merge patch set for the change. */ ChangeInfo createMergePatchSet(MergePatchSetInput in) throws RestApiException; @@ -502,6 +509,11 @@ } @Override + public RevertSubmissionInfo revertSubmission(RevertInput in) throws RestApiException { + throw new NotImplementedException(); + } + + @Override public void rebase(RebaseInput in) throws RestApiException { throw new NotImplementedException(); }
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java index 25eb7a8..6bd4b73 100644 --- a/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java +++ b/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java
@@ -150,7 +150,22 @@ * @param newContent the desired content of the file * @throws RestApiException if the content of the file couldn't be modified */ - void modifyFile(String filePath, RawInput newContent) throws RestApiException; + default void modifyFile(String filePath, RawInput newContent) throws RestApiException { + FileContentInput input = new FileContentInput(); + input.content = newContent; + modifyFile(filePath, input); + } + + /** + * Modify the contents of the specified file of the change edit. If no content is provided, the + * content of the file is erased but the file isn't deleted. If the change edit doesn't exist, it + * will be created based on the current patch set of the change. + * + * @param filePath the path of the file which should be modified + * @param input the desired content of the file + * @throws RestApiException if the content of the file couldn't be modified + */ + void modifyFile(String filePath, FileContentInput input) throws RestApiException; /** * Deletes the specified file from the change edit. If the change edit doesn't exist, it will be @@ -235,7 +250,7 @@ } @Override - public void modifyFile(String filePath, RawInput newContent) throws RestApiException { + public void modifyFile(String filePath, FileContentInput input) throws RestApiException { throw new NotImplementedException(); }
diff --git a/java/com/google/gerrit/extensions/api/changes/FileContentInput.java b/java/com/google/gerrit/extensions/api/changes/FileContentInput.java new file mode 100644 index 0000000..93c253d --- /dev/null +++ b/java/com/google/gerrit/extensions/api/changes/FileContentInput.java
@@ -0,0 +1,23 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.api.changes; + +import com.google.gerrit.extensions.restapi.DefaultInput; +import com.google.gerrit.extensions.restapi.RawInput; + +/** Content to be added to a file (new or existing) via change edit. */ +public class FileContentInput { + @DefaultInput public RawInput content; +}
diff --git a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java index f8404ce..7d4f555 100644 --- a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java +++ b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -22,6 +22,7 @@ import com.google.gerrit.extensions.common.CherryPickChangeInfo; import com.google.gerrit.extensions.common.CommentInfo; import com.google.gerrit.extensions.common.CommitInfo; +import com.google.gerrit.extensions.common.DiffInfo; import com.google.gerrit.extensions.common.EditInfo; import com.google.gerrit.extensions.common.FileInfo; import com.google.gerrit.extensions.common.MergeableInfo; @@ -125,6 +126,8 @@ */ EditInfo applyFix(String fixId) throws RestApiException; + Map<String, DiffInfo> getFixPreview(String fixId) throws RestApiException; + DraftApi createDraft(DraftInput in) throws RestApiException; DraftApi draft(String id) throws RestApiException; @@ -296,6 +299,11 @@ } @Override + public Map<String, DiffInfo> getFixPreview(String fixId) throws RestApiException { + throw new NotImplementedException(); + } + + @Override public Map<String, List<CommentInfo>> drafts() throws RestApiException { throw new NotImplementedException(); }
diff --git a/java/com/google/gerrit/extensions/api/projects/BanCommitInput.java b/java/com/google/gerrit/extensions/api/projects/BanCommitInput.java index b0f674f..b24eca0 100644 --- a/java/com/google/gerrit/extensions/api/projects/BanCommitInput.java +++ b/java/com/google/gerrit/extensions/api/projects/BanCommitInput.java
@@ -17,6 +17,7 @@ import com.google.common.collect.Lists; import java.util.List; +/** Commits that will forbidden to be uploaded. */ public class BanCommitInput { public List<String> commits; public String reason;
diff --git a/java/com/google/gerrit/extensions/api/projects/CommentLinkInput.java b/java/com/google/gerrit/extensions/api/projects/CommentLinkInput.java new file mode 100644 index 0000000..3aad7e1 --- /dev/null +++ b/java/com/google/gerrit/extensions/api/projects/CommentLinkInput.java
@@ -0,0 +1,27 @@ +// Copyright (C) 2018 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.api.projects; + +/* + * Input for a commentlink configuration on a project. + */ +public class CommentLinkInput { + /** A JavaScript regular expression to match positions to be replaced with a hyperlink. */ + public String match; + /** The URL to direct the user to whenever the regular expression is matched. */ + public String link; + /** Whether the commentlink is enabled. */ + public Boolean enabled; +}
diff --git a/java/com/google/gerrit/extensions/api/projects/ConfigInput.java b/java/com/google/gerrit/extensions/api/projects/ConfigInput.java index 1a6d77b..8005fc5 100644 --- a/java/com/google/gerrit/extensions/api/projects/ConfigInput.java +++ b/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
@@ -38,4 +38,5 @@ public SubmitType submitType; public ProjectState state; public Map<String, Map<String, ConfigValue>> pluginConfigValues; + public Map<String, CommentLinkInput> commentLinks; }
diff --git a/java/com/google/gerrit/extensions/api/projects/LabelApi.java b/java/com/google/gerrit/extensions/api/projects/LabelApi.java new file mode 100644 index 0000000..975a57e --- /dev/null +++ b/java/com/google/gerrit/extensions/api/projects/LabelApi.java
@@ -0,0 +1,61 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.api.projects; + +import com.google.gerrit.common.Nullable; +import com.google.gerrit.extensions.common.LabelDefinitionInfo; +import com.google.gerrit.extensions.common.LabelDefinitionInput; +import com.google.gerrit.extensions.restapi.NotImplementedException; +import com.google.gerrit.extensions.restapi.RestApiException; + +public interface LabelApi { + LabelApi create(LabelDefinitionInput input) throws RestApiException; + + LabelDefinitionInfo get() throws RestApiException; + + LabelDefinitionInfo update(LabelDefinitionInput input) throws RestApiException; + + default void delete() throws RestApiException { + delete(null); + } + + void delete(@Nullable String commitMessage) throws RestApiException; + + /** + * A default implementation which allows source compatibility when adding new methods to the + * interface. + */ + class NotImplemented implements LabelApi { + @Override + public LabelApi create(LabelDefinitionInput input) throws RestApiException { + throw new NotImplementedException(); + } + + @Override + public LabelDefinitionInfo get() throws RestApiException { + throw new NotImplementedException(); + } + + @Override + public LabelDefinitionInfo update(LabelDefinitionInput input) throws RestApiException { + throw new NotImplementedException(); + } + + @Override + public void delete(@Nullable String commitMessage) throws RestApiException { + throw new NotImplementedException(); + } + } +}
diff --git a/java/com/google/gerrit/extensions/api/projects/ParentInput.java b/java/com/google/gerrit/extensions/api/projects/ParentInput.java index 6e481ae..d68bb3b 100644 --- a/java/com/google/gerrit/extensions/api/projects/ParentInput.java +++ b/java/com/google/gerrit/extensions/api/projects/ParentInput.java
@@ -14,9 +14,9 @@ package com.google.gerrit.extensions.api.projects; +import com.google.gerrit.extensions.common.InputWithCommitMessage; import com.google.gerrit.extensions.restapi.DefaultInput; -public class ParentInput { +public class ParentInput extends InputWithCommitMessage { @DefaultInput public String parent; - public String commitMessage; }
diff --git a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java index c6d9dee..9873995 100644 --- a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java +++ b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -18,7 +18,9 @@ import com.google.gerrit.extensions.api.access.ProjectAccessInput; import com.google.gerrit.extensions.api.config.AccessCheckInfo; import com.google.gerrit.extensions.api.config.AccessCheckInput; +import com.google.gerrit.extensions.common.BatchLabelInput; import com.google.gerrit.extensions.common.ChangeInfo; +import com.google.gerrit.extensions.common.LabelDefinitionInfo; import com.google.gerrit.extensions.common.ProjectInfo; import com.google.gerrit.extensions.restapi.NotImplementedException; import com.google.gerrit.extensions.restapi.RestApiException; @@ -204,6 +206,28 @@ /** Reindexes all changes of the project. */ void indexChanges() throws RestApiException; + ListLabelsRequest labels() throws RestApiException; + + abstract class ListLabelsRequest { + protected boolean inherited; + + public abstract List<LabelDefinitionInfo> get() throws RestApiException; + + public ListLabelsRequest withInherited(boolean inherited) { + this.inherited = inherited; + return this; + } + } + + LabelApi label(String labelName) throws RestApiException; + + /** + * Adds, updates and deletes label definitions in a batch. + * + * @param input input that describes additions, updates and deletions of label definitions + */ + void labels(BatchLabelInput input) throws RestApiException; + /** * A default implementation which allows source compatibility when adding new methods to the * interface. @@ -378,5 +402,20 @@ public void indexChanges() throws RestApiException { throw new NotImplementedException(); } + + @Override + public ListLabelsRequest labels() throws RestApiException { + throw new NotImplementedException(); + } + + @Override + public LabelApi label(String labelName) throws RestApiException { + throw new NotImplementedException(); + } + + @Override + public void labels(BatchLabelInput input) throws RestApiException { + throw new NotImplementedException(); + } } }
diff --git a/java/com/google/gerrit/extensions/api/projects/SetDashboardInput.java b/java/com/google/gerrit/extensions/api/projects/SetDashboardInput.java index 0083c0e..3662b7f 100644 --- a/java/com/google/gerrit/extensions/api/projects/SetDashboardInput.java +++ b/java/com/google/gerrit/extensions/api/projects/SetDashboardInput.java
@@ -14,9 +14,9 @@ package com.google.gerrit.extensions.api.projects; +import com.google.gerrit.extensions.common.InputWithCommitMessage; import com.google.gerrit.extensions.restapi.DefaultInput; -public class SetDashboardInput { +public class SetDashboardInput extends InputWithCommitMessage { @DefaultInput public String id; - public String commitMessage; }
diff --git a/java/com/google/gerrit/extensions/common/BatchLabelInput.java b/java/com/google/gerrit/extensions/common/BatchLabelInput.java new file mode 100644 index 0000000..eb4c581 --- /dev/null +++ b/java/com/google/gerrit/extensions/common/BatchLabelInput.java
@@ -0,0 +1,26 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +import java.util.List; +import java.util.Map; + +/** Input for the REST API that describes additions, updates and deletions of label definitions. */ +public class BatchLabelInput { + public String commitMessage; + public List<String> delete; + public List<LabelDefinitionInput> create; + public Map<String, LabelDefinitionInput> update; +}
diff --git a/java/com/google/gerrit/extensions/common/ChangeIndexConfigInfo.java b/java/com/google/gerrit/extensions/common/ChangeIndexConfigInfo.java new file mode 100644 index 0000000..7bca79e --- /dev/null +++ b/java/com/google/gerrit/extensions/common/ChangeIndexConfigInfo.java
@@ -0,0 +1,19 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +public class ChangeIndexConfigInfo { + public Boolean indexMergeable; +}
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfo.java b/java/com/google/gerrit/extensions/common/ChangeInfo.java index 9a739ef..0d7c5c7 100644 --- a/java/com/google/gerrit/extensions/common/ChangeInfo.java +++ b/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -52,6 +52,7 @@ public Boolean workInProgress; public Boolean hasReviewStarted; public Integer revertOf; + public String submissionId; public int _number;
diff --git a/java/com/google/gerrit/extensions/common/IndexConfigInfo.java b/java/com/google/gerrit/extensions/common/IndexConfigInfo.java new file mode 100644 index 0000000..084c53a --- /dev/null +++ b/java/com/google/gerrit/extensions/common/IndexConfigInfo.java
@@ -0,0 +1,19 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +public class IndexConfigInfo { + public ChangeIndexConfigInfo change; +}
diff --git a/java/com/google/gerrit/extensions/common/InputWithCommitMessage.java b/java/com/google/gerrit/extensions/common/InputWithCommitMessage.java new file mode 100644 index 0000000..34bc203 --- /dev/null +++ b/java/com/google/gerrit/extensions/common/InputWithCommitMessage.java
@@ -0,0 +1,30 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +import com.google.gerrit.common.Nullable; + +/** A generic input with a commit message only. */ +public class InputWithCommitMessage { + @Nullable public String commitMessage; + + public InputWithCommitMessage() { + this(null); + } + + public InputWithCommitMessage(@Nullable String commitMessage) { + this.commitMessage = commitMessage; + } +}
diff --git a/java/com/google/gerrit/extensions/common/InputWithMessage.java b/java/com/google/gerrit/extensions/common/InputWithMessage.java new file mode 100644 index 0000000..45d23cf --- /dev/null +++ b/java/com/google/gerrit/extensions/common/InputWithMessage.java
@@ -0,0 +1,34 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +import com.google.gerrit.common.Nullable; + +/** + * A generic input with a message only. + * + * <p>See also {@link InputWithCommitMessage}. + */ +public class InputWithMessage { + @Nullable public String message; + + public InputWithMessage() { + this(null); + } + + public InputWithMessage(@Nullable String message) { + this.message = message; + } +}
diff --git a/java/com/google/gerrit/extensions/common/LabelDefinitionInfo.java b/java/com/google/gerrit/extensions/common/LabelDefinitionInfo.java new file mode 100644 index 0000000..64c3997 --- /dev/null +++ b/java/com/google/gerrit/extensions/common/LabelDefinitionInfo.java
@@ -0,0 +1,37 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +import java.util.List; +import java.util.Map; + +public class LabelDefinitionInfo { + public String name; + public String projectName; + public String function; + public Map<String, String> values; + public short defaultValue; + public List<String> branches; + public Boolean canOverride; + public Boolean copyAnyScore; + public Boolean copyMinScore; + public Boolean copyMaxScore; + public Boolean copyAllScoresIfNoChange; + public Boolean copyAllScoresIfNoCodeChange; + public Boolean copyAllScoresOnTrivialRebase; + public Boolean copyAllScoresOnMergeFirstParentUpdate; + public Boolean allowPostSubmit; + public Boolean ignoreSelfApproval; +}
diff --git a/java/com/google/gerrit/extensions/common/LabelDefinitionInput.java b/java/com/google/gerrit/extensions/common/LabelDefinitionInput.java new file mode 100644 index 0000000..0523f61 --- /dev/null +++ b/java/com/google/gerrit/extensions/common/LabelDefinitionInput.java
@@ -0,0 +1,36 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +import java.util.List; +import java.util.Map; + +public class LabelDefinitionInput extends InputWithCommitMessage { + public String name; + public String function; + public Map<String, String> values; + public Short defaultValue; + public List<String> branches; + public Boolean canOverride; + public Boolean copyAnyScore; + public Boolean copyMinScore; + public Boolean copyMaxScore; + public Boolean copyAllScoresIfNoChange; + public Boolean copyAllScoresIfNoCodeChange; + public Boolean copyAllScoresOnTrivialRebase; + public Boolean copyAllScoresOnMergeFirstParentUpdate; + public Boolean allowPostSubmit; + public Boolean ignoreSelfApproval; +}
diff --git a/java/com/google/gerrit/extensions/common/RevertSubmissionInfo.java b/java/com/google/gerrit/extensions/common/RevertSubmissionInfo.java new file mode 100644 index 0000000..dabd035 --- /dev/null +++ b/java/com/google/gerrit/extensions/common/RevertSubmissionInfo.java
@@ -0,0 +1,21 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +import java.util.List; + +public class RevertSubmissionInfo { + public List<ChangeInfo> revertChanges; +}
diff --git a/java/com/google/gerrit/extensions/common/ServerInfo.java b/java/com/google/gerrit/extensions/common/ServerInfo.java index 82d5bc8..9cf1ec1 100644 --- a/java/com/google/gerrit/extensions/common/ServerInfo.java +++ b/java/com/google/gerrit/extensions/common/ServerInfo.java
@@ -20,6 +20,7 @@ public ChangeConfigInfo change; public DownloadInfo download; public GerritInfo gerrit; + public IndexConfigInfo index; public Boolean noteDbEnabled; public PluginConfigInfo plugin; public SshdInfo sshd;
diff --git a/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java index 8853a30..e258134 100644 --- a/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java +++ b/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
@@ -18,12 +18,15 @@ import static com.google.gerrit.extensions.common.testing.FileMetaSubject.fileMetas; import static com.google.gerrit.truth.ListSubject.elements; +import com.google.common.truth.BooleanSubject; import com.google.common.truth.ComparableSubject; import com.google.common.truth.FailureMetadata; +import com.google.common.truth.IterableSubject; import com.google.common.truth.Subject; import com.google.gerrit.extensions.common.ChangeType; import com.google.gerrit.extensions.common.DiffInfo; import com.google.gerrit.extensions.common.DiffInfo.ContentEntry; +import com.google.gerrit.extensions.common.DiffInfo.IntraLineStatus; import com.google.gerrit.truth.ListSubject; public class DiffInfoSubject extends Subject { @@ -60,4 +63,24 @@ isNotNull(); return check("metaB").about(fileMetas()).that(diffInfo.metaB); } + + public ComparableSubject<IntraLineStatus> intralineStatus() { + isNotNull(); + return check("intralineStatus").that(diffInfo.intralineStatus); + } + + public IterableSubject webLinks() { + isNotNull(); + return check("webLinks").that(diffInfo.webLinks); + } + + public BooleanSubject binary() { + isNotNull(); + return check("binary").that(diffInfo.binary); + } + + public IterableSubject diffHeader() { + isNotNull(); + return check("diffHeader").that(diffInfo.diffHeader); + } }
diff --git a/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java b/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java index fb09a1f..0953bfe 100644 --- a/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java +++ b/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java
@@ -18,6 +18,8 @@ import com.google.common.truth.FailureMetadata; import com.google.common.truth.IntegerSubject; +import com.google.common.truth.IterableSubject; +import com.google.common.truth.StringSubject; import com.google.common.truth.Subject; import com.google.gerrit.extensions.common.DiffInfo.FileMeta; @@ -42,4 +44,24 @@ isNotNull(); return check("totalLineCount()").that(fileMeta.lines); } + + public StringSubject name() { + isNotNull(); + return check("name").that(fileMeta.name); + } + + public StringSubject commitId() { + isNotNull(); + return check("commitId").that(fileMeta.commitId); + } + + public StringSubject contentType() { + isNotNull(); + return check("contentType").that(fileMeta.contentType); + } + + public IterableSubject webLinks() { + isNotNull(); + return check("webLinks").that(fileMeta.webLinks); + } }
diff --git a/java/com/google/gerrit/extensions/restapi/Response.java b/java/com/google/gerrit/extensions/restapi/Response.java index 5504cfd..bf363d8 100644 --- a/java/com/google/gerrit/extensions/restapi/Response.java +++ b/java/com/google/gerrit/extensions/restapi/Response.java
@@ -14,10 +14,6 @@ package com.google.gerrit.extensions.restapi; -import static com.google.common.base.Preconditions.checkArgument; - -import com.google.gerrit.common.Nullable; -import java.util.Optional; import java.util.concurrent.TimeUnit; /** Special return value to mean specific HTTP status codes in a REST API. */ @@ -30,6 +26,12 @@ return new Impl<>(200, value); } + /** HTTP 200 OK: with empty value. */ + public static Response<String> ok() { + return ok(""); + } + + /** HTTP 200 OK: with forced revalidation of cache. */ public static <T> Response<T> withMustRevalidate(T value) { return ok(value).caching(CacheControl.PRIVATE(0, TimeUnit.SECONDS).setMustRevalidate()); } @@ -39,6 +41,11 @@ return new Impl<>(201, value); } + /** HTTP 201 Created: with empty value. */ + public static Response<String> created() { + return created(""); + } + /** HTTP 202 Accepted: accepted as background task. */ public static Accepted accepted(String location) { return new Accepted(location); @@ -55,48 +62,24 @@ return new Redirect(location); } - /** - * HTTP 500 Internal Server Error: failure due to an unexpected exception. - * - * <p>Can be returned from REST endpoints, instead of throwing the exception, if additional - * properties (e.g. a traceId) should be set on the response. - * - * @param cause the exception that caused the request to fail, must not be a {@link - * RestApiException} because such an exception would result in a 4XX response code - */ - public static <T> InternalServerError<T> internalServerError(Exception cause) { - return new InternalServerError<>(cause); - } - /** Arbitrary status code with wrapped result. */ public static <T> Response<T> withStatusCode(int statusCode, T value) { return new Impl<>(statusCode, value); } @SuppressWarnings({"unchecked", "rawtypes"}) - public static <T> T unwrap(T obj) throws Exception { + public static <T> T unwrap(T obj) { while (obj instanceof Response) { obj = (T) ((Response) obj).value(); } return obj; } - private String traceId; - - public Response<T> traceId(@Nullable String traceId) { - this.traceId = traceId; - return this; - } - - public Optional<String> traceId() { - return Optional.ofNullable(traceId); - } - public abstract boolean isNone(); public abstract int statusCode(); - public abstract T value() throws Exception; + public abstract T value(); public abstract CacheControl caching(); @@ -286,57 +269,4 @@ return String.format("[202 Accepted] %s", location); } } - - public static final class InternalServerError<T> extends Response<T> { - private final Exception cause; - - private InternalServerError(Exception cause) { - checkArgument(!(cause instanceof RestApiException), "cause must not be a RestApiException"); - this.cause = cause; - } - - @Override - public boolean isNone() { - return false; - } - - @Override - public int statusCode() { - return 500; - } - - @Override - public T value() throws Exception { - throw cause(); - } - - @Override - public CacheControl caching() { - return CacheControl.NONE; - } - - @Override - public Response<T> caching(CacheControl c) { - throw new UnsupportedOperationException(); - } - - public Exception cause() { - return cause; - } - - @Override - public int hashCode() { - return cause.hashCode(); - } - - @Override - public boolean equals(Object o) { - return o instanceof InternalServerError && ((InternalServerError<?>) o).cause.equals(cause); - } - - @Override - public String toString() { - return String.format("[500 Internal Server Error] %s", cause.getClass()); - } - } }
diff --git a/java/com/google/gerrit/gpg/PublicKeyStore.java b/java/com/google/gerrit/gpg/PublicKeyStore.java index 519c400..2cce480 100644 --- a/java/com/google/gerrit/gpg/PublicKeyStore.java +++ b/java/com/google/gerrit/gpg/PublicKeyStore.java
@@ -21,6 +21,7 @@ import com.google.common.base.Preconditions; import com.google.common.io.ByteStreams; +import com.google.gerrit.git.LockFailureException; import com.google.gerrit.git.ObjectIds; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -387,9 +388,10 @@ toAdd.clear(); toRemove.clear(); break; + case LOCK_FAILURE: + throw new LockFailureException("Failed to store public keys", ru); case FORCED: case IO_FAILURE: - case LOCK_FAILURE: case NOT_ATTEMPTED: case REJECTED: case REJECTED_CURRENT_BRANCH:
diff --git a/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/java/com/google/gerrit/gpg/server/PostGpgKeys.java index 5396e1c..1b5e06a 100644 --- a/java/com/google/gerrit/gpg/server/PostGpgKeys.java +++ b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -40,7 +40,6 @@ 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.git.LockFailureException; import com.google.gerrit.gpg.CheckResult; import com.google.gerrit.gpg.Fingerprint; import com.google.gerrit.gpg.GerritPublicKeyChecker; @@ -59,7 +58,6 @@ import com.google.gerrit.server.mail.send.DeleteKeySender; import com.google.gerrit.server.query.account.InternalAccountQuery; import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryHelper.ActionType; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; @@ -207,10 +205,9 @@ AccountResource rsrc, List<PGPPublicKeyRing> keyRings, Collection<Fingerprint> toRemove) throws RestApiException, PGPException, IOException { try { - retryHelper.execute( - ActionType.ACCOUNT_UPDATE, - () -> tryStoreKeys(rsrc, keyRings, toRemove), - LockFailureException.class::isInstance); + retryHelper + .accountUpdate("storeGpgKeys", () -> tryStoreKeys(rsrc, keyRings, toRemove)) + .call(); } catch (Exception e) { Throwables.throwIfUnchecked(e); Throwables.throwIfInstanceOf(e, RestApiException.class);
diff --git a/java/com/google/gerrit/httpd/BUILD b/java/com/google/gerrit/httpd/BUILD index bcb2a2a..d6a43b91 100644 --- a/java/com/google/gerrit/httpd/BUILD +++ b/java/com/google/gerrit/httpd/BUILD
@@ -38,7 +38,6 @@ "//lib:soy", "//lib/auto:auto-value", "//lib/auto:auto-value-annotations", - "//lib/commons:codec", "//lib/commons:lang", "//lib/flogger:api", "//lib/guice",
diff --git a/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java b/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java index 89ad878..77c5381 100644 --- a/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java +++ b/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java
@@ -24,6 +24,7 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import java.io.IOException; +import java.util.Optional; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -46,17 +47,15 @@ if (idString.endsWith("/")) { idString = idString.substring(0, idString.length() - 1); } - Change.Id id; - try { - id = Change.Id.parse(idString); - } catch (IllegalArgumentException e) { + Optional<Change.Id> id = Change.Id.tryParse(idString); + if (!id.isPresent()) { rsp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } ChangeResource changeResource; try { - changeResource = changesCollection.parse(id); + changeResource = changesCollection.parse(id.get()); } catch (ResourceConflictException | ResourceNotFoundException e) { rsp.sendError(HttpServletResponse.SC_NOT_FOUND); return;
diff --git a/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java b/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java index 88a3f0a..d43fcc7 100644 --- a/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java +++ b/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
@@ -20,6 +20,7 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Strings; import com.google.common.flogger.FluentLogger; +import com.google.common.io.BaseEncoding; import com.google.gerrit.entities.Account; import com.google.gerrit.extensions.client.GitBasicAuthPolicy; import com.google.gerrit.extensions.registration.DynamicItem; @@ -48,7 +49,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; -import org.apache.commons.codec.binary.Base64; /** * Authenticates the current user by HTTP basic authentication. @@ -110,7 +110,7 @@ return true; } - final byte[] decoded = Base64.decodeBase64(hdr.substring(LIT_BASIC.length())); + final byte[] decoded = BaseEncoding.base64().decode(hdr.substring(LIT_BASIC.length())); String usernamePassword = new String(decoded, encoding(req)); int splitPos = usernamePassword.indexOf(':'); if (splitPos < 1) {
diff --git a/java/com/google/gerrit/httpd/ProjectOAuthFilter.java b/java/com/google/gerrit/httpd/ProjectOAuthFilter.java index 4b5742d..693232f 100644 --- a/java/com/google/gerrit/httpd/ProjectOAuthFilter.java +++ b/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
@@ -22,6 +22,7 @@ import com.google.common.base.Strings; import com.google.common.collect.Iterables; import com.google.common.flogger.FluentLogger; +import com.google.common.io.BaseEncoding; import com.google.gerrit.entities.Account; import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider; import com.google.gerrit.extensions.registration.DynamicItem; @@ -53,7 +54,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; -import org.apache.commons.codec.binary.Base64; import org.eclipse.jgit.lib.Config; /** @@ -225,7 +225,7 @@ private AuthInfo extractAuthInfo(String hdr, String encoding) throws UnsupportedEncodingException { - byte[] decoded = Base64.decodeBase64(hdr.substring(BASIC.length())); + byte[] decoded = BaseEncoding.base64().decode(hdr.substring(BASIC.length())); String usernamePassword = new String(decoded, encoding); int splitPos = usernamePassword.indexOf(':'); if (splitPos < 1 || splitPos == usernamePassword.length() - 1) {
diff --git a/java/com/google/gerrit/httpd/RemoteUserUtil.java b/java/com/google/gerrit/httpd/RemoteUserUtil.java index a02b5a0..84954dc 100644 --- a/java/com/google/gerrit/httpd/RemoteUserUtil.java +++ b/java/com/google/gerrit/httpd/RemoteUserUtil.java
@@ -18,8 +18,8 @@ import static com.google.common.net.HttpHeaders.AUTHORIZATION; import static java.nio.charset.StandardCharsets.UTF_8; +import com.google.common.io.BaseEncoding; import javax.servlet.http.HttpServletRequest; -import org.eclipse.jgit.util.Base64; public class RemoteUserUtil { /** @@ -70,7 +70,7 @@ } else if (auth.startsWith("Basic ")) { auth = auth.substring("Basic ".length()); - auth = new String(Base64.decode(auth), UTF_8); + auth = new String(BaseEncoding.base64().decode(auth), UTF_8); final int c = auth.indexOf(':'); return c > 0 ? auth.substring(0, c) : null;
diff --git a/java/com/google/gerrit/httpd/auth/oauth/BUILD b/java/com/google/gerrit/httpd/auth/oauth/BUILD index dd4549e..11c9295 100644 --- a/java/com/google/gerrit/httpd/auth/oauth/BUILD +++ b/java/com/google/gerrit/httpd/auth/oauth/BUILD
@@ -16,7 +16,6 @@ "//lib:guava", "//lib:jgit", "//lib:servlet-api", - "//lib/commons:codec", "//lib/flogger:api", "//lib/guice", "//lib/guice:guice-servlet",
diff --git a/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java b/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java index 84dee6e..c7b65d0 100644 --- a/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java +++ b/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
@@ -19,6 +19,7 @@ import com.google.common.base.CharMatcher; import com.google.common.base.Strings; import com.google.common.flogger.FluentLogger; +import com.google.common.io.BaseEncoding; import com.google.gerrit.entities.Account; import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; import com.google.gerrit.extensions.auth.oauth.OAuthToken; @@ -45,7 +46,6 @@ import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.apache.commons.codec.binary.Base64; import org.eclipse.jgit.errors.ConfigInvalidException; @SessionScoped @@ -244,7 +244,7 @@ private static String generateRandomState() { byte[] state = new byte[32]; randomState.nextBytes(state); - return Base64.encodeBase64URLSafeString(state); + return BaseEncoding.base64Url().encode(state); } @Override
diff --git a/java/com/google/gerrit/httpd/auth/openid/BUILD b/java/com/google/gerrit/httpd/auth/openid/BUILD index 94f436b..29841aa 100644 --- a/java/com/google/gerrit/httpd/auth/openid/BUILD +++ b/java/com/google/gerrit/httpd/auth/openid/BUILD
@@ -17,7 +17,6 @@ "//java/com/google/gerrit/server", "//lib:guava", "//lib:servlet-api", - "//lib/commons:codec", "//lib/flogger:api", "//lib/guice", "//lib/guice:guice-servlet",
diff --git a/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java b/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java index bbdb0c4..f9e6286 100644 --- a/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java +++ b/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
@@ -18,6 +18,7 @@ import com.google.common.base.Strings; import com.google.common.flogger.FluentLogger; +import com.google.common.io.BaseEncoding; import com.google.gerrit.entities.Account; import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; import com.google.gerrit.extensions.auth.oauth.OAuthToken; @@ -43,7 +44,6 @@ import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.apache.commons.codec.binary.Base64; import org.eclipse.jgit.errors.ConfigInvalidException; /** OAuth protocol implementation */ @@ -229,7 +229,7 @@ private static String generateRandomState() { byte[] state = new byte[32]; randomState.nextBytes(state); - return Base64.encodeBase64URLSafeString(state); + return BaseEncoding.base64Url().encode(state); } @Override
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java index e6504e2..3da968e 100644 --- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java +++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -100,6 +100,7 @@ import com.google.gerrit.sshd.SshModule; import com.google.gerrit.sshd.commands.DefaultCommandModule; import com.google.gerrit.sshd.commands.IndexCommandsModule; +import com.google.gerrit.sshd.commands.SequenceCommandsModule; import com.google.gerrit.sshd.plugin.LfsPluginAuthCommand; import com.google.inject.AbstractModule; import com.google.inject.CreationException; @@ -373,6 +374,7 @@ sysInjector.getInstance(DownloadConfig.class), sysInjector.getInstance(LfsPluginAuthCommand.Module.class))); modules.add(new IndexCommandsModule(sysInjector)); + modules.add(new SequenceCommandsModule()); return sysInjector.createChildInjector(modules); }
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java b/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java index fc099a6..aa1b921 100644 --- a/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java +++ b/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java
@@ -15,10 +15,11 @@ package com.google.gerrit.httpd.restapi; import com.google.common.base.Strings; +import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.registration.PluginName; import com.google.gerrit.httpd.restapi.RestApiServlet.ViewData; import com.google.gerrit.metrics.Counter1; -import com.google.gerrit.metrics.Counter2; +import com.google.gerrit.metrics.Counter3; import com.google.gerrit.metrics.Description; import com.google.gerrit.metrics.Description.Units; import com.google.gerrit.metrics.Field; @@ -36,7 +37,7 @@ }; final Counter1<String> count; - final Counter2<String, Integer> errorCount; + final Counter3<String, Integer, String> errorCount; final Timer1<String> serverLatency; final Histogram1<String> responseBytes; @@ -59,6 +60,9 @@ viewField, Field.ofInteger("error_code", Metadata.Builder::httpStatus) .description("HTTP status code") + .build(), + Field.ofString("cause", Metadata.Builder::cause) + .description("The cause of the error.") .build()); serverLatency = @@ -79,16 +83,19 @@ } String view(ViewData viewData) { - String impl = viewData.view.getClass().getName().replace('$', '.'); + return view(viewData.view.getClass(), viewData.pluginName); + } + + String view(Class<?> clazz, @Nullable String pluginName) { + String impl = clazz.getName().replace('$', '.'); for (String p : PKGS) { if (impl.startsWith(p)) { impl = impl.substring(p.length()); break; } } - if (!Strings.isNullOrEmpty(viewData.pluginName) - && !PluginName.GERRIT.equals(viewData.pluginName)) { - impl = viewData.pluginName + '-' + impl; + if (!Strings.isNullOrEmpty(pluginName) && !PluginName.GERRIT.equals(pluginName)) { + impl = pluginName + '-' + impl; } return impl; }
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java index 61fa4f9..f390ebc 100644 --- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java +++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -17,6 +17,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.flogger.LazyArgs.lazy; import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS; import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS; @@ -51,6 +52,7 @@ import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.google.common.base.Strings; +import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableSet; @@ -103,6 +105,7 @@ import com.google.gerrit.server.AccessPath; import com.google.gerrit.server.AnonymousUser; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.ExceptionHook; import com.google.gerrit.server.OptionUtil; import com.google.gerrit.server.RequestInfo; import com.google.gerrit.server.RequestListener; @@ -111,10 +114,12 @@ import com.google.gerrit.server.change.ChangeFinder; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.group.GroupAuditService; +import com.google.gerrit.server.logging.Metadata; import com.google.gerrit.server.logging.PerformanceLogContext; import com.google.gerrit.server.logging.PerformanceLogger; import com.google.gerrit.server.logging.RequestId; import com.google.gerrit.server.logging.TraceContext; +import com.google.gerrit.server.logging.TraceContext.TraceTimer; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.permissions.GlobalPermission; import com.google.gerrit.server.permissions.PermissionBackend; @@ -123,6 +128,10 @@ import com.google.gerrit.server.quota.QuotaException; import com.google.gerrit.server.restapi.change.ChangesCollection; import com.google.gerrit.server.restapi.project.ProjectsCollection; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryableAction; +import com.google.gerrit.server.update.RetryableAction.Action; +import com.google.gerrit.server.update.RetryableAction.ActionType; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.time.TimeUtil; import com.google.gerrit.util.http.CacheHeaders; @@ -170,6 +179,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; import java.util.stream.Stream; import java.util.zip.GZIPOutputStream; @@ -241,6 +251,8 @@ final Config config; final DynamicSet<PerformanceLogger> performanceLoggers; final ChangeFinder changeFinder; + final RetryHelper retryHelper; + final PluginSetContext<ExceptionHook> exceptionHooks; @Inject Globals( @@ -254,7 +266,9 @@ RestApiQuotaEnforcer quotaChecker, @GerritServerConfig Config config, DynamicSet<PerformanceLogger> performanceLoggers, - ChangeFinder changeFinder) { + ChangeFinder changeFinder, + RetryHelper retryHelper, + PluginSetContext<ExceptionHook> exceptionHooks) { this.currentUser = currentUser; this.webSession = webSession; this.paramParser = paramParser; @@ -266,6 +280,8 @@ this.config = config; this.performanceLoggers = performanceLoggers; this.changeFinder = changeFinder; + this.retryHelper = retryHelper; + this.exceptionHooks = exceptionHooks; allowOrigin = makeAllowOrigin(config); } @@ -280,7 +296,6 @@ private final Globals globals; private final Provider<RestCollection<RestResource, RestResource>> members; - private Optional<String> traceId = Optional.empty(); public RestApiServlet( Globals globals, RestCollection<? extends RestResource, ? extends RestResource> members) { @@ -306,6 +321,7 @@ res.setHeader("X-Content-Type-Options", "nosniff"); int status = SC_OK; long responseBytes = -1; + Optional<Exception> cause = Optional.empty(); Response<?> response = null; QueryParams qp = null; Object inputRequestBody = null; @@ -326,12 +342,7 @@ // TraceIT#performanceLoggingForRestCall()). try (PerformanceLogContext performanceLogContext = new PerformanceLogContext(globals.config, globals.performanceLoggers)) { - logger.atFinest().log( - "Received REST request: %s %s (parameters: %s)", - req.getMethod(), req.getRequestURI(), getParameterNames(req)); - logger.atFinest().log("Calling user: %s", globals.currentUser.get().getLoggableName()); - logger.atFinest().log( - "Groups: %s", globals.currentUser.get().getEffectiveGroups().getKnownGroups()); + traceRequestData(req); if (isCorsPreflight(req)) { doCorsPreflight(req, res); @@ -376,7 +387,7 @@ } else { IdString id = path.remove(0); try { - rsrc = rc.parse(rsrc, id); + rsrc = parseResourceWithRetry(req, traceContext, viewData.pluginName, rc, rsrc, id); globals.quotaChecker.enforce(rsrc, req); if (path.isEmpty()) { checkPreconditions(req); @@ -449,7 +460,7 @@ } IdString id = path.remove(0); try { - rsrc = c.parse(rsrc, id); + rsrc = parseResourceWithRetry(req, traceContext, viewData.pluginName, c, rsrc, id); checkPreconditions(req); viewData = new ViewData(null, null); } catch (ResourceNotFoundException e) { @@ -484,7 +495,7 @@ checkRequiresCapability(viewData); } - if (notModified(req, rsrc, viewData.view)) { + if (notModified(req, traceContext, viewData, rsrc)) { logger.atFinest().log("REST call succeeded: %d", SC_NOT_MODIFIED); res.sendError(SC_NOT_MODIFIED); return; @@ -495,7 +506,9 @@ } if (viewData.view instanceof RestReadView<?> && isRead(req)) { - response = ((RestReadView<RestResource>) viewData.view).apply(rsrc); + response = + invokeRestReadViewWithRetry( + req, traceContext, viewData, (RestReadView<RestResource>) viewData.view, rsrc); } else if (viewData.view instanceof RestModifyView<?, ?>) { @SuppressWarnings("unchecked") RestModifyView<RestResource, Object> m = @@ -503,7 +516,10 @@ Type type = inputType(m); inputRequestBody = parseRequest(req, type); - response = m.apply(rsrc, inputRequestBody); + response = + invokeRestModifyViewWithRetry( + req, traceContext, viewData, m, rsrc, inputRequestBody); + if (inputRequestBody instanceof RawInput) { try (InputStream is = req.getInputStream()) { ServletUtils.consumeRequestBody(is); @@ -516,7 +532,9 @@ Type type = inputType(m); inputRequestBody = parseRequest(req, type); - response = m.apply(rsrc, path.get(0), inputRequestBody); + response = + invokeRestCollectionCreateViewWithRetry( + req, traceContext, viewData, m, rsrc, path.get(0), inputRequestBody); if (inputRequestBody instanceof RawInput) { try (InputStream is = req.getInputStream()) { ServletUtils.consumeRequestBody(is); @@ -529,7 +547,9 @@ Type type = inputType(m); inputRequestBody = parseRequest(req, type); - response = m.apply(rsrc, path.get(0), inputRequestBody); + response = + invokeRestCollectionDeleteMissingViewWithRetry( + req, traceContext, viewData, m, rsrc, path.get(0), inputRequestBody); if (inputRequestBody instanceof RawInput) { try (InputStream is = req.getInputStream()) { ServletUtils.consumeRequestBody(is); @@ -542,7 +562,9 @@ Type type = inputType(m); inputRequestBody = parseRequest(req, type); - response = m.apply(rsrc, inputRequestBody); + response = + invokeRestCollectionModifyViewWithRetry( + req, traceContext, viewData, m, rsrc, inputRequestBody); if (inputRequestBody instanceof RawInput) { try (InputStream is = req.getInputStream()) { ServletUtils.consumeRequestBody(is); @@ -552,9 +574,6 @@ throw new ResourceNotFoundException(); } - traceId = response.traceId(); - traceId.ifPresent(traceId -> res.addHeader(X_GERRIT_TRACE, traceId)); - if (response instanceof Response.Redirect) { CacheHeaders.setNotCacheable(res); String location = ((Response.Redirect) response).location(); @@ -567,16 +586,10 @@ res.setHeader(HttpHeaders.LOCATION, ((Response.Accepted) response).location()); logger.atFinest().log("REST call succeeded: %d", response.statusCode()); return; - } else if (response instanceof Response.InternalServerError) { - // Rethrow the exception to have exactly the same error handling as if the REST endpoint - // would have thrown the exception directly, instead of returning - // Response.InternalServerError. - Exception cause = ((Response.InternalServerError<?>) response).cause(); - throw cause; } status = response.statusCode(); - configureCaching(req, res, rsrc, viewData.view, response.caching()); + configureCaching(req, res, traceContext, rsrc, viewData, response.caching()); res.setStatus(status); logger.atFinest().log("REST call succeeded: %d", status); } @@ -590,22 +603,28 @@ } } } catch (MalformedJsonException | JsonParseException e) { + cause = Optional.of(e); responseBytes = replyError( req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e); } catch (BadRequestException e) { + cause = Optional.of(e); responseBytes = replyError( req, res, status = SC_BAD_REQUEST, messageOr(e, "Bad Request"), e.caching(), e); } catch (AuthException e) { + cause = Optional.of(e); responseBytes = replyError(req, res, status = SC_FORBIDDEN, messageOr(e, "Forbidden"), e.caching(), e); } catch (AmbiguousViewException e) { + cause = Optional.of(e); responseBytes = replyError(req, res, status = SC_NOT_FOUND, messageOr(e, "Ambiguous"), e); } catch (ResourceNotFoundException e) { + cause = Optional.of(e); responseBytes = replyError(req, res, status = SC_NOT_FOUND, messageOr(e, "Not Found"), e.caching(), e); } catch (MethodNotAllowedException e) { + cause = Optional.of(e); responseBytes = replyError( req, @@ -615,9 +634,11 @@ e.caching(), e); } catch (ResourceConflictException e) { + cause = Optional.of(e); responseBytes = replyError(req, res, status = SC_CONFLICT, messageOr(e, "Conflict"), e.caching(), e); } catch (PreconditionFailedException e) { + cause = Optional.of(e); responseBytes = replyError( req, @@ -627,6 +648,7 @@ e.caching(), e); } catch (UnprocessableEntityException e) { + cause = Optional.of(e); responseBytes = replyError( req, @@ -636,19 +658,22 @@ e.caching(), e); } catch (NotImplementedException e) { + cause = Optional.of(e); logger.atSevere().withCause(e).log("Error in %s %s", req.getMethod(), uriForLogging(req)); responseBytes = replyError(req, res, status = SC_NOT_IMPLEMENTED, messageOr(e, "Not Implemented"), e); } catch (UpdateException e) { + cause = Optional.of(e); Throwable t = e.getCause(); if (t instanceof LockFailureException) { logger.atSevere().withCause(t).log("Error in %s %s", req.getMethod(), uriForLogging(req)); responseBytes = replyError(req, res, status = SC_SERVICE_UNAVAILABLE, "Lock failure", e); } else { status = SC_INTERNAL_SERVER_ERROR; - responseBytes = handleException(e, req, res); + responseBytes = handleException(traceContext, e, req, res); } } catch (QuotaException e) { + cause = Optional.of(e); responseBytes = replyError( req, @@ -658,14 +683,15 @@ e.caching(), e); } catch (Exception e) { + cause = Optional.of(e); status = SC_INTERNAL_SERVER_ERROR; - responseBytes = handleException(e, req, res); + responseBytes = handleException(traceContext, e, req, res); } finally { - String metric = - viewData != null && viewData.view != null ? globals.metrics.view(viewData) : "_unknown"; + String metric = getViewName(viewData); + String formattedCause = cause.map(globals.retryHelper::formatCause).orElse("_none"); globals.metrics.count.increment(metric); if (status >= SC_BAD_REQUEST) { - globals.metrics.errorCount.increment(metric, status); + globals.metrics.errorCount.increment(metric, status, formattedCause); } if (responseBytes != -1) { globals.metrics.responseBytes.record(metric, responseBytes); @@ -688,6 +714,180 @@ } } + private String getEtagWithRetry( + HttpServletRequest req, + TraceContext traceContext, + ViewData viewData, + ETagView<RestResource> view, + RestResource rsrc) { + try (TraceTimer ignored = + TraceContext.newTimer( + "RestApiServlet#getEtagWithRetry:view", + Metadata.builder().restViewName(getViewName(viewData)).build())) { + return invokeRestEndpointWithRetry( + req, + traceContext, + getViewName(viewData) + "#etag", + ActionType.REST_READ_REQUEST, + () -> view.getETag(rsrc)); + } catch (Exception e) { + Throwables.throwIfUnchecked(e); + throw new IllegalStateException("Failed to get ETag for view", e); + } + } + + private String getEtagWithRetry( + HttpServletRequest req, TraceContext traceContext, RestResource.HasETag rsrc) { + try (TraceTimer ignored = + TraceContext.newTimer( + "RestApiServlet#getEtagWithRetry:resource", + Metadata.builder().restViewName(rsrc.getClass().getSimpleName()).build())) { + return invokeRestEndpointWithRetry( + req, + traceContext, + rsrc.getClass().getSimpleName() + "#etag", + ActionType.REST_READ_REQUEST, + () -> rsrc.getETag()); + } catch (Exception e) { + Throwables.throwIfUnchecked(e); + throw new IllegalStateException("Failed to get ETag for resource", e); + } + } + + private RestResource parseResourceWithRetry( + HttpServletRequest req, + TraceContext traceContext, + @Nullable String pluginName, + RestCollection<RestResource, RestResource> restCollection, + RestResource parentResource, + IdString id) + throws Exception { + return invokeRestEndpointWithRetry( + req, + traceContext, + globals.metrics.view(restCollection.getClass(), pluginName) + "#parse", + ActionType.REST_READ_REQUEST, + () -> restCollection.parse(parentResource, id)); + } + + private Response<?> invokeRestReadViewWithRetry( + HttpServletRequest req, + TraceContext traceContext, + ViewData viewData, + RestReadView<RestResource> view, + RestResource rsrc) + throws Exception { + return invokeRestEndpointWithRetry( + req, + traceContext, + getViewName(viewData), + ActionType.REST_READ_REQUEST, + () -> view.apply(rsrc)); + } + + private Response<?> invokeRestModifyViewWithRetry( + HttpServletRequest req, + TraceContext traceContext, + ViewData viewData, + RestModifyView<RestResource, Object> view, + RestResource rsrc, + Object inputRequestBody) + throws Exception { + return invokeRestEndpointWithRetry( + req, + traceContext, + getViewName(viewData), + ActionType.REST_WRITE_REQUEST, + () -> view.apply(rsrc, inputRequestBody)); + } + + private Response<?> invokeRestCollectionCreateViewWithRetry( + HttpServletRequest req, + TraceContext traceContext, + ViewData viewData, + RestCollectionCreateView<RestResource, RestResource, Object> view, + RestResource rsrc, + IdString path, + Object inputRequestBody) + throws Exception { + return invokeRestEndpointWithRetry( + req, + traceContext, + getViewName(viewData), + ActionType.REST_WRITE_REQUEST, + () -> view.apply(rsrc, path, inputRequestBody)); + } + + private Response<?> invokeRestCollectionDeleteMissingViewWithRetry( + HttpServletRequest req, + TraceContext traceContext, + ViewData viewData, + RestCollectionDeleteMissingView<RestResource, RestResource, Object> view, + RestResource rsrc, + IdString path, + Object inputRequestBody) + throws Exception { + return invokeRestEndpointWithRetry( + req, + traceContext, + getViewName(viewData), + ActionType.REST_WRITE_REQUEST, + () -> view.apply(rsrc, path, inputRequestBody)); + } + + private Response<?> invokeRestCollectionModifyViewWithRetry( + HttpServletRequest req, + TraceContext traceContext, + ViewData viewData, + RestCollectionModifyView<RestResource, RestResource, Object> view, + RestResource rsrc, + Object inputRequestBody) + throws Exception { + return invokeRestEndpointWithRetry( + req, + traceContext, + getViewName(viewData), + ActionType.REST_WRITE_REQUEST, + () -> view.apply(rsrc, inputRequestBody)); + } + + private <T> T invokeRestEndpointWithRetry( + HttpServletRequest req, + TraceContext traceContext, + String caller, + ActionType actionType, + Action<T> action) + throws Exception { + RetryableAction<T> retryableAction = globals.retryHelper.action(actionType, caller, action); + AtomicReference<Optional<String>> traceId = new AtomicReference<>(Optional.empty()); + if (!traceContext.isTracing()) { + // enable automatic retry with tracing in case of non-recoverable failure + retryableAction + .retryWithTrace(t -> !(t instanceof RestApiException)) + .onAutoTrace( + autoTraceId -> { + traceId.set(Optional.of(autoTraceId)); + + // Include details of the request into the trace. + traceRequestData(req); + }); + } + try { + return retryableAction.call(); + } finally { + // If auto-tracing got triggered due to a non-recoverable failure, also trace the rest of + // this request. This means logging is forced for all further log statements and the logs are + // associated with the same trace ID. + traceId + .get() + .ifPresent(tid -> traceContext.addTag(RequestId.Type.TRACE_ID, tid).forceLogging()); + } + } + + private String getViewName(ViewData viewData) { + return viewData != null && viewData.view != null ? globals.metrics.view(viewData) : "_unknown"; + } + private static HttpServletRequest applyXdOverrides(HttpServletRequest req, QueryParams qp) throws BadRequestException { if (!isPost(req)) { @@ -805,24 +1005,27 @@ return defaultMessage; } - @SuppressWarnings({"unchecked", "rawtypes"}) - private static boolean notModified( - HttpServletRequest req, RestResource rsrc, RestView<RestResource> view) { + private boolean notModified( + HttpServletRequest req, TraceContext traceContext, ViewData viewData, RestResource rsrc) { if (!isRead(req)) { return false; } + RestView<RestResource> view = viewData.view; if (view instanceof ETagView) { String have = req.getHeader(HttpHeaders.IF_NONE_MATCH); if (have != null) { - return have.equals(((ETagView) view).getETag(rsrc)); + String eTag = + getEtagWithRetry(req, traceContext, viewData, (ETagView<RestResource>) view, rsrc); + return have.equals(eTag); } } if (rsrc instanceof RestResource.HasETag) { String have = req.getHeader(HttpHeaders.IF_NONE_MATCH); if (have != null) { - return have.equals(((RestResource.HasETag) rsrc).getETag()); + String eTag = getEtagWithRetry(req, traceContext, (RestResource.HasETag) rsrc); + return have.equals(eTag); } } @@ -836,21 +1039,48 @@ return false; } - private static <R extends RestResource> void configureCaching( - HttpServletRequest req, HttpServletResponse res, R rsrc, RestView<R> view, CacheControl c) { + private <R extends RestResource> void configureCaching( + HttpServletRequest req, + HttpServletResponse res, + TraceContext traceContext, + R rsrc, + ViewData viewData, + CacheControl cacheControl) { + setCacheHeaders(req, res, cacheControl); if (isRead(req)) { - switch (c.getType()) { + switch (cacheControl.getType()) { + case NONE: + default: + break; + case PRIVATE: + addResourceStateHeaders(req, res, traceContext, viewData, rsrc); + break; + case PUBLIC: + addResourceStateHeaders(req, res, traceContext, viewData, rsrc); + break; + } + } + } + + private static <R extends RestResource> void setCacheHeaders( + HttpServletRequest req, HttpServletResponse res, CacheControl cacheControl) { + if (isRead(req)) { + switch (cacheControl.getType()) { case NONE: default: CacheHeaders.setNotCacheable(res); break; case PRIVATE: - addResourceStateHeaders(res, rsrc, view); - CacheHeaders.setCacheablePrivate(res, c.getAge(), c.getUnit(), c.isMustRevalidate()); + CacheHeaders.setCacheablePrivate( + res, cacheControl.getAge(), cacheControl.getUnit(), cacheControl.isMustRevalidate()); break; case PUBLIC: - addResourceStateHeaders(res, rsrc, view); - CacheHeaders.setCacheable(req, res, c.getAge(), c.getUnit(), c.isMustRevalidate()); + CacheHeaders.setCacheable( + req, + res, + cacheControl.getAge(), + cacheControl.getUnit(), + cacheControl.isMustRevalidate()); break; } } else { @@ -858,12 +1088,20 @@ } } - private static <R extends RestResource> void addResourceStateHeaders( - HttpServletResponse res, R rsrc, RestView<R> view) { + private void addResourceStateHeaders( + HttpServletRequest req, + HttpServletResponse res, + TraceContext traceContext, + ViewData viewData, + RestResource rsrc) { + RestView<RestResource> view = viewData.view; if (view instanceof ETagView) { - res.setHeader(HttpHeaders.ETAG, ((ETagView<R>) view).getETag(rsrc)); + String eTag = + getEtagWithRetry(req, traceContext, viewData, (ETagView<RestResource>) view, rsrc); + res.setHeader(HttpHeaders.ETAG, eTag); } else if (rsrc instanceof RestResource.HasETag) { - res.setHeader(HttpHeaders.ETAG, ((RestResource.HasETag) rsrc).getETag()); + String eTag = getEtagWithRetry(req, traceContext, (RestResource.HasETag) rsrc); + res.setHeader(HttpHeaders.ETAG, eTag); } if (rsrc instanceof RestResource.HasLastModified) { res.setDateHeader( @@ -1451,6 +1689,15 @@ return requestInfo.build(); } + private void traceRequestData(HttpServletRequest req) { + logger.atFinest().log( + "Received REST request: %s %s (parameters: %s)", + req.getMethod(), req.getRequestURI(), getParameterNames(req)); + logger.atFinest().log("Calling user: %s", globals.currentUser.get().getLoggableName()); + logger.atFinest().log( + "Groups: %s", globals.currentUser.get().getEffectiveGroups().getKnownGroups()); + } + private boolean isDelete(HttpServletRequest req) { return "DELETE".equals(req.getMethod()); } @@ -1493,15 +1740,50 @@ } } - private long handleException(Throwable err, HttpServletRequest req, HttpServletResponse res) + private long handleException( + TraceContext traceContext, Throwable err, HttpServletRequest req, HttpServletResponse res) throws IOException { - logger.atSevere().withCause(err).log("Error in %s %s", req.getMethod(), uriForLogging(req)); - if (!res.isCommitted()) { - res.reset(); - traceId.ifPresent(traceId -> res.addHeader(X_GERRIT_TRACE, traceId)); - return replyError(req, res, SC_INTERNAL_SERVER_ERROR, "Internal server error", err); + if (res.isCommitted()) { + logger.atSevere().withCause(err).log( + "Error in %s %s, response already committed", req.getMethod(), uriForLogging(req)); + return 0; } - return 0; + + res.reset(); + traceContext.getTraceId().ifPresent(traceId -> res.addHeader(X_GERRIT_TRACE, traceId)); + ImmutableList<String> userMessages = + globals.exceptionHooks.stream() + .map(h -> h.getUserMessage(err)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(toImmutableList()); + + Optional<Integer> statusCode = + globals.exceptionHooks.stream() + .map(h -> h.getStatusCode(err)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); + if (statusCode.isPresent() && statusCode.get() < 400) { + StringBuilder msg = new StringBuilder(); + if (userMessages.size() == 1) { + msg.append(userMessages.get(0)); + } else { + userMessages.forEach(m -> msg.append("\n* ").append(m)); + } + + res.setStatus(statusCode.get()); + logger.atFinest().withCause(err).log("REST call finished: %d", statusCode.get().intValue()); + return replyText(req, res, true, msg.toString()); + } + + logger.atSevere().withCause(err).log("Error in %s %s", req.getMethod(), uriForLogging(req)); + + StringBuilder msg = new StringBuilder("Internal server error"); + if (!userMessages.isEmpty()) { + userMessages.forEach(m -> msg.append("\n* ").append(m)); + } + return replyError(req, res, statusCode.orElse(SC_INTERNAL_SERVER_ERROR), msg.toString(), err); } private static String uriForLogging(HttpServletRequest req) { @@ -1527,13 +1809,13 @@ HttpServletResponse res, int statusCode, String msg, - CacheControl c, + CacheControl cacheControl, @Nullable Throwable err) throws IOException { if (err != null) { RequestUtil.setErrorTraceAttribute(req, err); } - configureCaching(req, res, null, null, c); + setCacheHeaders(req, res, cacheControl); checkArgument(statusCode >= 400, "non-error status: %s", statusCode); res.setStatus(statusCode); logger.atFinest().withCause(err).log("REST call failed: %d", statusCode);
diff --git a/java/com/google/gerrit/index/IndexCollection.java b/java/com/google/gerrit/index/IndexCollection.java index 0615453..c61e6c7 100644 --- a/java/com/google/gerrit/index/IndexCollection.java +++ b/java/com/google/gerrit/index/IndexCollection.java
@@ -32,7 +32,7 @@ this.searchIndex = new AtomicReference<>(); } - /** @return the current search index version. */ + /** Returns the current search index version. */ public I getSearchIndex() { return searchIndex.get(); }
diff --git a/java/com/google/gerrit/index/IndexRewriter.java b/java/com/google/gerrit/index/IndexRewriter.java index 4d6a35b..7f3bc2d 100644 --- a/java/com/google/gerrit/index/IndexRewriter.java +++ b/java/com/google/gerrit/index/IndexRewriter.java
@@ -17,7 +17,15 @@ import com.google.gerrit.index.query.Predicate; import com.google.gerrit.index.query.QueryParseException; +/** + * Rewriter to sanitize queries before they are sent to the index. The idea here is that the + * rewriter swaps out predicates so that the query can be processed by the index. + */ public interface IndexRewriter<T> { + /** + * Returns a sanitized version of the provided predicate. Uses {@link QueryOptions} to enforce + * index-specific limits such as {@code maxTerms}. + */ Predicate<T> rewrite(Predicate<T> in, QueryOptions opts) throws QueryParseException; }
diff --git a/java/com/google/gerrit/index/Schema.java b/java/com/google/gerrit/index/Schema.java index f9f8c48..0aa374b 100644 --- a/java/com/google/gerrit/index/Schema.java +++ b/java/com/google/gerrit/index/Schema.java
@@ -15,11 +15,12 @@ package com.google.gerrit.index; import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.ImmutableList.toImmutableList; import com.google.common.base.MoreObjects; -import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.flogger.FluentLogger; import java.util.ArrayList; import java.util.Arrays; @@ -181,12 +182,17 @@ * <p>Null values are omitted, as are fields which cause errors, which are logged. * * @param obj input object. + * @param skipFields set of field names to skip when indexing the document * @return all non-null field values from the object. */ - public final Iterable<Values<T>> buildFields(T obj) { - return FluentIterable.from(fields.values()) - .transform( + public final Iterable<Values<T>> buildFields(T obj, ImmutableSet<String> skipFields) { + return fields.values().stream() + .map( f -> { + if (skipFields.contains(f.getName())) { + return null; + } + Object v; try { v = f.get(obj); @@ -203,7 +209,8 @@ return new Values<>(f, Collections.singleton(v)); } }) - .filter(Objects::nonNull); + .filter(Objects::nonNull) + .collect(toImmutableList()); } @Override
diff --git a/java/com/google/gerrit/index/SiteIndexer.java b/java/com/google/gerrit/index/SiteIndexer.java index c3ab8a4..ecfc7bd 100644 --- a/java/com/google/gerrit/index/SiteIndexer.java +++ b/java/com/google/gerrit/index/SiteIndexer.java
@@ -17,6 +17,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Objects.requireNonNull; +import com.google.auto.value.AutoValue; import com.google.common.base.Stopwatch; import com.google.common.flogger.FluentLogger; import com.google.common.util.concurrent.ListenableFuture; @@ -31,36 +32,28 @@ import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.util.io.NullOutputStream; +/** Base class for implementations that can index all entities of a given type. */ public abstract class SiteIndexer<K, V, I extends Index<K, V>> { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - public static class Result { - private final long elapsedNanos; - private final boolean success; - private final int done; - private final int failed; + /** Result of an operation to index a subset or all of the entities of a given type. */ + @AutoValue + public abstract static class Result { + public abstract long elapsedNanos(); - public Result(Stopwatch sw, boolean success, int done, int failed) { - this.elapsedNanos = sw.elapsed(TimeUnit.NANOSECONDS); - this.success = success; - this.done = done; - this.failed = failed; - } + public abstract boolean success(); - public boolean success() { - return success; - } + public abstract int doneCount(); - public int doneCount() { - return done; - } + public abstract int failedCount(); - public int failedCount() { - return failed; + public static Result create(Stopwatch sw, boolean success, int done, int failed) { + return new AutoValue_SiteIndexer_Result( + sw.elapsed(TimeUnit.NANOSECONDS), success, done, failed); } public long elapsed(TimeUnit timeUnit) { - return timeUnit.convert(elapsedNanos, TimeUnit.NANOSECONDS); + return timeUnit.convert(elapsedNanos(), TimeUnit.NANOSECONDS); } } @@ -80,6 +73,7 @@ verboseWriter = newPrintWriter(requireNonNull(out)); } + /** Indexes all entities for the provided index. */ public abstract Result indexAll(I index); protected final void addErrorListener(
diff --git a/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java b/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java index f355216..3cc5f9b 100644 --- a/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java +++ b/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
@@ -19,6 +19,7 @@ import com.google.gerrit.index.Schema; import com.google.gerrit.index.SchemaDefinitions; +/** Definition of project index versions (schemata). See {@link SchemaDefinitions}. */ public class ProjectSchemaDefinitions extends SchemaDefinitions<ProjectData> { @Deprecated @@ -39,10 +40,14 @@ // Lucene index was changed to add an additional field for sorting. static final Schema<ProjectData> V4 = schema(V3); - public static final ProjectSchemaDefinitions INSTANCE = new ProjectSchemaDefinitions(); - + /** + * Name of the project index to be used when contacting index backends or loading configurations. + */ public static final String NAME = "projects"; + /** Singleton instance of the schema definitions. This is one per JVM. */ + public static final ProjectSchemaDefinitions INSTANCE = new ProjectSchemaDefinitions(); + private ProjectSchemaDefinitions() { super(NAME, ProjectData.class); }
diff --git a/java/com/google/gerrit/index/query/QueryBuilder.java b/java/com/google/gerrit/index/query/QueryBuilder.java index d24cfeb..85dcf3e 100644 --- a/java/com/google/gerrit/index/query/QueryBuilder.java +++ b/java/com/google/gerrit/index/query/QueryBuilder.java
@@ -29,6 +29,7 @@ import com.google.common.base.Ascii; import com.google.common.base.CharMatcher; +import com.google.common.base.MoreObjects; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.gerrit.common.Nullable; @@ -42,7 +43,9 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; import org.antlr.runtime.tree.Tree; /** @@ -184,6 +187,7 @@ protected final Definition<T, Q> builderDef; private final ImmutableMap<String, OperatorFactory<T, Q>> opFactories; + protected Map<String, String> opAliases = Collections.emptyMap(); protected QueryBuilder( Definition<T, Q> def, @@ -220,6 +224,10 @@ return toPredicate(QueryParser.parse(query)); } + public void setOperatorAliases(Map<String, String> opAliases) { + this.opAliases = opAliases; + } + /** * Parse multiple user-supplied query strings into a list of predicates. * @@ -290,8 +298,12 @@ @SuppressWarnings("unchecked") private Predicate<T> operator(String name, String value) throws QueryParseException { + String opName = MoreObjects.firstNonNull(opAliases.get(name), name); @SuppressWarnings("rawtypes") - OperatorFactory f = opFactories.get(name); + OperatorFactory f = opFactories.get(opName); + if (f == null && !opName.equals(name)) { + f = opFactories.get(name); + } if (f == null) { throw error("Unsupported operator " + name + ":" + value); }
diff --git a/java/com/google/gerrit/index/query/QueryProcessor.java b/java/com/google/gerrit/index/query/QueryProcessor.java index 9501e52..c05516b 100644 --- a/java/com/google/gerrit/index/query/QueryProcessor.java +++ b/java/com/google/gerrit/index/query/QueryProcessor.java
@@ -18,7 +18,7 @@ import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.flogger.LazyArgs.lazy; -import static java.util.stream.Collectors.toSet; +import static java.util.stream.Collectors.toList; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; @@ -272,7 +272,7 @@ ImmutableList<T> matchesList = matches.get(i).toList(); logger.atFine().log( "Matches[%d]:\n%s", - i, lazy(() -> matchesList.stream().map(this::formatForLogging).collect(toSet()))); + i, lazy(() -> matchesList.stream().map(this::formatForLogging).collect(toList()))); out.add( QueryResult.create( queryStrings != null ? queryStrings.get(i) : null,
diff --git a/java/com/google/gerrit/index/query/RangeUtil.java b/java/com/google/gerrit/index/query/RangeUtil.java index 1f22f36..cfe1929 100644 --- a/java/com/google/gerrit/index/query/RangeUtil.java +++ b/java/com/google/gerrit/index/query/RangeUtil.java
@@ -106,6 +106,10 @@ break; } + // Ensure that minValue <= min/max <= maxValue. + min = Ints.constrainToRange(min, minValue, maxValue); + max = Ints.constrainToRange(max, minValue, maxValue); + return new Range(prefix, min, max); }
diff --git a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java index deb3203..5392ab4 100644 --- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java +++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -21,6 +21,7 @@ import com.google.common.base.Joiner; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.ListMultimap; import com.google.common.collect.Sets; import com.google.common.flogger.FluentLogger; @@ -98,6 +99,7 @@ private final SitePaths sitePaths; private final Directory dir; private final String name; + private final ImmutableSet<String> skipFields; private final ListeningExecutorService writerThread; private final IndexWriter writer; private final ReferenceManager<IndexSearcher> searcherManager; @@ -110,6 +112,7 @@ SitePaths sitePaths, Directory dir, String name, + ImmutableSet<String> skipFields, String subIndex, GerritIndexWriterConfig writerConfig, SearcherFactory searcherFactory) @@ -118,6 +121,7 @@ this.sitePaths = sitePaths; this.dir = dir; this.name = name; + this.skipFields = skipFields; String index = Joiner.on('_').skipNulls().join(name, subIndex); long commitPeriod = writerConfig.getCommitWithinMs(); @@ -311,7 +315,7 @@ Document toDocument(V obj) { Document result = new Document(); - for (Values<V> vs : schema.buildFields(obj)) { + for (Values<V> vs : schema.buildFields(obj, skipFields)) { if (vs.getValues() != null) { add(result, vs); }
diff --git a/java/com/google/gerrit/lucene/ChangeSubIndex.java b/java/com/google/gerrit/lucene/ChangeSubIndex.java index fd439f1..e51a91a7 100644 --- a/java/com/google/gerrit/lucene/ChangeSubIndex.java +++ b/java/com/google/gerrit/lucene/ChangeSubIndex.java
@@ -20,6 +20,7 @@ import static com.google.gerrit.lucene.LuceneChangeIndex.UPDATED_SORT_FIELD; import static com.google.gerrit.server.index.change.ChangeSchemaDefinitions.NAME; +import com.google.common.collect.ImmutableSet; import com.google.gerrit.entities.Change; import com.google.gerrit.index.FieldDef; import com.google.gerrit.index.QueryOptions; @@ -48,6 +49,7 @@ Schema<ChangeData> schema, SitePaths sitePaths, Path path, + ImmutableSet<String> skipFields, GerritIndexWriterConfig writerConfig, SearcherFactory searcherFactory) throws IOException { @@ -56,6 +58,7 @@ sitePaths, FSDirectory.open(path), path.getFileName().toString(), + skipFields, writerConfig, searcherFactory); } @@ -65,10 +68,11 @@ SitePaths sitePaths, Directory dir, String subIndex, + ImmutableSet<String> skipFields, GerritIndexWriterConfig writerConfig, SearcherFactory searcherFactory) throws IOException { - super(schema, sitePaths, dir, NAME, subIndex, writerConfig, searcherFactory); + super(schema, sitePaths, dir, NAME, skipFields, subIndex, writerConfig, searcherFactory); } @Override
diff --git a/java/com/google/gerrit/lucene/LuceneAccountIndex.java b/java/com/google/gerrit/lucene/LuceneAccountIndex.java index efd7ea3..242cffd 100644 --- a/java/com/google/gerrit/lucene/LuceneAccountIndex.java +++ b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
@@ -20,6 +20,7 @@ import static com.google.gerrit.server.index.account.AccountField.ID_STR; import static com.google.gerrit.server.index.account.AccountField.PREFERRED_EMAIL_EXACT; +import com.google.common.collect.ImmutableSet; import com.google.gerrit.entities.Account; import com.google.gerrit.exceptions.StorageException; import com.google.gerrit.index.FieldDef; @@ -100,6 +101,7 @@ sitePaths, dir(schema, cfg, sitePaths), ACCOUNTS, + ImmutableSet.of(), null, new GerritIndexWriterConfig(cfg, ACCOUNTS), new SearcherFactory());
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java index 16d66b6..3b277dd 100644 --- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java +++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -29,6 +29,7 @@ import com.google.common.collect.Collections2; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import com.google.common.collect.MultimapBuilder; @@ -167,6 +168,7 @@ private final String idSortFieldName; private final IdTerm idTerm; private final ChangeIdExtractor extractor; + private final ImmutableSet<String> skipFields; @Inject LuceneChangeIndex( @@ -179,6 +181,10 @@ this.executor = executor; this.changeDataFactory = changeDataFactory; this.schema = schema; + this.skipFields = + cfg.getBoolean("index", "change", "indexMergeable", true) + ? ImmutableSet.of() + : ImmutableSet.of(ChangeField.MERGEABLE.getName()); GerritIndexWriterConfig openConfig = new GerritIndexWriterConfig(cfg, "changes_open"); GerritIndexWriterConfig closedConfig = new GerritIndexWriterConfig(cfg, "changes_closed"); @@ -189,18 +195,40 @@ if (LuceneIndexModule.isInMemoryTest(cfg)) { openIndex = new ChangeSubIndex( - schema, sitePaths, new RAMDirectory(), "ramOpen", openConfig, searcherFactory); + schema, + sitePaths, + new RAMDirectory(), + "ramOpen", + skipFields, + openConfig, + searcherFactory); closedIndex = new ChangeSubIndex( - schema, sitePaths, new RAMDirectory(), "ramClosed", closedConfig, searcherFactory); + schema, + sitePaths, + new RAMDirectory(), + "ramClosed", + skipFields, + closedConfig, + searcherFactory); } else { Path dir = LuceneVersionManager.getDir(sitePaths, CHANGES, schema); openIndex = new ChangeSubIndex( - schema, sitePaths, dir.resolve(CHANGES_OPEN), openConfig, searcherFactory); + schema, + sitePaths, + dir.resolve(CHANGES_OPEN), + skipFields, + openConfig, + searcherFactory); closedIndex = new ChangeSubIndex( - schema, sitePaths, dir.resolve(CHANGES_CLOSED), closedConfig, searcherFactory); + schema, + sitePaths, + dir.resolve(CHANGES_CLOSED), + skipFields, + closedConfig, + searcherFactory); } idField = this.schema.useLegacyNumericFields() ? LEGACY_ID : LEGACY_ID_STR; @@ -565,7 +593,7 @@ private void decodeMergeable(ListMultimap<String, IndexableField> doc, ChangeData cd) { IndexableField f = Iterables.getFirst(doc.get(MERGEABLE_FIELD), null); - if (f != null) { + if (f != null && !skipFields.contains(MERGEABLE_FIELD)) { String mergeable = f.stringValue(); if ("1".equals(mergeable)) { cd.setMergeable(true);
diff --git a/java/com/google/gerrit/lucene/LuceneGroupIndex.java b/java/com/google/gerrit/lucene/LuceneGroupIndex.java index 99cd40d..3d1d471 100644 --- a/java/com/google/gerrit/lucene/LuceneGroupIndex.java +++ b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
@@ -17,6 +17,7 @@ import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.gerrit.server.index.group.GroupField.UUID; +import com.google.common.collect.ImmutableSet; import com.google.gerrit.entities.AccountGroup; import com.google.gerrit.exceptions.StorageException; import com.google.gerrit.index.FieldDef; @@ -90,6 +91,7 @@ sitePaths, dir(schema, cfg, sitePaths), GROUPS, + ImmutableSet.of(), null, new GerritIndexWriterConfig(cfg, GROUPS), new SearcherFactory());
diff --git a/java/com/google/gerrit/lucene/LuceneProjectIndex.java b/java/com/google/gerrit/lucene/LuceneProjectIndex.java index 97454c7..a3a0d9c 100644 --- a/java/com/google/gerrit/lucene/LuceneProjectIndex.java +++ b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
@@ -17,6 +17,7 @@ import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.gerrit.index.project.ProjectField.NAME; +import com.google.common.collect.ImmutableSet; import com.google.gerrit.entities.Project; import com.google.gerrit.exceptions.StorageException; import com.google.gerrit.index.FieldDef; @@ -90,6 +91,7 @@ sitePaths, dir(schema, cfg, sitePaths), PROJECTS, + ImmutableSet.of(), null, new GerritIndexWriterConfig(cfg, PROJECTS), new SearcherFactory());
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java index 88ccf18..d8beb76 100644 --- a/java/com/google/gerrit/pgm/Daemon.java +++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -110,6 +110,7 @@ import com.google.gerrit.sshd.SshModule; import com.google.gerrit.sshd.commands.DefaultCommandModule; import com.google.gerrit.sshd.commands.IndexCommandsModule; +import com.google.gerrit.sshd.commands.SequenceCommandsModule; import com.google.gerrit.sshd.plugin.LfsPluginAuthCommand; import com.google.inject.AbstractModule; import com.google.inject.Guice; @@ -518,6 +519,7 @@ sysInjector.getInstance(LfsPluginAuthCommand.Module.class))); if (!replica) { modules.add(new IndexCommandsModule(sysInjector)); + modules.add(new SequenceCommandsModule()); } return sysInjector.createChildInjector(modules); }
diff --git a/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/java/com/google/gerrit/pgm/http/jetty/JettyServer.java index 22bc21d..096e4a1 100644 --- a/java/com/google/gerrit/pgm/http/jetty/JettyServer.java +++ b/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -36,8 +36,10 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.EnumSet; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import javax.servlet.DispatcherType; @@ -411,10 +413,20 @@ Class<? extends Filter> filterClass = (Class<? extends Filter>) Class.forName(filterClassName); Filter filter = env.webInjector.getInstance(filterClass); - app.addFilter( - new FilterHolder(filter), - "/*", - EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC)); + + Map<String, String> initParams = new HashMap<>(); + Set<String> initParamKeys = cfg.getNames("filterClass", filterClassName, true); + initParamKeys.forEach( + paramKey -> { + String paramValue = cfg.getString("filterClass", filterClassName, paramKey); + initParams.put(paramKey, paramValue); + }); + + FilterHolder filterHolder = new FilterHolder(filter); + if (initParams.size() > 0) { + filterHolder.setInitParameters(initParams); + } + app.addFilter(filterHolder, "/*", EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC)); } catch (Throwable e) { throw new IllegalArgumentException( "Unable to instantiate front-end HTTP Filter " + filterClassName, e);
diff --git a/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java b/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java index 9354209..4f9d7e7 100644 --- a/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java +++ b/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
@@ -34,6 +34,10 @@ import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.servlet.AsyncContext; +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; +import javax.servlet.DispatcherType; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; @@ -43,16 +47,13 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.eclipse.jetty.continuation.Continuation; -import org.eclipse.jetty.continuation.ContinuationListener; -import org.eclipse.jetty.continuation.ContinuationSupport; import org.eclipse.jgit.lib.Config; /** - * Use Jetty continuations to defer execution until threads are available. + * Use AsyncContexts to defer execution until threads are available. * * <p>We actually schedule a task into the same execution queue as the SSH daemon uses for command - * execution, and then park the web request in a continuation until an execution thread is + * execution, and then park the web request in an AsyncContext until an execution thread is * available. This ensures that the overall JVM process doesn't exceed the configured limit on * concurrent Git requests. * @@ -61,12 +62,10 @@ * Jetty's HTTP parser to crash, so we instead block the SSH execution queue thread and ask Jetty to * resume processing on the web service thread. */ -@SuppressWarnings("deprecation") @Singleton public class ProjectQoSFilter implements Filter { - private static final String ATT_SPACE = ProjectQoSFilter.class.getName(); - private static final String TASK = ATT_SPACE + "/TASK"; - private static final String CANCEL = ATT_SPACE + "/CANCEL"; + private static final String ATT_SPACE = ProjectQoSFilter.class.getName() + "/"; + private static final String TASK = ATT_SPACE + "TASK"; private static final String FILTER_RE = "^/(.*)/(git-upload-pack|git-receive-pack)$"; private static final Pattern URI_PATTERN = Pattern.compile(FILTER_RE); @@ -79,6 +78,59 @@ } } + public enum RequestState { + INITIAL, + SUSPENDED, + RESUMED, + CANCELED, + UNEXPECTED; + + private static final String CANCELED_ATT = ATT_SPACE + CANCELED; + private static final String SUSPENDED_ATT = ATT_SPACE + SUSPENDED; + private static final String RESUMED_ATT = ATT_SPACE + RESUMED; + + private void set(ServletRequest req) { + switch (this) { + case SUSPENDED: + req.setAttribute(SUSPENDED_ATT, true); + req.setAttribute(RESUMED_ATT, false); + break; + case CANCELED: + req.setAttribute(CANCELED_ATT, true); + break; + case RESUMED: + req.setAttribute(RESUMED_ATT, true); + break; + case INITIAL: + case UNEXPECTED: + default: + break; + } + } + + private static RequestState get(ServletRequest req) { + if (Boolean.FALSE.equals(req.getAttribute(RESUMED_ATT)) + && Boolean.TRUE.equals(req.getAttribute(SUSPENDED_ATT))) { + return SUSPENDED; + } + + if (req.getDispatcherType() != DispatcherType.ASYNC) { + return INITIAL; + } + + if (Boolean.TRUE.equals(req.getAttribute(RESUMED_ATT)) + && Boolean.TRUE.equals(req.getAttribute(CANCELED_ATT))) { + return CANCELED; + } + + if (Boolean.TRUE.equals(req.getAttribute(RESUMED_ATT))) { + return RESUMED; + } + + return UNEXPECTED; + } + } + private final AccountLimits.Factory limitsFactory; private final Provider<CurrentUser> user; private final QueueProvider queue; @@ -104,40 +156,50 @@ throws IOException, ServletException { final HttpServletRequest req = (HttpServletRequest) request; final HttpServletResponse rsp = (HttpServletResponse) response; - final Continuation cont = ContinuationSupport.getContinuation(req); - if (cont.isInitial()) { - TaskThunk task = new TaskThunk(cont, req); - if (maxWait > 0) { - cont.setTimeout(maxWait); - } - cont.suspend(rsp); - cont.setAttribute(TASK, task); + final TaskThunk task; - Future<?> f = getExecutor().submit(task); - cont.addContinuationListener(new Listener(f)); - } else if (cont.isExpired()) { - rsp.sendError(SC_SERVICE_UNAVAILABLE); + switch (RequestState.get(request)) { + case INITIAL: + AsyncContext asyncContext = suspend(request); + task = new TaskThunk(asyncContext, req); + if (maxWait > 0) { + asyncContext.setTimeout(maxWait); + } - } else if (cont.isResumed() && cont.getAttribute(CANCEL) == Boolean.TRUE) { - rsp.sendError(SC_SERVICE_UNAVAILABLE); + request.setAttribute(TASK, task); - } else if (cont.isResumed()) { - TaskThunk task = (TaskThunk) cont.getAttribute(TASK); - try { - task.begin(Thread.currentThread()); - chain.doFilter(req, rsp); - } finally { - task.end(); - Thread.interrupted(); - } - - } else { - context.log("Unexpected QoS continuation state, aborting request"); - rsp.sendError(SC_SERVICE_UNAVAILABLE); + Future<?> f = getExecutor().submit(task); + asyncContext.addListener(new Listener(f)); + break; + case CANCELED: + rsp.sendError(SC_SERVICE_UNAVAILABLE); + break; + case RESUMED: + task = (TaskThunk) request.getAttribute(TASK); + try { + task.begin(Thread.currentThread()); + chain.doFilter(req, rsp); + } finally { + task.end(); + Thread.interrupted(); + } + break; + case SUSPENDED: + case UNEXPECTED: + default: + context.log("Unexpected QoS state, aborting request"); + rsp.sendError(SC_SERVICE_UNAVAILABLE); + break; } } + private AsyncContext suspend(ServletRequest request) { + AsyncContext asyncContext = request.startAsync(); + RequestState.SUSPENDED.set(request); + return asyncContext; + } + private ScheduledThreadPoolExecutor getExecutor() { QueueProvider.QueueType qt = limitsFactory.create(user.get()).getQueueType(); return queue.getQueue(qt); @@ -149,7 +211,7 @@ @Override public void destroy() {} - private static final class Listener implements ContinuationListener { + private static final class Listener implements AsyncListener { final Future<?> future; Listener(Future<?> future) { @@ -157,29 +219,35 @@ } @Override - public void onComplete(Continuation self) {} + public void onComplete(AsyncEvent event) throws IOException {} @Override - public void onTimeout(Continuation self) { + public void onTimeout(AsyncEvent event) throws IOException { future.cancel(true); } + + @Override + public void onError(AsyncEvent event) throws IOException {} + + @Override + public void onStartAsync(AsyncEvent event) throws IOException {} } private final class TaskThunk implements CancelableRunnable { - private final Continuation cont; + private final AsyncContext asyncContext; private final String name; private final Object lock = new Object(); private boolean done; private Thread worker; - TaskThunk(Continuation cont, HttpServletRequest req) { - this.cont = cont; + TaskThunk(AsyncContext asyncContext, HttpServletRequest req) { + this.asyncContext = asyncContext; this.name = generateName(req); } @Override public void run() { - cont.resume(); + resume(); synchronized (lock) { while (!done) { @@ -212,8 +280,16 @@ @Override public void cancel() { - cont.setAttribute(CANCEL, Boolean.TRUE); - cont.resume(); + RequestState.CANCELED.set(asyncContext.getRequest()); + resume(); + } + + private void resume() { + ServletRequest req = asyncContext.getRequest(); + if (RequestState.SUSPENDED.equals(RequestState.get(req))) { + RequestState.RESUMED.set(req); + asyncContext.dispatch(); + } } @Override
diff --git a/java/com/google/gerrit/prettify/BUILD b/java/com/google/gerrit/prettify/BUILD index 7c1241a..0a15fda 100644 --- a/java/com/google/gerrit/prettify/BUILD +++ b/java/com/google/gerrit/prettify/BUILD
@@ -7,5 +7,7 @@ deps = [ "//lib:guava", "//lib:jgit", + "//lib/auto:auto-value", + "//lib/auto:auto-value-annotations", ], )
diff --git a/java/com/google/gerrit/prettify/common/SparseFileContent.java b/java/com/google/gerrit/prettify/common/SparseFileContent.java index 348f9b2..1249b65 100644 --- a/java/com/google/gerrit/prettify/common/SparseFileContent.java +++ b/java/com/google/gerrit/prettify/common/SparseFileContent.java
@@ -14,160 +14,175 @@ package com.google.gerrit.prettify.common; -import java.util.ArrayList; -import java.util.List; +import com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; -public class SparseFileContent { - protected List<Range> ranges; - protected int size; +/** + * A class to store subset of a file's lines in a memory efficient way. Internally, it stores lines + * as a list of ranges. Each range represents continuous set of lines and has information about line + * numbers in original file (zero-based). + * + * <p>{@link SparseFileContent.Accessor} must be used to work with the stored content. + */ +@AutoValue +public abstract class SparseFileContent { + abstract ImmutableList<Range> getRanges(); - private transient int currentRangeIdx; + public abstract int getSize(); - public SparseFileContent() { - ranges = new ArrayList<>(); + public static SparseFileContent create(ImmutableList<Range> ranges, int size) { + return new AutoValue_SparseFileContent(ranges, size); } - public int size() { - return size; + @VisibleForTesting + public int getRangesCount() { + return getRanges().size(); } - public void setSize(int s) { - size = s; + public Accessor createAccessor() { + return new Accessor(this); } - public String get(int idx) { - final String line = getLine(idx); - if (line == null) { - throw new ArrayIndexOutOfBoundsException(idx); - } - return line; - } + /** + * Provide a methods to work with the content of a {@link SparseFileContent}. + * + * <p>The class hides internal representation of a {@link SparseFileContent} and provides + * convenient way for accessing a content. + */ + public static class Accessor { + private final SparseFileContent content; + private int currentRangeIdx; - public boolean contains(int idx) { - return getLine(idx) != null; - } - - public int first() { - return ranges.isEmpty() ? size() : ranges.get(0).base; - } - - public int next(int idx) { - // Most requests are sequential in nature, fetching the next - // line from the current range, or the immediate next range. - // - int high = ranges.size(); - if (currentRangeIdx < high) { - Range cur = ranges.get(currentRangeIdx); - if (cur.contains(idx + 1)) { - return idx + 1; - } - - if (++currentRangeIdx < high) { - // Its not plus one, its the base of the next range. - // - return ranges.get(currentRangeIdx).base; - } + private Accessor(SparseFileContent content) { + this.content = content; } - // Binary search for the current value, since we know its a sorted list. - // - int low = 0; - do { - final int mid = (low + high) / 2; - final Range cur = ranges.get(mid); + public String get(int idx) { + final String line = getLine(idx); + if (line == null) { + throw new ArrayIndexOutOfBoundsException(idx); + } + return line; + } - if (cur.contains(idx)) { + public int getSize() { + return content.getSize(); + } + + public boolean contains(int idx) { + return getLine(idx) != null; + } + + public int first() { + return content.getRanges().isEmpty() ? getSize() : content.getRanges().get(0).getBase(); + } + + public int next(int idx) { + // Most requests are sequential in nature, fetching the next + // line from the current range, or the immediate next range. + // + ImmutableList<Range> ranges = content.getRanges(); + int high = ranges.size(); + if (currentRangeIdx < high) { + Range cur = ranges.get(currentRangeIdx); if (cur.contains(idx + 1)) { - // Trivial plus one case above failed due to wrong currentRangeIdx. - // Reset the cache so we don't miss in the future. - // - currentRangeIdx = mid; return idx + 1; } - if (mid + 1 < ranges.size()) { - // Its the base of the next range. - currentRangeIdx = mid + 1; - return ranges.get(currentRangeIdx).base; - } - - // No more lines in the file. - // - return size(); - } - - if (idx < cur.base) { - high = mid; - } else { - low = mid + 1; - } - } while (low < high); - - return size(); - } - - private String getLine(int idx) { - // Most requests are sequential in nature, fetching the next - // line from the current range, or the next range. - // - int high = ranges.size(); - if (currentRangeIdx < high) { - Range cur = ranges.get(currentRangeIdx); - if (cur.contains(idx)) { - return cur.get(idx); - } - - if (++currentRangeIdx < high) { - final Range next = ranges.get(currentRangeIdx); - if (next.contains(idx)) { - return next.get(idx); + if (++currentRangeIdx < high) { + // Its not plus one, its the base of the next range. + // + return ranges.get(currentRangeIdx).getBase(); } } + + // Binary search for the current value, since we know its a sorted list. + // + int low = 0; + do { + final int mid = (low + high) / 2; + final Range cur = ranges.get(mid); + + if (cur.contains(idx)) { + if (cur.contains(idx + 1)) { + // Trivial plus one case above failed due to wrong currentRangeIdx. + // Reset the cache so we don't miss in the future. + // + currentRangeIdx = mid; + return idx + 1; + } + + if (mid + 1 < ranges.size()) { + // Its the base of the next range. + currentRangeIdx = mid + 1; + return ranges.get(currentRangeIdx).getBase(); + } + + // No more lines in the file. + // + return getSize(); + } + + if (idx < cur.getBase()) { + high = mid; + } else { + low = mid + 1; + } + } while (low < high); + + return getSize(); } - // Binary search for the range, since we know its a sorted list. - // - if (ranges.isEmpty()) { + private String getLine(int idx) { + // Most requests are sequential in nature, fetching the next + // line from the current range, or the next range. + // + ImmutableList<Range> ranges = content.getRanges(); + int high = ranges.size(); + if (currentRangeIdx < high) { + Range cur = ranges.get(currentRangeIdx); + if (cur.contains(idx)) { + return cur.get(idx); + } + + if (++currentRangeIdx < high) { + final Range next = ranges.get(currentRangeIdx); + if (next.contains(idx)) { + return next.get(idx); + } + } + } + + // Binary search for the range, since we know its a sorted list. + // + if (ranges.isEmpty()) { + return null; + } + + int low = 0; + do { + final int mid = (low + high) / 2; + final Range cur = ranges.get(mid); + if (cur.contains(idx)) { + currentRangeIdx = mid; + return cur.get(idx); + } + if (idx < cur.getBase()) { + high = mid; + } else { + low = mid + 1; + } + } while (low < high); return null; } - - int low = 0; - do { - final int mid = (low + high) / 2; - final Range cur = ranges.get(mid); - if (cur.contains(idx)) { - currentRangeIdx = mid; - return cur.get(idx); - } - if (idx < cur.base) { - high = mid; - } else { - low = mid + 1; - } - } while (low < high); - return null; - } - - public void addLine(int i, String content) { - final Range r; - if (!ranges.isEmpty() && i == last().end()) { - r = last(); - } else { - r = new Range(i); - ranges.add(r); - } - r.lines.add(content); - } - - private Range last() { - return ranges.get(ranges.size() - 1); } @Override - public String toString() { + public final String toString() { final StringBuilder b = new StringBuilder(); b.append("SparseFileContent[\n"); - for (Range r : ranges) { + for (Range r : getRanges()) { b.append(" "); b.append(r.toString()); b.append('\n'); @@ -176,33 +191,32 @@ return b.toString(); } - static class Range { - protected int base; - protected List<String> lines; - - private Range(int b) { - base = b; - lines = new ArrayList<>(); + @AutoValue + abstract static class Range { + static Range create(int base, ImmutableList<String> lines) { + return new AutoValue_SparseFileContent_Range(base, lines); } - protected Range() {} + abstract int getBase(); + + abstract ImmutableList<String> getLines(); private String get(int i) { - return lines.get(i - base); + return getLines().get(i - getBase()); } private int end() { - return base + lines.size(); + return getBase() + getLines().size(); } private boolean contains(int i) { - return base <= i && i < end(); + return getBase() <= i && i < end(); } @Override - public String toString() { + public final String toString() { // Usage of [ and ) is intentional to denote inclusive/exclusive range - return "Range[" + base + "," + end() + ")"; + return "Range[" + getBase() + "," + end() + ")"; } } }
diff --git a/java/com/google/gerrit/prettify/common/SparseFileContentBuilder.java b/java/com/google/gerrit/prettify/common/SparseFileContentBuilder.java new file mode 100644 index 0000000..04fb5d1 --- /dev/null +++ b/java/com/google/gerrit/prettify/common/SparseFileContentBuilder.java
@@ -0,0 +1,90 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.prettify.common; + +import com.google.common.collect.ImmutableList; +import com.google.gerrit.prettify.common.SparseFileContent.Range; + +/** + * A builder for creating immutable {@link SparseFileContent}. Lines can be only be added in + * sequential (increased) order + */ +public class SparseFileContentBuilder { + private final ImmutableList.Builder<Range> ranges; + private final int size; + private int lastRangeBase; + private int lastRangeEnd; + private ImmutableList.Builder<String> lastRangeLines; + + public SparseFileContentBuilder(int size) { + ranges = new ImmutableList.Builder<>(); + startNextRange(0); + this.size = size; + } + + public void addLine(int lineNumber, String content) { + if (lineNumber < 0) { + throw new IllegalArgumentException("Line number must be non-negative"); + } + // if (lineNumber >= size) { + // The following 4 tests are failed if you uncomment this condition: + // + // + // diffOfFileWithMultilineRebaseHunkRemovingNewlineAtEndOfFileAndWithCommentReturnsFileContents + // + // diffOfFileWithMultilineRebaseHunkAddingNewlineAtEndOfFileAndWithCommentReturnsFileContents + // + // + // diffOfFileWithMultilineRebaseHunkRemovingNewlineAtEndOfFileAndWithCommentReturnsFileContents + // + // diffOfFileWithMultilineRebaseHunkAddingNewlineAtEndOfFileAndWithCommentReturnsFileContents + // Tests are failed because there are some bug with diff calculation. + // The condition must be uncommented after all these bugs are fixed. + // Also don't forget to remove ignore from for SparseFileContentBuilder + // throw new IllegalArgumentException(String.format("The zero-based line number %d is after + // the end of file. The file size is %d line(s).", lineNumber, size)); + // } + if (lineNumber < lastRangeEnd) { + throw new IllegalArgumentException( + String.format( + "Invalid line number %d. You are trying to add a line before an already added line" + + " %d", + lineNumber, lastRangeEnd)); + } + if (lineNumber > lastRangeEnd) { + finishLastRange(); + startNextRange(lineNumber); + } + lastRangeLines.add(content); + lastRangeEnd++; + } + + private void startNextRange(int base) { + lastRangeLines = new ImmutableList.Builder<>(); + lastRangeBase = lastRangeEnd = base; + } + + private void finishLastRange() { + if (lastRangeEnd > lastRangeBase) { + ranges.add(Range.create(lastRangeBase, lastRangeLines.build())); + lastRangeLines = null; + } + } + + public SparseFileContent build() { + finishLastRange(); + return SparseFileContent.create(ranges.build(), size); + } +}
diff --git a/java/com/google/gerrit/prettify/common/testing/BUILD b/java/com/google/gerrit/prettify/common/testing/BUILD new file mode 100644 index 0000000..5057fdb --- /dev/null +++ b/java/com/google/gerrit/prettify/common/testing/BUILD
@@ -0,0 +1,14 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package(default_testonly = True) + +java_library( + name = "testing", + srcs = glob(["*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//java/com/google/gerrit/prettify:server", + "//lib:guava", + "//lib/truth", + ], +)
diff --git a/java/com/google/gerrit/prettify/common/testing/SparseFileContentSubject.java b/java/com/google/gerrit/prettify/common/testing/SparseFileContentSubject.java new file mode 100644 index 0000000..c1fe1ec --- /dev/null +++ b/java/com/google/gerrit/prettify/common/testing/SparseFileContentSubject.java
@@ -0,0 +1,65 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.prettify.common.testing; + +import static com.google.common.truth.Truth.assertAbout; + +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.IntegerSubject; +import com.google.common.truth.MapSubject; +import com.google.common.truth.Subject; +import com.google.gerrit.prettify.common.SparseFileContent; +import java.util.HashMap; +import java.util.Map; + +public class SparseFileContentSubject extends Subject { + public static SparseFileContentSubject assertThat(SparseFileContent sparseFileContent) { + return assertAbout(sparseFileContent()).that(sparseFileContent); + } + + private final SparseFileContent sparseFileContent; + + private SparseFileContentSubject(FailureMetadata metadata, SparseFileContent actual) { + super(metadata, actual); + this.sparseFileContent = actual; + } + + private static Subject.Factory<SparseFileContentSubject, SparseFileContent> sparseFileContent() { + return SparseFileContentSubject::new; + } + + public IntegerSubject getSize() { + isNotNull(); + return check("size()").that(sparseFileContent.getSize()); + } + + public IntegerSubject getRangesCount() { + isNotNull(); + return check("rangesCount()").that(sparseFileContent.getRangesCount()); + } + + public MapSubject lines() { + isNotNull(); + Map<Integer, String> lines = new HashMap<>(); + SparseFileContent.Accessor accessor = sparseFileContent.createAccessor(); + int size = accessor.getSize(); + int idx = accessor.first(); + while (idx < size) { + lines.put(idx, accessor.get(idx)); + idx = accessor.next(idx); + } + return check("lines()").that(lines); + } +}
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD index 6675595..45d037a 100644 --- a/java/com/google/gerrit/server/BUILD +++ b/java/com/google/gerrit/server/BUILD
@@ -54,6 +54,7 @@ "//java/com/google/gerrit/prettify:server", "//java/com/google/gerrit/proto", "//java/com/google/gerrit/server/cache/serialize", + "//java/com/google/gerrit/server/git/receive:ref_cache", "//java/com/google/gerrit/server/ioutil", "//java/com/google/gerrit/server/logging", "//java/com/google/gerrit/server/util/git", @@ -106,12 +107,12 @@ "//lib/auto:auto-value-annotations", "//lib/bouncycastle:bcpkix-neverlink", "//lib/bouncycastle:bcprov-neverlink", - "//lib/commons:codec", "//lib/commons:compress", "//lib/commons:dbcp", "//lib/commons:lang", "//lib/commons:net", "//lib/commons:validator", + "//lib/errorprone:annotations", "//lib/flogger:api", "//lib/guice", "//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/server/ChangeUtil.java b/java/com/google/gerrit/server/ChangeUtil.java index ee82a26..a166d97 100644 --- a/java/com/google/gerrit/server/ChangeUtil.java +++ b/java/com/google/gerrit/server/ChangeUtil.java
@@ -25,7 +25,6 @@ import java.io.IOException; import java.security.SecureRandom; import java.util.Collection; -import java.util.Map; import java.util.Random; import java.util.Set; import java.util.stream.Stream; @@ -50,22 +49,6 @@ } /** - * Get the next patch set ID from a previously-read map of all refs. - * - * @param allRefs map of full ref name to ref. - * @param id previous patch set ID. - * @return next unused patch set ID for the same change, skipping any IDs whose corresponding ref - * names appear in the {@code allRefs} map. - */ - public static PatchSet.Id nextPatchSetIdFromAllRefsMap(Map<String, Ref> allRefs, PatchSet.Id id) { - PatchSet.Id next = nextPatchSetId(id); - while (allRefs.containsKey(next.toRefName())) { - next = nextPatchSetId(next); - } - return next; - } - - /** * Get the next patch set ID from a previously-read map of refs below the change prefix. * * @param changeRefNames existing full change ref names with the same change ID as {@code id}. @@ -95,9 +78,7 @@ /** * Get the next patch set ID just looking at a single previous patch set ID. * - * <p>This patch set ID may or may not be available in the database; callers that want a - * previously-unused ID should use {@link #nextPatchSetIdFromAllRefsMap} or {@link - * #nextPatchSetIdFromChangeRefs}. + * <p>This patch set ID may or may not be available in the database. * * @param id previous patch set ID. * @return next patch set ID for the same change, incrementing by 1.
diff --git a/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java b/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java index 996257c..5e7919f 100644 --- a/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java +++ b/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java
@@ -115,11 +115,12 @@ .collect(toList())); config.replace(createGroupAccessSection); } else { - Permission createGroupPermission = new Permission(Permission.CREATE); - createGroupAccessSection.addPermission(createGroupPermission); - createGroupsGlobal.forEach(createGroupPermission::add); // The create permission is managed by Gerrit at this point only so there is no concern of // overwriting user-defined permissions here. + Permission createGroupPermission = new Permission(Permission.CREATE); + createGroupAccessSection.remove(createGroupPermission); + createGroupAccessSection.addPermission(createGroupPermission); + createGroupsGlobal.forEach(createGroupPermission::add); config.replace(createGroupAccessSection); }
diff --git a/java/com/google/gerrit/server/ExceptionHook.java b/java/com/google/gerrit/server/ExceptionHook.java index a0d98d2..7a1bdde 100644 --- a/java/com/google/gerrit/server/ExceptionHook.java +++ b/java/com/google/gerrit/server/ExceptionHook.java
@@ -15,6 +15,7 @@ package com.google.gerrit.server; import com.google.gerrit.extensions.annotations.ExtensionPoint; +import java.util.Optional; /** * Allows implementors to control how certain exceptions should be handled. @@ -33,10 +34,95 @@ * <p>Only affects operations that are executed with {@link * com.google.gerrit.server.update.RetryHelper}. * + * <p>Should return {@code true} only for exceptions that are caused by temporary issues where a + * retry of the operation has a chance to succeed. + * + * <p>If {@code false} is returned the operation is still retried once to capture a trace, unless + * {@link #skipRetryWithTrace(String, String, Throwable)} skips the auto-retry. + * * @param throwable throwable that was thrown while executing the operation + * @param actionType the type of the action for which the exception occurred + * @param actionName the name of the action for which the exception occurred * @return whether the operation should be retried */ - default boolean shouldRetry(Throwable throwable) { + default boolean shouldRetry(String actionType, String actionName, Throwable throwable) { return false; } + + /** + * Whether auto-retrying of an operation with tracing should be skipped for the given throwable. + * + * <p>Only affects operations that are executed with {@link + * com.google.gerrit.server.update.RetryHelper}. + * + * <p>This method is only called for exceptions for which the operation should not be retried + * ({@link #shouldRetry(String, String, Throwable)} returned {@code false}). + * + * <p>By default this method returns {@code false}, so that by default traces for unexpected + * exceptions are captured, which allows to investigate them. + * + * <p>Implementors may use this method to skip retry with tracing for exceptions that occur due to + * known causes that are permanent and where a trace is not needed for the investigation. For + * example, if an operation fails because persisted data is corrupt, it makes no sense to retry + * the operation with a trace, because the trace will not help with fixing the corrupt data. + * + * <p>This method is only invoked if retry with tracing is enabled on the server ({@code + * retry.retryWithTraceOnFailure} in {@code gerrit.config} is set to {@code true}). + * + * @param throwable throwable that was thrown while executing the operation + * @param actionType the type of the action for which the exception occurred + * @param actionName the name of the action for which the exception occurred + * @return whether auto-retrying of an operation with tracing should be skipped for the given + * throwable + */ + default boolean skipRetryWithTrace(String actionType, String actionName, Throwable throwable) { + return false; + } + + /** + * Formats the cause of an exception for use in metrics. + * + * <p>This method allows implementors to group exceptions that have the same cause into one metric + * bucket. + * + * @param throwable the exception cause + * @return formatted cause or {@link Optional#empty()} if no formatting was done + */ + default Optional<String> formatCause(Throwable throwable) { + return Optional.empty(); + } + + /** + * Returns a message that should be returned to the user. + * + * <p>This message is included into the HTTP response that is sent to the user. + * + * @param throwable throwable that was thrown while executing an operation + * @return error message that should be returned to the user, {@link Optional#empty()} if no + * message should be returned to the user + */ + default Optional<String> getUserMessage(Throwable throwable) { + return Optional.empty(); + } + + /** + * Returns the HTTP status code that should be returned to the user. + * + * <p>If no value is returned ({@link Optional#empty()}) the HTTP status code defaults to {@code + * 500 Internal Server Error}. + * + * <p>{@link #getUserMessage(Throwable)} allows to define which message should be included into + * the body of the HTTP response. + * + * <p>Implementors may use this method to change the status code for certain exceptions (e.g. + * using this method it would be possible to return {@code 409 Conflict} for {@link + * com.google.gerrit.git.LockFailureException}s instead of {@code 500 Internal Server Error}). + * + * @param throwable throwable that was thrown while executing an operation + * @return HTTP status code that should be returned to the user, {@link Optional#empty()} if the + * exception should result in {@code 500 Internal Server Error} + */ + default Optional<Integer> getStatusCode(Throwable throwable) { + return Optional.empty(); + } }
diff --git a/java/com/google/gerrit/server/ExceptionHookImpl.java b/java/com/google/gerrit/server/ExceptionHookImpl.java new file mode 100644 index 0000000..eabb914 --- /dev/null +++ b/java/com/google/gerrit/server/ExceptionHookImpl.java
@@ -0,0 +1,68 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server; + +import com.google.common.base.Predicate; +import com.google.common.base.Throwables; +import com.google.gerrit.git.LockFailureException; +import java.util.Optional; +import org.eclipse.jgit.lib.RefUpdate; + +/** + * Class to detect and handle exceptions that are caused by temporary errors, and hence should cause + * a retry of the failed operation. + */ +public class ExceptionHookImpl implements ExceptionHook { + private static final String LOCK_FAILURE_USER_MESSAGE = + "Updating a ref failed with LOCK_FAILURE.\n" + + "This may be a temporary issue due to concurrent updates.\n" + + "Please retry later."; + + @Override + public boolean shouldRetry(String actionType, String actionName, Throwable throwable) { + return isLockFailure(throwable); + } + + @Override + public Optional<String> formatCause(Throwable throwable) { + if (isLockFailure(throwable)) { + return Optional.of(RefUpdate.Result.LOCK_FAILURE.name()); + } + return Optional.empty(); + } + + @Override + public Optional<String> getUserMessage(Throwable throwable) { + if (isLockFailure(throwable)) { + return Optional.of(LOCK_FAILURE_USER_MESSAGE); + } + return Optional.empty(); + } + + private static boolean isLockFailure(Throwable throwable) { + return isMatching(throwable, t -> t instanceof LockFailureException); + } + + /** + * Check whether the given exception or any of its causes matches the given predicate. + * + * @param throwable Exception that should be tested + * @param predicate predicate to check if a throwable matches + * @return {@code true} if the given exception or any of its causes matches the given predicate + */ + private static boolean isMatching(Throwable throwable, Predicate<Throwable> predicate) { + return Throwables.getCausalChain(throwable).stream().anyMatch(predicate); + } +}
diff --git a/java/com/google/gerrit/server/PublishCommentUtil.java b/java/com/google/gerrit/server/PublishCommentUtil.java index 26539c5..f1c2f82 100644 --- a/java/com/google/gerrit/server/PublishCommentUtil.java +++ b/java/com/google/gerrit/server/PublishCommentUtil.java
@@ -15,12 +15,13 @@ package com.google.gerrit.server; import static com.google.common.base.Preconditions.checkArgument; -import static com.google.gerrit.entities.Comment.Status; import static java.util.stream.Collectors.toSet; import com.google.common.collect.ImmutableList; +import com.google.common.flogger.FluentLogger; import com.google.gerrit.common.Nullable; import com.google.gerrit.entities.Comment; +import com.google.gerrit.entities.Comment.Status; import com.google.gerrit.entities.PatchSet; import com.google.gerrit.exceptions.StorageException; import com.google.gerrit.extensions.validators.CommentForValidation; @@ -34,10 +35,14 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import java.util.Collection; +import java.util.HashSet; import java.util.Map; +import java.util.Set; @Singleton public class PublishCommentUtil { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private final PatchListCache patchListCache; private final PatchSetUtil psUtil; private final CommentsUtil commentsUtil; @@ -63,11 +68,32 @@ Map<PatchSet.Id, PatchSet> patchSets = psUtil.getAsMap(notes, draftComments.stream().map(d -> psId(notes, d)).collect(toSet())); + Set<Comment> commentsToPublish = new HashSet<>(); for (Comment draftComment : draftComments) { PatchSet.Id psIdOfDraftComment = psId(notes, draftComment); PatchSet ps = patchSets.get(psIdOfDraftComment); if (ps == null) { - throw new StorageException("patch set " + psIdOfDraftComment + " not found"); + // This can happen if changes with the same numeric ID exist: + // - change 12345 has 3 patch sets in repo X + // - another change 12345 has 7 patch sets in repo Y + // - the user saves a draft comment on patch set 6 of the change in repo Y + // - this draft comment gets stored in: + // AllUsers -> refs/draft-comments/45/12345/<account-id> + // - when posting a review with draft handling PUBLISH_ALL_REVISIONS on the change in + // repo X, the draft comments are loaded from + // AllUsers -> refs/draft-comments/45/12345/<account-id>, including the draft + // comment that was saved for patch set 6 of the change in repo Y + // - patch set 6 does not exist for the change in repo x, hence we get null for the patch + // set here + // Instead of failing hard (and returning an Internal Server Error) to the caller, + // just ignore that comment. + // Gerrit ensures that numeric change IDs are unique, but you can get duplicates if + // change refs of one repo are copied/pushed to another repo on the same host (this + // should never be done, but we know it happens). + logger.atWarning().log( + "Ignoring draft comment %s on non existing patch set %s (repo = %s)", + draftComment, psIdOfDraftComment, notes.getProjectName()); + continue; } draftComment.writtenOn = ctx.getWhen(); draftComment.tag = tag; @@ -79,8 +105,9 @@ } catch (PatchListNotAvailableException e) { throw new StorageException(e); } + commentsToPublish.add(draftComment); } - commentsUtil.putComments(ctx.getUpdate(psId), Status.PUBLISHED, draftComments); + commentsUtil.putComments(ctx.getUpdate(psId), Status.PUBLISHED, commentsToPublish); } private static PatchSet.Id psId(ChangeNotes notes, Comment c) { @@ -98,8 +125,8 @@ ImmutableList.Builder<CommentValidationFailure> commentValidationFailures = new ImmutableList.Builder<>(); commentValidators.runEach( - listener -> - commentValidationFailures.addAll(listener.validateComments(commentsForValidation))); + validator -> + commentValidationFailures.addAll(validator.validateComments(commentsForValidation))); return commentValidationFailures.build(); } }
diff --git a/java/com/google/gerrit/server/RequestListener.java b/java/com/google/gerrit/server/RequestListener.java index 461b91a..b158631 100644 --- a/java/com/google/gerrit/server/RequestListener.java +++ b/java/com/google/gerrit/server/RequestListener.java
@@ -16,6 +16,12 @@ import com.google.gerrit.extensions.annotations.ExtensionPoint; +/** + * Extension point that allows to listen to incoming requests. + * + * <p>This extension point is invoked each time the server executes a request from a user (REST + * request, SSH request, Git push/fetch). + */ @ExtensionPoint public interface RequestListener { void onRequest(RequestInfo requestInfo);
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java index 5824240..93cf0de 100644 --- a/java/com/google/gerrit/server/StarredChangesUtil.java +++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -479,8 +479,10 @@ case FAST_FORWARD: gitRefUpdated.fire(allUsers, u, null); return; - case IO_FAILURE: case LOCK_FAILURE: + throw new LockFailureException( + String.format("Update star labels on ref %s failed", refName), u); + case IO_FAILURE: case NOT_ATTEMPTED: case REJECTED: case REJECTED_CURRENT_BRANCH: @@ -513,11 +515,12 @@ case FORCED: gitRefUpdated.fire(allUsers, u, null); return; + case LOCK_FAILURE: + throw new LockFailureException(String.format("Delete star ref %s failed", refName), u); case NEW: case NO_CHANGE: case FAST_FORWARD: case IO_FAILURE: - case LOCK_FAILURE: case NOT_ATTEMPTED: case REJECTED: case REJECTED_CURRENT_BRANCH:
diff --git a/java/com/google/gerrit/server/account/AccountResolver.java b/java/com/google/gerrit/server/account/AccountResolver.java index 988d871..a41a36c 100644 --- a/java/com/google/gerrit/server/account/AccountResolver.java +++ b/java/com/google/gerrit/server/account/AccountResolver.java
@@ -516,12 +516,22 @@ * @throws IOException if an error occurs. */ public Result resolve(String input) throws ConfigInvalidException, IOException { - return searchImpl(input, searchers, visibilitySupplier(), accountActivityPredicate()); + return searchImpl(input, searchers, visibilitySupplierCanSee(), accountActivityPredicate()); } public Result resolve(String input, Predicate<AccountState> accountActivityPredicate) throws ConfigInvalidException, IOException { - return searchImpl(input, searchers, visibilitySupplier(), accountActivityPredicate); + return searchImpl(input, searchers, visibilitySupplierCanSee(), accountActivityPredicate); + } + + public Result resolveIgnoreVisibility(String input) throws ConfigInvalidException, IOException { + return searchImpl(input, searchers, visibilitySupplierAll(), accountActivityPredicate()); + } + + public Result resolveIgnoreVisibility( + String input, Predicate<AccountState> accountActivityPredicate) + throws ConfigInvalidException, IOException { + return searchImpl(input, searchers, visibilitySupplierAll(), accountActivityPredicate); } /** @@ -550,13 +560,23 @@ @Deprecated public Result resolveByNameOrEmail(String input) throws ConfigInvalidException, IOException { return searchImpl( - input, nameOrEmailSearchers, visibilitySupplier(), accountActivityPredicate()); + input, nameOrEmailSearchers, visibilitySupplierCanSee(), accountActivityPredicate()); } - private Supplier<Predicate<AccountState>> visibilitySupplier() { + private Supplier<Predicate<AccountState>> visibilitySupplierCanSee() { return () -> accountControlFactory.get()::canSee; } + private Supplier<Predicate<AccountState>> visibilitySupplierAll() { + return () -> all(); + } + + private Predicate<AccountState> all() { + return accountState -> { + return true; + }; + } + private Predicate<AccountState> accountActivityPredicate() { return (AccountState accountState) -> accountState.account().isActive(); }
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java index 1caee58..125edfe 100644 --- a/java/com/google/gerrit/server/account/AccountsUpdate.java +++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -41,8 +41,7 @@ import com.google.gerrit.server.index.change.ReindexAfterRefUpdate; import com.google.gerrit.server.notedb.Sequences; import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryHelper.Action; -import com.google.gerrit.server.update.RetryHelper.ActionType; +import com.google.gerrit.server.update.RetryableAction.Action; import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; @@ -421,8 +420,7 @@ private Optional<AccountState> executeAccountUpdate(Action<Optional<AccountState>> action) throws IOException, ConfigInvalidException { try { - return retryHelper.execute( - ActionType.ACCOUNT_UPDATE, action, LockFailureException.class::isInstance); + return retryHelper.accountUpdate("updateAccount", action).call(); } catch (Exception e) { Throwables.throwIfUnchecked(e); Throwables.throwIfInstanceOf(e, IOException.class);
diff --git a/java/com/google/gerrit/server/account/Emails.java b/java/com/google/gerrit/server/account/Emails.java index 76c22cf..98d0d50 100644 --- a/java/com/google/gerrit/server/account/Emails.java +++ b/java/com/google/gerrit/server/account/Emails.java
@@ -17,22 +17,16 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableSet.toImmutableSet; -import com.google.common.base.Throwables; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.MultimapBuilder; import com.google.common.collect.SetMultimap; import com.google.gerrit.entities.Account; import com.google.gerrit.entities.UserIdentity; -import com.google.gerrit.exceptions.StorageException; import com.google.gerrit.server.account.externalids.ExternalId; import com.google.gerrit.server.account.externalids.ExternalIds; -import com.google.gerrit.server.query.account.InternalAccountQuery; import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryHelper.Action; -import com.google.gerrit.server.update.RetryHelper.ActionType; import com.google.inject.Inject; -import com.google.inject.Provider; import com.google.inject.Singleton; import java.io.IOException; import java.sql.Timestamp; @@ -45,16 +39,11 @@ @Singleton public class Emails { private final ExternalIds externalIds; - private final Provider<InternalAccountQuery> queryProvider; private final RetryHelper retryHelper; @Inject - public Emails( - ExternalIds externalIds, - Provider<InternalAccountQuery> queryProvider, - RetryHelper retryHelper) { + public Emails(ExternalIds externalIds, RetryHelper retryHelper) { this.externalIds = externalIds; - this.queryProvider = queryProvider; this.retryHelper = retryHelper; } @@ -85,7 +74,9 @@ return accounts; } - return executeIndexQuery(() -> queryProvider.get().byPreferredEmail(email).stream()) + return retryHelper + .accountIndexQuery("queryAccountsByPreferredEmail", q -> q.byPreferredEmail(email)).call() + .stream() .map(a -> a.account().id()) .collect(toImmutableSet()); } @@ -104,8 +95,10 @@ List<String> emailsToBackfill = Arrays.stream(emails).filter(e -> !result.containsKey(e)).collect(toImmutableList()); if (!emailsToBackfill.isEmpty()) { - executeIndexQuery( - () -> queryProvider.get().byPreferredEmail(emailsToBackfill).entries().stream()) + retryHelper + .accountIndexQuery( + "queryAccountsByPreferredEmails", q -> q.byPreferredEmail(emailsToBackfill)) + .call().entries().stream() .forEach(e -> result.put(e.getKey(), e.getValue().account().id())); } return ImmutableSetMultimap.copyOf(result); @@ -138,14 +131,4 @@ return u; } - - private <T> T executeIndexQuery(Action<T> action) { - try { - return retryHelper.execute( - ActionType.INDEX_QUERY, action, StorageException.class::isInstance); - } catch (Exception e) { - Throwables.throwIfUnchecked(e); - throw new StorageException(e); - } - } }
diff --git a/java/com/google/gerrit/server/account/Realm.java b/java/com/google/gerrit/server/account/Realm.java index 798b4e8..d56ed07 100644 --- a/java/com/google/gerrit/server/account/Realm.java +++ b/java/com/google/gerrit/server/account/Realm.java
@@ -24,6 +24,12 @@ import javax.naming.NamingException; import javax.security.auth.login.LoginException; +/** + * Interface between Gerrit and an account system. + * + * <p>This interface provides the glue layer between the Gerrit and external account/authentication + * systems (eg. LDAP, OpenID). + */ public interface Realm { /** Can the end-user modify this field of their own account? */ boolean allowsEdit(AccountFieldName field);
diff --git a/java/com/google/gerrit/server/account/SetInactiveFlag.java b/java/com/google/gerrit/server/account/SetInactiveFlag.java index fb3d4ea..32ed694 100644 --- a/java/com/google/gerrit/server/account/SetInactiveFlag.java +++ b/java/com/google/gerrit/server/account/SetInactiveFlag.java
@@ -32,6 +32,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jgit.errors.ConfigInvalidException; +/** Toggler for account active state. */ @Singleton public class SetInactiveFlag { private final PluginSetContext<AccountActivationValidationListener> @@ -106,6 +107,6 @@ if (exception.get().isPresent()) { throw exception.get().get(); } - return alreadyActive.get() ? Response.ok("") : Response.created(""); + return alreadyActive.get() ? Response.ok() : Response.created(); } }
diff --git a/java/com/google/gerrit/server/account/VersionedAccountQueries.java b/java/com/google/gerrit/server/account/VersionedAccountQueries.java index e2ffe9b..5e63875 100644 --- a/java/com/google/gerrit/server/account/VersionedAccountQueries.java +++ b/java/com/google/gerrit/server/account/VersionedAccountQueries.java
@@ -28,7 +28,12 @@ import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.CommitBuilder; -/** Named Queries for user accounts. */ +/** + * Named Queries for user accounts. + * + * <p>Users can define aliases for change queries. These are stored as versioned account data and + * (de)serialized with this class. + */ public class VersionedAccountQueries extends VersionedMetaData { private static final FluentLogger logger = FluentLogger.forEnclosingClass();
diff --git a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java index a04be30..0d640d9 100644 --- a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java +++ b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -48,8 +48,10 @@ import com.google.gerrit.extensions.common.CommentInfo; import com.google.gerrit.extensions.common.CommitMessageInput; import com.google.gerrit.extensions.common.Input; +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.RevertSubmissionInfo; import com.google.gerrit.extensions.common.RobotCommentInfo; import com.google.gerrit.extensions.common.SuggestedReviewerInfo; import com.google.gerrit.extensions.registration.DynamicMap; @@ -62,7 +64,6 @@ import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException; import com.google.gerrit.server.change.ChangeMessageResource; import com.google.gerrit.server.change.ChangeResource; -import com.google.gerrit.server.change.SetPrivateOp; import com.google.gerrit.server.change.WorkInProgressOp; import com.google.gerrit.server.restapi.change.Abandon; import com.google.gerrit.server.restapi.change.ChangeIncludedIn; @@ -96,6 +97,7 @@ import com.google.gerrit.server.restapi.change.Rebase; import com.google.gerrit.server.restapi.change.Restore; import com.google.gerrit.server.restapi.change.Revert; +import com.google.gerrit.server.restapi.change.RevertSubmission; import com.google.gerrit.server.restapi.change.Reviewers; import com.google.gerrit.server.restapi.change.Revisions; import com.google.gerrit.server.restapi.change.SetReadyForReview; @@ -132,6 +134,7 @@ private final ChangeResource change; private final Abandon abandon; private final Revert revert; + private final RevertSubmission revertSubmission; private final Restore restore; private final CreateMergePatchSet updateByMerge; private final Provider<SubmittedTogether> submittedTogether; @@ -181,6 +184,7 @@ ListReviewers listReviewers, Abandon abandon, Revert revert, + RevertSubmission revertSubmission, Restore restore, CreateMergePatchSet updateByMerge, Provider<SubmittedTogether> submittedTogether, @@ -219,6 +223,7 @@ @Assisted ChangeResource change) { this.changeApi = changeApi; this.revert = revert; + this.revertSubmission = revertSubmission; this.reviewers = reviewers; this.revisions = revisions; this.reviewerApi = reviewerApi; @@ -319,7 +324,7 @@ @Override public void setPrivate(boolean value, @Nullable String message) throws RestApiException { try { - SetPrivateOp.Input input = new SetPrivateOp.Input(message); + InputWithMessage input = new InputWithMessage(message); if (value) { postPrivate.apply(change, input); } else { @@ -358,6 +363,15 @@ } @Override + public RevertSubmissionInfo revertSubmission(RevertInput in) throws RestApiException { + try { + return revertSubmission.apply(change, in).value(); + } catch (Exception e) { + throw asRestApiException("Cannot revert a change submission", e); + } + } + + @Override public ChangeInfo createMergePatchSet(MergePatchSetInput in) throws RestApiException { try { return updateByMerge.apply(change, in).value();
diff --git a/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java index 7f0feba..1b0f0c5 100644 --- a/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java +++ b/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
@@ -17,6 +17,7 @@ import static com.google.gerrit.server.api.ApiUtil.asRestApiException; import com.google.gerrit.extensions.api.changes.ChangeEditApi; +import com.google.gerrit.extensions.api.changes.FileContentInput; import com.google.gerrit.extensions.api.changes.PublishChangeEditInput; import com.google.gerrit.extensions.client.ChangeEditDetailOption; import com.google.gerrit.extensions.common.EditInfo; @@ -24,7 +25,6 @@ import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BinaryResult; import com.google.gerrit.extensions.restapi.IdString; -import com.google.gerrit.extensions.restapi.RawInput; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestApiException; @@ -200,9 +200,9 @@ } @Override - public void modifyFile(String filePath, RawInput newContent) throws RestApiException { + public void modifyFile(String filePath, FileContentInput input) throws RestApiException { try { - changeEditsPut.apply(changeResource, filePath, newContent); + changeEditsPut.apply(changeResource, filePath, input); } catch (Exception e) { throw asRestApiException("Cannot modify file of change edit", e); }
diff --git a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java index 01dfe36..1a4cbb8 100644 --- a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java +++ b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -44,6 +44,7 @@ import com.google.gerrit.extensions.common.CommentInfo; import com.google.gerrit.extensions.common.CommitInfo; import com.google.gerrit.extensions.common.DescriptionInput; +import com.google.gerrit.extensions.common.DiffInfo; import com.google.gerrit.extensions.common.EditInfo; import com.google.gerrit.extensions.common.FileInfo; import com.google.gerrit.extensions.common.Input; @@ -71,6 +72,7 @@ import com.google.gerrit.server.restapi.change.Fixes; import com.google.gerrit.server.restapi.change.GetCommit; import com.google.gerrit.server.restapi.change.GetDescription; +import com.google.gerrit.server.restapi.change.GetFixPreview; import com.google.gerrit.server.restapi.change.GetMergeList; import com.google.gerrit.server.restapi.change.GetPatch; import com.google.gerrit.server.restapi.change.GetRelated; @@ -126,6 +128,7 @@ private final ListRevisionComments listComments; private final ListRobotComments listRobotComments; private final ApplyFix applyFix; + private final GetFixPreview getFixPreview; private final Fixes fixes; private final ListRevisionDrafts listDrafts; private final CreateDraftComment createDraft; @@ -169,6 +172,7 @@ ListRevisionComments listComments, ListRobotComments listRobotComments, ApplyFix applyFix, + GetFixPreview getFixPreview, Fixes fixes, ListRevisionDrafts listDrafts, CreateDraftComment createDraft, @@ -211,6 +215,7 @@ this.robotComments = robotComments; this.listRobotComments = listRobotComments; this.applyFix = applyFix; + this.getFixPreview = getFixPreview; this.fixes = fixes; this.listDrafts = listDrafts; this.createDraft = createDraft; @@ -452,6 +457,15 @@ } @Override + public Map<String, DiffInfo> getFixPreview(String fixId) throws RestApiException { + try { + return getFixPreview.apply(fixes.parse(revision, IdString.fromDecoded(fixId))).value(); + } catch (Exception e) { + throw asRestApiException("Cannot get fix preview", e); + } + } + + @Override public List<CommentInfo> draftsAsList() throws RestApiException { try { return listDrafts.getComments(revision);
diff --git a/java/com/google/gerrit/server/api/projects/LabelApiImpl.java b/java/com/google/gerrit/server/api/projects/LabelApiImpl.java new file mode 100644 index 0000000..ad7ec31 --- /dev/null +++ b/java/com/google/gerrit/server/api/projects/LabelApiImpl.java
@@ -0,0 +1,118 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.api.projects; + +import static com.google.gerrit.server.api.ApiUtil.asRestApiException; + +import com.google.gerrit.common.Nullable; +import com.google.gerrit.extensions.api.projects.LabelApi; +import com.google.gerrit.extensions.common.InputWithCommitMessage; +import com.google.gerrit.extensions.common.LabelDefinitionInfo; +import com.google.gerrit.extensions.common.LabelDefinitionInput; +import com.google.gerrit.extensions.restapi.IdString; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.project.LabelResource; +import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.server.project.ProjectResource; +import com.google.gerrit.server.restapi.project.CreateLabel; +import com.google.gerrit.server.restapi.project.DeleteLabel; +import com.google.gerrit.server.restapi.project.GetLabel; +import com.google.gerrit.server.restapi.project.LabelsCollection; +import com.google.gerrit.server.restapi.project.SetLabel; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; + +public class LabelApiImpl implements LabelApi { + interface Factory { + LabelApiImpl create(ProjectResource project, String label); + } + + private final LabelsCollection labels; + private final CreateLabel createLabel; + private final GetLabel getLabel; + private final SetLabel setLabel; + private final DeleteLabel deleteLabel; + private final ProjectCache projectCache; + private final String label; + + private ProjectResource project; + + @Inject + LabelApiImpl( + LabelsCollection labels, + CreateLabel createLabel, + GetLabel getLabel, + SetLabel setLabel, + DeleteLabel deleteLabel, + ProjectCache projectCache, + @Assisted ProjectResource project, + @Assisted String label) { + this.labels = labels; + this.createLabel = createLabel; + this.getLabel = getLabel; + this.setLabel = setLabel; + this.deleteLabel = deleteLabel; + this.projectCache = projectCache; + this.project = project; + this.label = label; + } + + @Override + public LabelApi create(LabelDefinitionInput input) throws RestApiException { + try { + createLabel.apply(project, IdString.fromDecoded(label), input); + + // recreate project resource because project state was updated by creating the new label and + // needs to be reloaded + project = + new ProjectResource(projectCache.checkedGet(project.getNameKey()), project.getUser()); + return this; + } catch (Exception e) { + throw asRestApiException("Cannot create branch", e); + } + } + + @Override + public LabelDefinitionInfo get() throws RestApiException { + try { + return getLabel.apply(resource()).value(); + } catch (Exception e) { + throw asRestApiException("Cannot get label", e); + } + } + + @Override + public LabelDefinitionInfo update(LabelDefinitionInput input) throws RestApiException { + try { + return setLabel.apply(resource(), input).value(); + } catch (Exception e) { + throw asRestApiException("Cannot update label", e); + } + } + + @Override + public void delete(@Nullable String commitMessage) throws RestApiException { + try { + deleteLabel.apply(resource(), new InputWithCommitMessage(commitMessage)); + } catch (Exception e) { + throw asRestApiException("Cannot delete label", e); + } + } + + private LabelResource resource() throws RestApiException, PermissionBackendException { + return labels.parse(project, IdString.fromDecoded(label)); + } +}
diff --git a/java/com/google/gerrit/server/api/projects/Module.java b/java/com/google/gerrit/server/api/projects/Module.java index f1e21d28..8df5495 100644 --- a/java/com/google/gerrit/server/api/projects/Module.java +++ b/java/com/google/gerrit/server/api/projects/Module.java
@@ -28,5 +28,6 @@ factory(ChildProjectApiImpl.Factory.class); factory(CommitApiImpl.Factory.class); factory(DashboardApiImpl.Factory.class); + factory(LabelApiImpl.Factory.class); } }
diff --git a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java index 1ac905d..6d7fc15 100644 --- a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java +++ b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -37,13 +37,16 @@ import com.google.gerrit.extensions.api.projects.DescriptionInput; import com.google.gerrit.extensions.api.projects.HeadInput; import com.google.gerrit.extensions.api.projects.IndexProjectInput; +import com.google.gerrit.extensions.api.projects.LabelApi; import com.google.gerrit.extensions.api.projects.ParentInput; import com.google.gerrit.extensions.api.projects.ProjectApi; import com.google.gerrit.extensions.api.projects.ProjectInput; import com.google.gerrit.extensions.api.projects.TagApi; import com.google.gerrit.extensions.api.projects.TagInfo; +import com.google.gerrit.extensions.common.BatchLabelInput; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.Input; +import com.google.gerrit.extensions.common.LabelDefinitionInfo; import com.google.gerrit.extensions.common.ProjectInfo; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.IdString; @@ -73,7 +76,9 @@ import com.google.gerrit.server.restapi.project.IndexChanges; import com.google.gerrit.server.restapi.project.ListBranches; import com.google.gerrit.server.restapi.project.ListDashboards; +import com.google.gerrit.server.restapi.project.ListLabels; import com.google.gerrit.server.restapi.project.ListTags; +import com.google.gerrit.server.restapi.project.PostLabels; import com.google.gerrit.server.restapi.project.ProjectsCollection; import com.google.gerrit.server.restapi.project.PutConfig; import com.google.gerrit.server.restapi.project.PutDescription; @@ -127,6 +132,9 @@ private final SetParent setParent; private final Index index; private final IndexChanges indexChanges; + private final Provider<ListLabels> listLabels; + private final PostLabels postLabels; + private final LabelApiImpl.Factory labelApi; @AssistedInject ProjectApiImpl( @@ -162,6 +170,9 @@ SetParent setParent, Index index, IndexChanges indexChanges, + Provider<ListLabels> listLabels, + PostLabels postLabels, + LabelApiImpl.Factory labelApi, @Assisted ProjectResource project) { this( permissionBackend, @@ -197,6 +208,9 @@ setParent, index, indexChanges, + listLabels, + postLabels, + labelApi, null); } @@ -234,6 +248,9 @@ SetParent setParent, Index index, IndexChanges indexChanges, + Provider<ListLabels> listLabels, + PostLabels postLabels, + LabelApiImpl.Factory labelApi, @Assisted String name) { this( permissionBackend, @@ -269,6 +286,9 @@ setParent, index, indexChanges, + listLabels, + postLabels, + labelApi, name); } @@ -306,6 +326,9 @@ SetParent setParent, Index index, IndexChanges indexChanges, + Provider<ListLabels> listLabels, + PostLabels postLabels, + LabelApiImpl.Factory labelApi, String name) { this.permissionBackend = permissionBackend; this.createProject = createProject; @@ -341,6 +364,9 @@ this.name = name; this.index = index; this.indexChanges = indexChanges; + this.listLabels = listLabels; + this.postLabels = postLabels; + this.labelApi = labelApi; } @Override @@ -672,4 +698,36 @@ } return project; } + + @Override + public ListLabelsRequest labels() { + return new ListLabelsRequest() { + @Override + public List<LabelDefinitionInfo> get() throws RestApiException { + try { + return listLabels.get().withInherited(inherited).apply(checkExists()).value(); + } catch (Exception e) { + throw asRestApiException("Cannot list labels", e); + } + } + }; + } + + @Override + public LabelApi label(String labelName) throws RestApiException { + try { + return labelApi.create(checkExists(), labelName); + } catch (Exception e) { + throw asRestApiException("Cannot parse label", e); + } + } + + @Override + public void labels(BatchLabelInput input) throws RestApiException { + try { + postLabels.apply(checkExists(), input); + } catch (Exception e) { + throw asRestApiException("Cannot update labels", e); + } + } }
diff --git a/java/com/google/gerrit/server/audit/BUILD b/java/com/google/gerrit/server/audit/BUILD index 95929d3..a7ecde4 100644 --- a/java/com/google/gerrit/server/audit/BUILD +++ b/java/com/google/gerrit/server/audit/BUILD
@@ -57,7 +57,6 @@ "//lib/auto:auto-value-annotations", "//lib/bouncycastle:bcpkix-neverlink", "//lib/bouncycastle:bcprov-neverlink", - "//lib/commons:codec", "//lib/commons:compress", "//lib/commons:dbcp", "//lib/commons:lang",
diff --git a/java/com/google/gerrit/server/change/ActionJson.java b/java/com/google/gerrit/server/change/ActionJson.java index d493b31..031c1f2 100644 --- a/java/com/google/gerrit/server/change/ActionJson.java +++ b/java/com/google/gerrit/server/change/ActionJson.java
@@ -136,6 +136,7 @@ copy._number = changeInfo._number; copy.requirements = changeInfo.requirements; copy.revertOf = changeInfo.revertOf; + copy.submissionId = changeInfo.submissionId; copy.starred = changeInfo.starred; copy.stars = changeInfo.stars; copy.submitted = changeInfo.submitted;
diff --git a/java/com/google/gerrit/server/change/ChangeCleanupRunner.java b/java/com/google/gerrit/server/change/ChangeCleanupRunner.java index 0299f10..6cf7a8f 100644 --- a/java/com/google/gerrit/server/change/ChangeCleanupRunner.java +++ b/java/com/google/gerrit/server/change/ChangeCleanupRunner.java
@@ -79,11 +79,14 @@ // abandonInactiveOpenChanges skips failures instead of throwing, so retrying will never // actually happen. For the purposes of this class that is fine: they'll get tried again the // next time the scheduled task is run. - retryHelper.execute( - updateFactory -> { - abandonUtil.abandonInactiveOpenChanges(updateFactory); - return null; - }); + retryHelper + .changeUpdate( + "abandonInactiveOpenChanges", + updateFactory -> { + abandonUtil.abandonInactiveOpenChanges(updateFactory); + return null; + }) + .call(); } catch (RestApiException | UpdateException e) { logger.atSevere().withCause(e).log("Failed to cleanup changes."); }
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java index 1bf5103..d1c27d5 100644 --- a/java/com/google/gerrit/server/change/ChangeInserter.java +++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -37,6 +37,7 @@ import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.PatchSetApproval; import com.google.gerrit.entities.PatchSetInfo; +import com.google.gerrit.entities.SubmissionId; import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.client.ReviewerState; import com.google.gerrit.extensions.restapi.ResourceConflictException; @@ -386,7 +387,7 @@ psUtil.insert( ctx.getRevWalk(), update, psId, commitId, newGroups, pushCert, patchSetDescription); - /* TODO: fixStatus is used here because the tests + /* TODO: fixStatusToMerged is used here because the tests * (byStatusClosed() in AbstractQueryChangesTest) * insert changes that are already merged, * and setStatus may not be used to set the Status to merged @@ -394,7 +395,11 @@ * is it possible to make the tests use the merge code path, * instead of setting the status directly? */ - update.fixStatus(change.getStatus()); + if (change.getStatus() == Change.Status.MERGED) { + update.fixStatusToMerged(new SubmissionId(change)); + } else { + update.setStatus(change.getStatus()); + } reviewerAdditions = reviewerAdder.prepare(ctx.getNotes(), ctx.getUser(), getReviewerInputs(), true);
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java index 3b7a2c4..21ee28a 100644 --- a/java/com/google/gerrit/server/change/ChangeJson.java +++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -138,6 +138,7 @@ COMMIT_FOOTERS, CURRENT_ACTIONS, CURRENT_COMMIT, + DETAILED_LABELS, // may need to load ChangeNotes to check remove reviewer permissions MESSAGES); @Singleton @@ -586,6 +587,7 @@ out.plugins = pluginDefinedAttributesFactory.get().create(cd); } out.revertOf = cd.change().getRevertOf() != null ? cd.change().getRevertOf().get() : null; + out.submissionId = cd.change().getSubmissionId(); if (has(REVIEWER_UPDATES)) { out.reviewerUpdates = reviewerUpdates(cd);
diff --git a/java/com/google/gerrit/server/change/ConsistencyChecker.java b/java/com/google/gerrit/server/change/ConsistencyChecker.java index 0374a1c..0f03c88 100644 --- a/java/com/google/gerrit/server/change/ConsistencyChecker.java +++ b/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -33,6 +33,7 @@ import com.google.gerrit.entities.Change; import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.Project; +import com.google.gerrit.entities.SubmissionId; import com.google.gerrit.exceptions.StorageException; import com.google.gerrit.extensions.api.changes.FixInput; import com.google.gerrit.extensions.common.ProblemInfo; @@ -169,26 +170,29 @@ public Result check(ChangeNotes notes, @Nullable FixInput f) { requireNonNull(notes); try { - return retryHelper.execute( - buf -> { - try { - reset(); - this.updateFactory = buf; - this.notes = notes; - fix = f; - checkImpl(); - return result(); - } finally { - if (rw != null) { - rw.getObjectReader().close(); - rw.close(); - oi.close(); - } - if (repo != null) { - repo.close(); - } - } - }); + return retryHelper + .changeUpdate( + "checkChangeConsistency", + buf -> { + try { + reset(); + this.updateFactory = buf; + this.notes = notes; + fix = f; + checkImpl(); + return result(); + } finally { + if (rw != null) { + rw.getObjectReader().close(); + rw.close(); + oi.close(); + } + if (repo != null) { + repo.close(); + } + } + }) + .call(); } catch (RestApiException e) { return logAndReturnOneProblem(e, notes, "Error checking change: " + e.getMessage()); } catch (UpdateException e) { @@ -576,7 +580,8 @@ @Override public boolean updateChange(ChangeContext ctx) { ctx.getChange().setStatus(Change.Status.MERGED); - ctx.getUpdate(ctx.getChange().currentPatchSetId()).fixStatus(Change.Status.MERGED); + ctx.getUpdate(ctx.getChange().currentPatchSetId()) + .fixStatusToMerged(new SubmissionId(ctx.getChange())); p.status = Status.FIXED; p.outcome = "Marked change as merged"; return true;
diff --git a/java/com/google/gerrit/server/change/FileContentUtil.java b/java/com/google/gerrit/server/change/FileContentUtil.java index 5c7946c..49c1fe2 100644 --- a/java/com/google/gerrit/server/change/FileContentUtil.java +++ b/java/com/google/gerrit/server/change/FileContentUtil.java
@@ -100,7 +100,7 @@ public BinaryResult getContent( Repository repo, ProjectState project, ObjectId revstr, String path) - throws IOException, ResourceNotFoundException { + throws IOException, ResourceNotFoundException, BadRequestException { try (RevWalk rw = new RevWalk(repo)) { RevCommit commit = rw.parseCommit(revstr); try (TreeWalk tw = TreeWalk.forPath(rw.getObjectReader(), path, commit.getTree())) { @@ -114,6 +114,10 @@ return BinaryResult.create(id.name()).setContentType(X_GIT_GITLINK).base64(); } + if (mode == org.eclipse.jgit.lib.FileMode.TREE) { + throw new BadRequestException("cannot retrieve content of directories"); + } + ObjectLoader obj = repo.open(id, OBJ_BLOB); byte[] raw; try {
diff --git a/java/com/google/gerrit/server/change/SetPrivateOp.java b/java/com/google/gerrit/server/change/SetPrivateOp.java index 28d178d..382a4f6 100644 --- a/java/com/google/gerrit/server/change/SetPrivateOp.java +++ b/java/com/google/gerrit/server/change/SetPrivateOp.java
@@ -19,6 +19,7 @@ import com.google.gerrit.entities.Change; import com.google.gerrit.entities.ChangeMessage; import com.google.gerrit.entities.PatchSet; +import com.google.gerrit.extensions.common.InputWithMessage; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.server.ChangeMessagesUtil; @@ -34,25 +35,15 @@ import com.google.inject.assistedinject.Assisted; public class SetPrivateOp implements BatchUpdateOp { - public static class Input { - String message; - - public Input() {} - - public Input(String message) { - this.message = message; - } - } - public interface Factory { - SetPrivateOp create(boolean isPrivate, @Nullable Input input); + SetPrivateOp create(boolean isPrivate, @Nullable InputWithMessage input); } private final PrivateStateChanged privateStateChanged; private final PatchSetUtil psUtil; private final ChangeMessagesUtil cmUtil; private final boolean isPrivate; - @Nullable private final Input input; + @Nullable private final InputWithMessage input; private Change change; private PatchSet ps; @@ -64,7 +55,7 @@ PatchSetUtil psUtil, ChangeMessagesUtil cmUtil, @Assisted boolean isPrivate, - @Assisted @Nullable Input input) { + @Assisted @Nullable InputWithMessage input) { this.privateStateChanged = privateStateChanged; this.psUtil = psUtil; this.cmUtil = cmUtil;
diff --git a/java/com/google/gerrit/server/change/WorkInProgressOp.java b/java/com/google/gerrit/server/change/WorkInProgressOp.java index 78edada..283cff8 100644 --- a/java/com/google/gerrit/server/change/WorkInProgressOp.java +++ b/java/com/google/gerrit/server/change/WorkInProgressOp.java
@@ -21,6 +21,7 @@ import com.google.gerrit.entities.ChangeMessage; import com.google.gerrit.entities.PatchSet; import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.common.InputWithMessage; import com.google.gerrit.server.ChangeMessagesUtil; import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.extensions.events.WorkInProgressStateChanged; @@ -34,15 +35,15 @@ /* Set work in progress or ready for review state on a change */ public class WorkInProgressOp implements BatchUpdateOp { - public static class Input { - @Nullable public String message; - + public static class Input extends InputWithMessage { @Nullable public NotifyHandling notify; - public Input() {} + public Input() { + this(null); + } - public Input(String message) { - this.message = message; + public Input(@Nullable String message) { + super(message); } }
diff --git a/java/com/google/gerrit/server/config/ChangeCleanupConfig.java b/java/com/google/gerrit/server/config/ChangeCleanupConfig.java index 4d41ed7..afd159c 100644 --- a/java/com/google/gerrit/server/config/ChangeCleanupConfig.java +++ b/java/com/google/gerrit/server/config/ChangeCleanupConfig.java
@@ -15,6 +15,7 @@ package com.google.gerrit.server.config; import com.google.common.base.Strings; +import com.google.common.flogger.FluentLogger; import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.server.config.ScheduleConfig.Schedule; import com.google.inject.Inject; @@ -25,6 +26,8 @@ @Singleton public class ChangeCleanupConfig { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private static String SECTION = "changeCleanup"; private static String KEY_ABANDON_AFTER = "abandonAfter"; private static String KEY_ABANDON_IF_MERGEABLE = "abandonIfMergeable"; @@ -48,12 +51,26 @@ this.urlFormatter = urlFormatter; schedule = ScheduleConfig.createSchedule(cfg, SECTION); abandonAfter = readAbandonAfter(cfg); - abandonIfMergeable = cfg.getBoolean(SECTION, null, KEY_ABANDON_IF_MERGEABLE, true); + boolean indexMergeable = cfg.getBoolean("index", "change", "indexMergeable", true); + if (!indexMergeable) { + if (!readAbandonIfMergeable(cfg)) { + logger.atWarning().log( + "index.change.indexMergeable is disabled; %s.%s=false will be ineffective", + SECTION, KEY_ABANDON_IF_MERGEABLE); + } + abandonIfMergeable = true; + } else { + abandonIfMergeable = readAbandonIfMergeable(cfg); + } cleanupAccountPatchReview = cfg.getBoolean(SECTION, null, KEY_CLEANUP_ACCOUNT_PATCH_REVIEW, false); abandonMessage = readAbandonMessage(cfg); } + private boolean readAbandonIfMergeable(Config cfg) { + return cfg.getBoolean(SECTION, null, KEY_ABANDON_IF_MERGEABLE, true); + } + private long readAbandonAfter(Config cfg) { long abandonAfter = ConfigUtil.getTimeUnit(cfg, SECTION, null, KEY_ABANDON_AFTER, 0, TimeUnit.MILLISECONDS);
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java index 3b9c40e..edf8864 100644 --- a/java/com/google/gerrit/server/config/GerritGlobalModule.java +++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -79,6 +79,7 @@ import com.google.gerrit.server.CreateGroupPermissionSyncer; import com.google.gerrit.server.DynamicOptions; import com.google.gerrit.server.ExceptionHook; +import com.google.gerrit.server.ExceptionHookImpl; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.RequestListener; import com.google.gerrit.server.TraceRequestListener; @@ -157,6 +158,7 @@ import com.google.gerrit.server.notedb.NoteDbModule; import com.google.gerrit.server.patch.PatchListCacheImpl; import com.google.gerrit.server.patch.PatchScriptFactory; +import com.google.gerrit.server.patch.PatchScriptFactoryForAutoFix; import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gerrit.server.permissions.PermissionCollection; import com.google.gerrit.server.permissions.SectionSortCache; @@ -260,6 +262,7 @@ factory(LabelsJson.Factory.class); factory(MergeUtil.Factory.class); factory(PatchScriptFactory.Factory.class); + factory(PatchScriptFactoryForAutoFix.Factory.class); factory(ProjectState.Factory.class); factory(RevisionJson.Factory.class); factory(InboundEmailRejectionSender.Factory.class); @@ -393,6 +396,7 @@ DynamicSet.bind(binder(), RequestListener.class).to(TraceRequestListener.class); DynamicSet.setOf(binder(), ChangeETagComputation.class); DynamicSet.setOf(binder(), ExceptionHook.class); + DynamicSet.bind(binder(), ExceptionHook.class).to(ExceptionHookImpl.class); DynamicSet.setOf(binder(), MailSoyTemplateProvider.class); DynamicMap.mapOf(binder(), MailFilter.class);
diff --git a/java/com/google/gerrit/server/config/OperatorAliasConfig.java b/java/com/google/gerrit/server/config/OperatorAliasConfig.java new file mode 100644 index 0000000..0c5fc6e --- /dev/null +++ b/java/com/google/gerrit/server/config/OperatorAliasConfig.java
@@ -0,0 +1,46 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.config; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.util.HashMap; +import java.util.Map; +import org.eclipse.jgit.lib.Config; + +@Singleton +public class OperatorAliasConfig { + private static final String SECTION = "operator-alias"; + private static final String SUBSECTION_CHANGE = "change"; + private final Config cfg; + private final Map<String, String> changeQueryOperatorAliases; + + @Inject + OperatorAliasConfig(@GerritServerConfig Config cfg) { + this.cfg = cfg; + changeQueryOperatorAliases = new HashMap<>(); + loadChangeOperatorAliases(); + } + + public Map<String, String> getChangeQueryOperatorAliases() { + return changeQueryOperatorAliases; + } + + private void loadChangeOperatorAliases() { + for (String name : cfg.getNames(SECTION, SUBSECTION_CHANGE)) { + changeQueryOperatorAliases.put(name, cfg.getString(SECTION, SUBSECTION_CHANGE, name)); + } + } +}
diff --git a/java/com/google/gerrit/server/diff/DiffInfoCreator.java b/java/com/google/gerrit/server/diff/DiffInfoCreator.java new file mode 100644 index 0000000..c29ffc8 --- /dev/null +++ b/java/com/google/gerrit/server/diff/DiffInfoCreator.java
@@ -0,0 +1,299 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.diff; + +import static com.google.common.base.Preconditions.checkState; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.common.data.PatchScript; +import com.google.gerrit.common.data.PatchScript.DisplayMethod; +import com.google.gerrit.common.data.PatchScript.PatchScriptFileInfo; +import com.google.gerrit.entities.Patch; +import com.google.gerrit.extensions.common.ChangeType; +import com.google.gerrit.extensions.common.DiffInfo; +import com.google.gerrit.extensions.common.DiffInfo.ContentEntry; +import com.google.gerrit.extensions.common.DiffInfo.FileMeta; +import com.google.gerrit.extensions.common.DiffInfo.IntraLineStatus; +import com.google.gerrit.extensions.common.DiffWebLinkInfo; +import com.google.gerrit.extensions.common.WebLinkInfo; +import com.google.gerrit.jgit.diff.ReplaceEdit; +import com.google.gerrit.prettify.common.SparseFileContent; +import com.google.gerrit.server.change.FileContentUtil; +import com.google.gerrit.server.project.ProjectState; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.eclipse.jgit.diff.Edit; + +/** Creates and fills a new {@link DiffInfo} object based on diff between files. */ +public class DiffInfoCreator { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private static final ImmutableMap<Patch.ChangeType, ChangeType> CHANGE_TYPE = + Maps.immutableEnumMap( + new ImmutableMap.Builder<Patch.ChangeType, ChangeType>() + .put(Patch.ChangeType.ADDED, ChangeType.ADDED) + .put(Patch.ChangeType.MODIFIED, ChangeType.MODIFIED) + .put(Patch.ChangeType.DELETED, ChangeType.DELETED) + .put(Patch.ChangeType.RENAMED, ChangeType.RENAMED) + .put(Patch.ChangeType.COPIED, ChangeType.COPIED) + .put(Patch.ChangeType.REWRITE, ChangeType.REWRITE) + .build()); + + private final DiffWebLinksProvider webLinksProvider; + private final boolean intraline; + private final ProjectState state; + + public DiffInfoCreator( + ProjectState state, DiffWebLinksProvider webLinksProvider, boolean intraline) { + this.webLinksProvider = webLinksProvider; + this.state = state; + this.intraline = intraline; + } + + /* Returns the {@link DiffInfo} to display for end-users */ + public DiffInfo create(PatchScript ps, DiffSide sideA, DiffSide sideB) { + DiffInfo result = new DiffInfo(); + + ImmutableList<DiffWebLinkInfo> links = webLinksProvider.getDiffLinks(); + result.webLinks = links.isEmpty() ? null : links; + + if (ps.isBinary()) { + result.binary = true; + } + result.metaA = createFileMeta(sideA).orElse(null); + result.metaB = createFileMeta(sideB).orElse(null); + + if (intraline) { + if (ps.hasIntralineTimeout()) { + result.intralineStatus = IntraLineStatus.TIMEOUT; + } else if (ps.hasIntralineFailure()) { + result.intralineStatus = IntraLineStatus.FAILURE; + } else { + result.intralineStatus = IntraLineStatus.OK; + } + logger.atFine().log("intralineStatus = %s", result.intralineStatus); + } + + result.changeType = CHANGE_TYPE.get(ps.getChangeType()); + logger.atFine().log("changeType = %s", result.changeType); + if (result.changeType == null) { + throw new IllegalStateException("unknown change type: " + ps.getChangeType()); + } + + if (ps.getPatchHeader().size() > 0) { + result.diffHeader = ps.getPatchHeader(); + } + result.content = calculateDiffContentEntries(ps); + return result; + } + + private static List<ContentEntry> calculateDiffContentEntries(PatchScript ps) { + ContentCollector contentCollector = new ContentCollector(ps); + Set<Edit> editsDueToRebase = ps.getEditsDueToRebase(); + for (Edit edit : ps.getEdits()) { + logger.atFine().log("next edit = %s", edit); + + if (edit.getType() == Edit.Type.EMPTY) { + logger.atFine().log("skip empty edit"); + continue; + } + contentCollector.addCommon(edit.getBeginA()); + + checkState( + contentCollector.nextA == edit.getBeginA(), + "nextA = %s; want %s", + contentCollector.nextA, + edit.getBeginA()); + checkState( + contentCollector.nextB == edit.getBeginB(), + "nextB = %s; want %s", + contentCollector.nextB, + edit.getBeginB()); + switch (edit.getType()) { + case DELETE: + case INSERT: + case REPLACE: + List<Edit> internalEdit = + edit instanceof ReplaceEdit ? ((ReplaceEdit) edit).getInternalEdits() : null; + boolean dueToRebase = editsDueToRebase.contains(edit); + contentCollector.addDiff(edit.getEndA(), edit.getEndB(), internalEdit, dueToRebase); + break; + case EMPTY: + default: + throw new IllegalStateException(); + } + } + contentCollector.addCommon(ps.getA().getSize()); + + return contentCollector.lines; + } + + private Optional<FileMeta> createFileMeta(DiffSide side) { + PatchScriptFileInfo fileInfo = side.fileInfo(); + if (fileInfo.displayMethod == DisplayMethod.NONE) { + return Optional.empty(); + } + FileMeta result = new FileMeta(); + result.name = side.fileName(); + result.contentType = + FileContentUtil.resolveContentType( + state, side.fileName(), fileInfo.mode, fileInfo.mimeType); + result.lines = fileInfo.content.getSize(); + ImmutableList<WebLinkInfo> links = webLinksProvider.getFileWebLinks(side.type()); + result.webLinks = links.isEmpty() ? null : links; + result.commitId = fileInfo.commitId; + return Optional.of(result); + } + + private static class ContentCollector { + + private final List<ContentEntry> lines; + private final SparseFileContent.Accessor fileA; + private final SparseFileContent.Accessor fileB; + private final boolean ignoreWS; + + private int nextA; + private int nextB; + + ContentCollector(PatchScript ps) { + lines = Lists.newArrayListWithExpectedSize(ps.getEdits().size() + 2); + fileA = ps.getA().createAccessor(); + fileB = ps.getB().createAccessor(); + ignoreWS = ps.isIgnoreWhitespace(); + } + + void addCommon(int end) { + logger.atFine().log("addCommon: end = %d", end); + + end = Math.min(end, fileA.getSize()); + logger.atFine().log("end = %d", end); + + if (nextA >= end) { + logger.atFine().log("nextA >= end: nextA = %d, end = %d", nextA, end); + return; + } + + while (nextA < end) { + logger.atFine().log("nextA < end: nextA = %d, end = %d", nextA, end); + + if (!fileA.contains(nextA)) { + logger.atFine().log("fileA does not contain nextA: nextA = %d", nextA); + + int endRegion = Math.min(end, nextA == 0 ? fileA.first() : fileA.next(nextA - 1)); + int len = endRegion - nextA; + entry().skip = len; + nextA = endRegion; + nextB += len; + + logger.atFine().log("setting: nextA = %d, nextB = %d", nextA, nextB); + continue; + } + + ContentEntry e = null; + for (int i = nextA; i == nextA && i < end; i = fileA.next(i), nextA++, nextB++) { + if (ignoreWS && fileB.contains(nextB)) { + if (e == null || e.common == null) { + logger.atFine().log("create new common entry: nextA = %d, nextB = %d", nextA, nextB); + e = entry(); + e.a = Lists.newArrayListWithCapacity(end - nextA); + e.b = Lists.newArrayListWithCapacity(end - nextA); + e.common = true; + } + e.a.add(fileA.get(nextA)); + e.b.add(fileB.get(nextB)); + } else { + if (e == null || e.common != null) { + logger.atFine().log( + "create new non-common entry: nextA = %d, nextB = %d", nextA, nextB); + e = entry(); + e.ab = Lists.newArrayListWithCapacity(end - nextA); + } + e.ab.add(fileA.get(nextA)); + } + } + } + } + + void addDiff(int endA, int endB, List<Edit> internalEdit, boolean dueToRebase) { + logger.atFine().log( + "addDiff: endA = %d, endB = %d, numberOfInternalEdits = %d, dueToRebase = %s", + endA, endB, internalEdit != null ? internalEdit.size() : 0, dueToRebase); + + int lenA = endA - nextA; + int lenB = endB - nextB; + logger.atFine().log("lenA = %d, lenB = %d", lenA, lenB); + checkState(lenA > 0 || lenB > 0); + + logger.atFine().log("create non-common entry"); + ContentEntry e = entry(); + if (lenA > 0) { + logger.atFine().log("lenA > 0: lenA = %d", lenA); + e.a = Lists.newArrayListWithCapacity(lenA); + for (; nextA < endA; nextA++) { + e.a.add(fileA.get(nextA)); + } + } + if (lenB > 0) { + logger.atFine().log("lenB > 0: lenB = %d", lenB); + e.b = Lists.newArrayListWithCapacity(lenB); + for (; nextB < endB; nextB++) { + e.b.add(fileB.get(nextB)); + } + } + if (internalEdit != null && !internalEdit.isEmpty()) { + logger.atFine().log("processing internal edits"); + + e.editA = Lists.newArrayListWithCapacity(internalEdit.size() * 2); + e.editB = Lists.newArrayListWithCapacity(internalEdit.size() * 2); + int lastA = 0; + int lastB = 0; + for (Edit edit : internalEdit) { + logger.atFine().log("internal edit = %s", edit); + + if (edit.getBeginA() != edit.getEndA()) { + logger.atFine().log( + "edit.getBeginA() != edit.getEndA(): edit.getBeginA() = %d, edit.getEndA() = %d", + edit.getBeginA(), edit.getEndA()); + e.editA.add( + ImmutableList.of(edit.getBeginA() - lastA, edit.getEndA() - edit.getBeginA())); + lastA = edit.getEndA(); + logger.atFine().log("lastA = %d", lastA); + } + if (edit.getBeginB() != edit.getEndB()) { + logger.atFine().log( + "edit.getBeginB() != edit.getEndB(): edit.getBeginB() = %d, edit.getEndB() = %d", + edit.getBeginB(), edit.getEndB()); + e.editB.add( + ImmutableList.of(edit.getBeginB() - lastB, edit.getEndB() - edit.getBeginB())); + lastB = edit.getEndB(); + logger.atFine().log("lastB = %d", lastB); + } + } + } + e.dueToRebase = dueToRebase ? true : null; + } + + private ContentEntry entry() { + ContentEntry e = new ContentEntry(); + lines.add(e); + return e; + } + } +}
diff --git a/java/com/google/gerrit/server/diff/DiffSide.java b/java/com/google/gerrit/server/diff/DiffSide.java new file mode 100644 index 0000000..28c7810 --- /dev/null +++ b/java/com/google/gerrit/server/diff/DiffSide.java
@@ -0,0 +1,37 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.diff; + +import com.google.auto.value.AutoValue; +import com.google.gerrit.common.data.PatchScript.PatchScriptFileInfo; + +/** Contains settings for one of two sides in diff view. Each diff view has exactly 2 sides. */ +@AutoValue +public abstract class DiffSide { + public enum Type { + SIDE_A, + SIDE_B + } + + public static DiffSide create(PatchScriptFileInfo fileInfo, String fileName, Type type) { + return new AutoValue_DiffSide(fileInfo, fileName, type); + } + + public abstract PatchScriptFileInfo fileInfo(); + + public abstract String fileName(); + + public abstract Type type(); +}
diff --git a/java/com/google/gerrit/server/diff/DiffWebLinksProvider.java b/java/com/google/gerrit/server/diff/DiffWebLinksProvider.java new file mode 100644 index 0000000..0f71b17 --- /dev/null +++ b/java/com/google/gerrit/server/diff/DiffWebLinksProvider.java
@@ -0,0 +1,29 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.diff; + +import com.google.common.collect.ImmutableList; +import com.google.gerrit.extensions.common.DiffWebLinkInfo; +import com.google.gerrit.extensions.common.WebLinkInfo; + +/** Provider for different types of links which can be displayed in a diff view. */ +public interface DiffWebLinksProvider { + + /** Returns links associated with the diff view */ + ImmutableList<DiffWebLinkInfo> getDiffLinks(); + + /** Returns links associated with the diff side */ + ImmutableList<WebLinkInfo> getFileWebLinks(DiffSide.Type fileInfoType); +}
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java index afaf695..c05a47d 100644 --- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java +++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -17,12 +17,14 @@ import com.google.common.collect.ImmutableList; import com.google.gerrit.entities.Change; import com.google.gerrit.entities.PatchSet; +import com.google.gerrit.entities.Project; import com.google.gerrit.entities.RefNames; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.MergeConflictException; import com.google.gerrit.extensions.restapi.RawInput; import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.git.LockFailureException; import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.GerritPersonIdent; @@ -228,7 +230,12 @@ createCommit(repository, basePatchSetCommit, baseTree, newCommitMessage, nowTimestamp); if (optionalChangeEdit.isPresent()) { - updateEdit(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp); + updateEdit( + notes.getProjectName(), + repository, + optionalChangeEdit.get(), + newEditCommit, + nowTimestamp); } else { createEdit(repository, notes, basePatchSet, newEditCommit, nowTimestamp); } @@ -331,7 +338,12 @@ createCommit(repository, basePatchSetCommit, newTreeId, commitMessage, nowTimestamp); if (optionalChangeEdit.isPresent()) { - updateEdit(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp); + updateEdit( + notes.getProjectName(), + repository, + optionalChangeEdit.get(), + newEditCommit, + nowTimestamp); } else { createEdit(repository, notes, basePatchSet, newEditCommit, nowTimestamp); } @@ -384,7 +396,12 @@ createCommit(repository, patchSetCommit, newTreeId, commitMessage, nowTimestamp); if (optionalChangeEdit.isPresent()) { - return updateEdit(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp); + return updateEdit( + notes.getProjectName(), + repository, + optionalChangeEdit.get(), + newEditCommit, + nowTimestamp); } return createEdit(repository, notes, patchSet, newEditCommit, nowTimestamp); } @@ -531,7 +548,13 @@ throws IOException { Change change = notes.getChange(); String editRefName = getEditRefName(change, basePatchSet); - updateReference(repository, editRefName, ObjectId.zeroId(), newEditCommitId, timestamp); + updateReference( + notes.getProjectName(), + repository, + editRefName, + ObjectId.zeroId(), + newEditCommitId, + timestamp); reindex(change); RevCommit newEditCommit = lookupCommit(repository, newEditCommitId); @@ -544,11 +567,16 @@ } private ChangeEdit updateEdit( - Repository repository, ChangeEdit changeEdit, ObjectId newEditCommitId, Timestamp timestamp) + Project.NameKey projectName, + Repository repository, + ChangeEdit changeEdit, + ObjectId newEditCommitId, + Timestamp timestamp) throws IOException { String editRefName = changeEdit.getRefName(); RevCommit currentEditCommit = changeEdit.getEditCommit(); - updateReference(repository, editRefName, currentEditCommit, newEditCommitId, timestamp); + updateReference( + projectName, repository, editRefName, currentEditCommit, newEditCommitId, timestamp); reindex(changeEdit.getChange()); RevCommit newEditCommit = lookupCommit(repository, newEditCommitId); @@ -557,6 +585,7 @@ } private void updateReference( + Project.NameKey projectName, Repository repository, String refName, ObjectId currentObjectId, @@ -571,14 +600,12 @@ ru.setForceUpdate(true); try (RevWalk revWalk = new RevWalk(repository)) { RefUpdate.Result res = ru.update(revWalk); + String message = "cannot update " + ru.getName() + " in " + projectName + ": " + res; + if (res == RefUpdate.Result.LOCK_FAILURE) { + throw new LockFailureException(message, ru); + } if (res != RefUpdate.Result.NEW && res != RefUpdate.Result.FORCED) { - throw new IOException( - "cannot update " - + ru.getName() - + " in " - + repository.getDirectory() - + ": " - + ru.getResult()); + throw new IOException(message); } } }
diff --git a/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/java/com/google/gerrit/server/edit/ChangeEditUtil.java index 6ba30bf..926e0d5 100644 --- a/java/com/google/gerrit/server/edit/ChangeEditUtil.java +++ b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -24,6 +24,7 @@ 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.git.LockFailureException; import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; @@ -248,9 +249,10 @@ case NEW: case NO_CHANGE: break; + case LOCK_FAILURE: + throw new LockFailureException(String.format("Failed to delete ref %s", refName), ru); case FAST_FORWARD: case IO_FAILURE: - case LOCK_FAILURE: case NOT_ATTEMPTED: case REJECTED: case REJECTED_CURRENT_BRANCH:
diff --git a/java/com/google/gerrit/server/fixes/FixCalculator.java b/java/com/google/gerrit/server/fixes/FixCalculator.java new file mode 100644 index 0000000..5d9d2c2 --- /dev/null +++ b/java/com/google/gerrit/server/fixes/FixCalculator.java
@@ -0,0 +1,366 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.fixes; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.collect.ImmutableList; +import com.google.gerrit.entities.Comment; +import com.google.gerrit.entities.FixReplacement; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.jgit.diff.ReplaceEdit; +import com.google.gerrit.server.patch.Text; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import org.eclipse.jgit.diff.Edit; + +/** + * Produces final version of an input content with all fixes applied together with list of edits. + */ +public class FixCalculator { + private static final Comparator<FixReplacement> ASC_RANGE_FIX_REPLACEMENT_COMPARATOR = + Comparator.comparing(fixReplacement -> fixReplacement.range); + + private FixCalculator() {} + + /** + * Returns a result of applying fixes to an original content. + * + * @param originalContent is a text to which fixes must be applied + * @param fixReplacements is a list of fixes to be applied + * @throws ResourceConflictException if the fixReplacements contains invalid data (for example, if + * an item points to an invalid range or if some ranges are intersected). + */ + public static String getNewFileContent( + String originalContent, List<FixReplacement> fixReplacements) + throws ResourceConflictException { + FixResult fixResult = calculateFix(new Text(originalContent.getBytes(UTF_8)), fixReplacements); + return fixResult.text.getString(0, fixResult.text.size(), false); + } + + /** + * Returns a result of applying fixes to an original content and list of applied edits. + * + * @param originalText is a text to which fixes must be applied + * @param fixReplacements is a list of fixes to be applied + * @return {@link FixResult} + * @throws ResourceConflictException if the fixReplacements contains invalid data (for example, if + * an item points to an invalid range or if some ranges are intersected). + */ + public static FixResult calculateFix(Text originalText, List<FixReplacement> fixReplacements) + throws ResourceConflictException { + List<FixReplacement> sortedReplacements = new ArrayList<>(fixReplacements); + sortedReplacements.sort(ASC_RANGE_FIX_REPLACEMENT_COMPARATOR); + if (!sortedReplacements.isEmpty() && sortedReplacements.get(0).range.startLine <= 0) { + throw new ResourceConflictException( + String.format( + "Cannot calculate fix replacement for range %s", + toString(sortedReplacements.get(0).range))); + } + ContentBuilder builder = new ContentBuilder(originalText); + for (FixReplacement fixReplacement : sortedReplacements) { + try { + builder.addReplacement(fixReplacement); + } catch (IndexOutOfBoundsException e) { + throw new ResourceConflictException( + String.format( + "Cannot calculate fix replacement for range %s", toString(fixReplacement.range)), + e); + } + } + return builder.build(); + } + + private static String toString(Comment.Range range) { + return String.format( + "(%s:%s - %s:%s)", range.startLine, range.startChar, range.endLine, range.endChar); + } + + private static class ContentBuilder { + private static class FixRegion { + int startSrcLine; + int startDstLine; + int startSrcPos; + int startDstPos; + List<Edit> internalEdits; + + FixRegion() { + this.internalEdits = new ArrayList<>(); + } + } + + private final ContentProcessor contentProcessor; + final ImmutableList.Builder<Edit> edits; + FixRegion currentRegion; + + ContentBuilder(Text src) { + this.contentProcessor = new ContentProcessor(src); + this.edits = new ImmutableList.Builder<>(); + } + + void addReplacement(FixReplacement replacement) { + if (shouldStartNewEdit(replacement)) { + finishExistingEdit(); + } + // processSrcContent expects that line number is 0-based, + // but replacement.range.startLine is 1-based, so subtract 1 + processSrcContent(replacement.range.startLine - 1, replacement.range.startChar, true); + processReplacement(replacement); + } + + Text getNewText() { + return new Text(contentProcessor.sb.toString().getBytes(UTF_8)); + } + + void finish() { + finishExistingEdit(); + if (contentProcessor.hasMoreLines()) { + contentProcessor.appendLinesToEndOfContent(); + } + } + + public FixResult build() { + finish(); + return new FixResult(edits.build(), this.getNewText()); + } + + private void finishExistingEdit() { + if (contentProcessor.srcPosition.column > 0 || contentProcessor.dstPosition.column > 0) { + contentProcessor.processToEndOfLine(true); + } + if (currentRegion != null) { + int endSrc = contentProcessor.srcPosition.line; + if (contentProcessor.srcPosition.column > 0) { + endSrc++; + } + int endDst = contentProcessor.dstPosition.line; + if (contentProcessor.dstPosition.column > 0) { + endDst++; + } + ReplaceEdit edit = + new ReplaceEdit( + currentRegion.startSrcLine, + endSrc, + currentRegion.startDstLine, + endDst, + currentRegion.internalEdits); + currentRegion = null; + edits.add(edit); + } + } + + private boolean shouldStartNewEdit(FixReplacement replacement) { + if (currentRegion == null) { + return true; + } + // New edit must be started if there is at least one unchanged line after the last edit + // Subtract 1 from replacement.range.startLine because it is a 1-based line number, + // and contentProcessor.srcPosition.line is a 0-based line number + return replacement.range.startLine - 1 > contentProcessor.srcPosition.line + 1; + } + + private void processSrcContent(int toLine, int toColumn, boolean append) + throws IndexOutOfBoundsException { + // toLine >= currentSrcLineIndex + if (toLine == contentProcessor.srcPosition.line) { + contentProcessor.processLineToColumn(toColumn, append); + } else { + contentProcessor.processToEndOfLine(append); + contentProcessor.processMultiline(toLine, append); + contentProcessor.processLineToColumn(toColumn, append); + } + } + + private void processReplacement(FixReplacement fix) { + if (currentRegion == null) { + currentRegion = new FixRegion(); + currentRegion.startSrcLine = contentProcessor.srcPosition.line; + currentRegion.startSrcPos = contentProcessor.srcPosition.getLineStartPos(); + currentRegion.startDstLine = contentProcessor.dstPosition.line; + currentRegion.startDstPos = contentProcessor.dstPosition.getLineStartPos(); + } + int srcStartPos = contentProcessor.srcPosition.textPos; + int dstStartPos = contentProcessor.dstPosition.textPos; + contentProcessor.appendReplacement(fix.replacement); + processSrcContent(fix.range.endLine - 1, fix.range.endChar, false); + + currentRegion.internalEdits.add( + new Edit( + srcStartPos - currentRegion.startSrcPos, + contentProcessor.srcPosition.textPos - currentRegion.startSrcPos, + dstStartPos - currentRegion.startDstPos, + contentProcessor.dstPosition.textPos - currentRegion.startDstPos)); + } + } + + private static class ContentProcessor { + static class ContentPosition { + int line; + int column; + int textPos; + + void appendMultilineContent(int lineCount, int charCount) { + line += lineCount; + column = 0; + textPos += charCount; + } + + void appendLineEndedWithEOLMark(int charCount) { + textPos += charCount; + line++; + column = 0; + } + + void appendStringWithoutEOLMark(int charCount) { + textPos += charCount; + column += charCount; + } + + int getLineStartPos() { + return textPos - column; + } + } + + private final StringBuilder sb; + final ContentPosition srcPosition; + final ContentPosition dstPosition; + String currentSrcLine; + Text src; + boolean endOfSource; + + ContentProcessor(Text src) { + this.src = src; + sb = new StringBuilder(src.size()); + srcPosition = new ContentPosition(); + dstPosition = new ContentPosition(); + endOfSource = src.size() == 0; + } + + void processMultiline(int toLine, boolean append) { + if (endOfSource || toLine <= srcPosition.line) { + return; + } + int fromLine = srcPosition.line; + String lines = src.getString(fromLine, toLine, false); + int lineCount = toLine - fromLine; + int charCount = lines.length(); + srcPosition.appendMultilineContent(lineCount, charCount); + + if (append) { + sb.append(lines); + dstPosition.appendMultilineContent(lineCount, charCount); + } + currentSrcLine = null; + endOfSource = srcPosition.line >= src.size(); + } + + void processToEndOfLine(boolean append) { + if (endOfSource) { + return; + } + String srcLine = getCurrentSrcLine(); + int from = srcPosition.column; + int charCount = srcLine.length() - from; + boolean lastLineNoEOLMark = srcPosition.line >= src.size() - 1 && src.isMissingNewlineAtEnd(); + if (!lastLineNoEOLMark) { + srcPosition.appendLineEndedWithEOLMark(charCount); + endOfSource = srcPosition.line >= src.size(); + } else { + srcPosition.appendStringWithoutEOLMark(charCount); + endOfSource = true; + } + if (append) { + sb.append(srcLine, from, srcLine.length()); + if (!lastLineNoEOLMark) { + dstPosition.appendLineEndedWithEOLMark(charCount); + } else { + dstPosition.appendStringWithoutEOLMark(charCount); + } + } + currentSrcLine = null; + } + + void processLineToColumn(int to, boolean append) throws IndexOutOfBoundsException { + if (to == 0) { + return; + } + String srcLine = getCurrentSrcLine(); + if (to > srcLine.length()) { + throw new IndexOutOfBoundsException("Parameter to is out of string"); + } else if (to == srcLine.length()) { + if (srcPosition.line < src.size() - 1 || !src.isMissingNewlineAtEnd()) { + throw new IndexOutOfBoundsException("The processLineToColumn shouldn't add end of line"); + } + } + int from = srcPosition.column; + int charCount = to - from; + srcPosition.appendStringWithoutEOLMark(charCount); + if (append) { + sb.append(srcLine, from, to); + dstPosition.appendStringWithoutEOLMark(charCount); + } + } + + void appendLinesToEndOfContent() { + processMultiline(src.size(), true); + } + + void appendReplacement(String replacement) { + if (replacement.length() == 0) { + return; + } + sb.append(replacement); + int lastNewLinePos = -1; + int newLineMarkCount = 0; + while (true) { + int index = replacement.indexOf('\n', lastNewLinePos + 1); + if (index < 0) { + break; + } + lastNewLinePos = index; + newLineMarkCount++; + } + if (newLineMarkCount > 0) { + dstPosition.appendMultilineContent(newLineMarkCount, lastNewLinePos + 1); + } + dstPosition.appendStringWithoutEOLMark(replacement.length() - lastNewLinePos - 1); + } + + boolean hasMoreLines() { + return !endOfSource; + } + + private String getCurrentSrcLine() { + if (currentSrcLine == null) { + currentSrcLine = src.getString(srcPosition.line, srcPosition.line + 1, false); + } + return currentSrcLine; + } + } + + /** The result of applying fix to a file content */ + public static class FixResult { + /** List of edits to transform an original text to a final text (with all fixes applied) */ + public final ImmutableList<Edit> edits; + /** Final text with all applied fixes */ + public final Text text; + + FixResult(ImmutableList<Edit> edits, Text text) { + this.edits = edits; + this.text = text; + } + } +}
diff --git a/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java b/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java index a1682fe..72a5176 100644 --- a/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java +++ b/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java
@@ -18,8 +18,8 @@ import static java.util.stream.Collectors.groupingBy; import com.google.gerrit.common.RawInputUtil; -import com.google.gerrit.entities.Comment; import com.google.gerrit.entities.FixReplacement; +import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.BinaryResult; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; @@ -31,7 +31,6 @@ import com.google.inject.Singleton; import java.io.IOException; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import java.util.Map; import org.eclipse.jgit.lib.ObjectId; @@ -41,9 +40,6 @@ @Singleton public class FixReplacementInterpreter { - private static final Comparator<FixReplacement> ASC_RANGE_FIX_REPLACEMENT_COMPARATOR = - Comparator.comparing(fixReplacement -> fixReplacement.range); - private final FileContentUtil fileContentUtil; @Inject @@ -69,7 +65,8 @@ ProjectState projectState, ObjectId patchSetCommitId, List<FixReplacement> fixReplacements) - throws ResourceNotFoundException, IOException, ResourceConflictException { + throws BadRequestException, ResourceNotFoundException, IOException, + ResourceConflictException { requireNonNull(fixReplacements, "Fix replacements must not be null"); Map<String, List<FixReplacement>> fixReplacementsPerFilePath = @@ -91,62 +88,20 @@ ObjectId patchSetCommitId, String filePath, List<FixReplacement> fixReplacements) - throws ResourceNotFoundException, IOException, ResourceConflictException { + throws BadRequestException, ResourceNotFoundException, IOException, + ResourceConflictException { String fileContent = getFileContent(repository, projectState, patchSetCommitId, filePath); - String newFileContent = getNewFileContent(fileContent, fixReplacements); + String newFileContent = FixCalculator.getNewFileContent(fileContent, fixReplacements); + return new ChangeFileContentModification(filePath, RawInputUtil.create(newFileContent)); } private String getFileContent( Repository repository, ProjectState projectState, ObjectId patchSetCommitId, String filePath) - throws ResourceNotFoundException, IOException { + throws ResourceNotFoundException, BadRequestException, IOException { try (BinaryResult fileContent = fileContentUtil.getContent(repository, projectState, patchSetCommitId, filePath)) { return fileContent.asString(); } } - - private static String getNewFileContent(String fileContent, List<FixReplacement> fixReplacements) - throws ResourceConflictException { - List<FixReplacement> sortedReplacements = new ArrayList<>(fixReplacements); - sortedReplacements.sort(ASC_RANGE_FIX_REPLACEMENT_COMPARATOR); - - LineIdentifier lineIdentifier = new LineIdentifier(fileContent); - StringModifier fileContentModifier = new StringModifier(fileContent); - for (FixReplacement fixReplacement : sortedReplacements) { - Comment.Range range = fixReplacement.range; - try { - int startLineIndex = lineIdentifier.getStartIndexOfLine(range.startLine); - int startLineLength = lineIdentifier.getLengthOfLine(range.startLine); - - int endLineIndex = lineIdentifier.getStartIndexOfLine(range.endLine); - int endLineLength = lineIdentifier.getLengthOfLine(range.endLine); - - if (range.startChar > startLineLength || range.endChar > endLineLength) { - throw new ResourceConflictException( - String.format( - "Range %s refers to a non-existent offset (start line length: %s," - + " end line length: %s)", - toString(range), startLineLength, endLineLength)); - } - - int startIndex = startLineIndex + range.startChar; - int endIndex = endLineIndex + range.endChar; - fileContentModifier.replace(startIndex, endIndex, fixReplacement.replacement); - } catch (StringIndexOutOfBoundsException e) { - // Most of the StringIndexOutOfBoundsException should never occur because we reject fix - // replacements for invalid ranges. However, we can't cover all cases for efficiency - // reasons. For instance, we don't determine the number of lines in a file. That's why we - // need to map this exception and thus provide a meaningful error. - throw new ResourceConflictException( - String.format("Cannot apply fix replacement for range %s", toString(range)), e); - } - } - return fileContentModifier.getResult(); - } - - private static String toString(Comment.Range range) { - return String.format( - "(%s:%s - %s:%s)", range.startLine, range.startChar, range.endLine, range.endChar); - } }
diff --git a/java/com/google/gerrit/server/fixes/LineIdentifier.java b/java/com/google/gerrit/server/fixes/LineIdentifier.java deleted file mode 100644 index 3d09c34..0000000 --- a/java/com/google/gerrit/server/fixes/LineIdentifier.java +++ /dev/null
@@ -1,110 +0,0 @@ -// 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.fixes; - -import static java.util.Objects.requireNonNull; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * An identifier of lines in a string. Lines are sequences of characters which are separated by any - * Unicode linebreak sequence as defined by the regular expression {@code \R}. If data for several - * lines is requested, calls which are ordered according to ascending line numbers are the most - * efficient. - */ -class LineIdentifier { - - private static final Pattern LINE_SEPARATOR_PATTERN = Pattern.compile("\\R"); - private final Matcher lineSeparatorMatcher; - - private int nextLineNumber; - private int nextLineStartIndex; - private int currentLineStartIndex; - private int currentLineEndIndex; - - LineIdentifier(String string) { - requireNonNull(string); - lineSeparatorMatcher = LINE_SEPARATOR_PATTERN.matcher(string); - reset(); - } - - /** - * Returns the start index of the indicated line within the given string. Start indices are - * zero-based while line numbers are one-based. - * - * <p><b>Note:</b> Requesting data for several lines is more efficient if those calls occur with - * increasing line number. - * - * @param lineNumber the line whose start index should be determined - * @return the start index of the line - * @throws StringIndexOutOfBoundsException if the line number is negative, zero or greater than - * the identified number of lines - */ - public int getStartIndexOfLine(int lineNumber) { - findLine(lineNumber); - return currentLineStartIndex; - } - - /** - * Returns the length of the indicated line in the given string. The character(s) used to separate - * lines aren't included in the count. Line numbers are one-based. - * - * <p><b>Note:</b> Requesting data for several lines is more efficient if those calls occur with - * increasing line number. - * - * @param lineNumber the line whose length should be determined - * @return the length of the line - * @throws StringIndexOutOfBoundsException if the line number is negative, zero or greater than - * the identified number of lines - */ - public int getLengthOfLine(int lineNumber) { - findLine(lineNumber); - return currentLineEndIndex - currentLineStartIndex; - } - - private void findLine(int targetLineNumber) { - if (targetLineNumber <= 0) { - throw new StringIndexOutOfBoundsException("Line number must be positive"); - } - if (targetLineNumber < nextLineNumber) { - reset(); - } - while (nextLineNumber < targetLineNumber + 1 && lineSeparatorMatcher.find()) { - currentLineStartIndex = nextLineStartIndex; - currentLineEndIndex = lineSeparatorMatcher.start(); - nextLineStartIndex = lineSeparatorMatcher.end(); - nextLineNumber++; - } - - // End of string - if (nextLineNumber == targetLineNumber) { - currentLineStartIndex = nextLineStartIndex; - currentLineEndIndex = lineSeparatorMatcher.regionEnd(); - } - if (nextLineNumber < targetLineNumber) { - throw new StringIndexOutOfBoundsException( - String.format("Line %d isn't available", targetLineNumber)); - } - } - - private void reset() { - nextLineNumber = 1; - nextLineStartIndex = 0; - currentLineStartIndex = 0; - currentLineEndIndex = 0; - lineSeparatorMatcher.reset(); - } -}
diff --git a/java/com/google/gerrit/server/fixes/testing/BUILD b/java/com/google/gerrit/server/fixes/testing/BUILD new file mode 100644 index 0000000..cccf7a5 --- /dev/null +++ b/java/com/google/gerrit/server/fixes/testing/BUILD
@@ -0,0 +1,19 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package(default_visibility = ["//visibility:public"]) + +java_library( + name = "testing", + testonly = True, + srcs = glob(["*.java"]), + deps = [ + "//java/com/google/gerrit/common:server", + "//java/com/google/gerrit/entities", + "//java/com/google/gerrit/jgit", + "//java/com/google/gerrit/server", + "//java/com/google/gerrit/truth", + "//lib:guava", + "//lib:jgit", + "//lib/truth", + ], +)
diff --git a/java/com/google/gerrit/server/fixes/testing/FixResultSubject.java b/java/com/google/gerrit/server/fixes/testing/FixResultSubject.java new file mode 100644 index 0000000..21f96cd --- /dev/null +++ b/java/com/google/gerrit/server/fixes/testing/FixResultSubject.java
@@ -0,0 +1,49 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.fixes.testing; + +import static com.google.common.truth.Truth.assertAbout; +import static com.google.gerrit.server.fixes.testing.GitEditSubject.gitEdits; +import static com.google.gerrit.truth.ListSubject.elements; + +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.StringSubject; +import com.google.common.truth.Subject; +import com.google.gerrit.server.fixes.FixCalculator.FixResult; +import com.google.gerrit.truth.ListSubject; +import org.eclipse.jgit.diff.Edit; + +public class FixResultSubject extends Subject { + public static FixResultSubject assertThat(FixResult fixResult) { + return assertAbout(FixResultSubject::new).that(fixResult); + } + + private final FixResult fixResult; + + private FixResultSubject(FailureMetadata failureMetadata, FixResult fixResult) { + super(failureMetadata, fixResult); + this.fixResult = fixResult; + } + + public StringSubject text() { + isNotNull(); + return check("text").that(fixResult.text.getString(0, fixResult.text.size(), false)); + } + + public ListSubject<GitEditSubject, Edit> edits() { + isNotNull(); + return check("edits").about(elements()).thatCustom(fixResult.edits, gitEdits()); + } +}
diff --git a/java/com/google/gerrit/server/fixes/testing/GitEditSubject.java b/java/com/google/gerrit/server/fixes/testing/GitEditSubject.java new file mode 100644 index 0000000..53b88b1 --- /dev/null +++ b/java/com/google/gerrit/server/fixes/testing/GitEditSubject.java
@@ -0,0 +1,87 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.fixes.testing; + +import static com.google.common.truth.Truth.assertAbout; +import static com.google.gerrit.truth.ListSubject.elements; + +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.Subject; +import com.google.gerrit.jgit.diff.ReplaceEdit; +import com.google.gerrit.truth.ListSubject; +import org.eclipse.jgit.diff.Edit; +import org.eclipse.jgit.diff.Edit.Type; + +public class GitEditSubject extends Subject { + + public static GitEditSubject assertThat(Edit edit) { + return assertAbout(gitEdits()).that(edit); + } + + public static Subject.Factory<GitEditSubject, Edit> gitEdits() { + return GitEditSubject::new; + } + + private final Edit edit; + + private GitEditSubject(FailureMetadata failureMetadata, Edit edit) { + super(failureMetadata, edit); + this.edit = edit; + } + + public void hasRegions(int beginA, int endA, int beginB, int endB) { + isNotNull(); + check("beginA").that(edit.getBeginA()).isEqualTo(beginA); + check("endA").that(edit.getEndA()).isEqualTo(endA); + check("beginB").that(edit.getBeginB()).isEqualTo(beginB); + check("endB").that(edit.getEndB()).isEqualTo(endB); + } + + public void hasType(Type type) { + isNotNull(); + check("getType").that(edit.getType()).isEqualTo(type); + } + + public void isInsert(int insertPos, int beginB, int insertedLength) { + isNotNull(); + hasType(Type.INSERT); + hasRegions(insertPos, insertPos, beginB, beginB + insertedLength); + } + + public void isDelete(int deletePos, int deletedLength, int posB) { + isNotNull(); + hasType(Type.DELETE); + hasRegions(deletePos, deletePos + deletedLength, posB, posB); + } + + public void isReplace(int originalPos, int originalLength, int newPos, int newLength) { + isNotNull(); + hasType(Type.REPLACE); + hasRegions(originalPos, originalPos + originalLength, newPos, newPos + newLength); + } + + public void isEmpty() { + isNotNull(); + hasType(Type.EMPTY); + } + + public ListSubject<GitEditSubject, Edit> internalEdits() { + isNotNull(); + isInstanceOf(ReplaceEdit.class); + return check("internalEdits") + .about(elements()) + .thatCustom(((ReplaceEdit) edit).getInternalEdits(), gitEdits()); + } +}
diff --git a/java/com/google/gerrit/server/git/BanCommit.java b/java/com/google/gerrit/server/git/BanCommit.java index 4473ab7..242c11b 100644 --- a/java/com/google/gerrit/server/git/BanCommit.java +++ b/java/com/google/gerrit/server/git/BanCommit.java
@@ -20,7 +20,6 @@ import com.google.gerrit.entities.Project; import com.google.gerrit.entities.RefNames; import com.google.gerrit.extensions.restapi.AuthException; -import com.google.gerrit.git.LockFailureException; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.IdentifiedUser; @@ -47,6 +46,12 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; +/** + * Logic for banning commits from being uploaded. + * + * <p>Gerrit has a per-project list of commits that are forbidden to be pushed. This class reads and + * writes the banned commits list in {@code refs/meta/reject-commits}. + */ @Singleton public class BanCommit { /** @@ -91,9 +96,14 @@ this.tz = gerritIdent.getTimeZone(); } + /** + * Bans a list of commits from the given project. + * + * <p>The user must be specified, so it can be checked for the {@code BAN_COMMIT} permission. + */ public BanCommitResult ban( Project.NameKey project, CurrentUser user, List<ObjectId> commitsToBan, String reason) - throws AuthException, LockFailureException, IOException, PermissionBackendException { + throws AuthException, IOException, PermissionBackendException { permissionBackend.user(user).project(project).check(ProjectPermission.BAN_COMMIT); final BanCommitResult result = new BanCommitResult();
diff --git a/java/com/google/gerrit/server/git/BanCommitResult.java b/java/com/google/gerrit/server/git/BanCommitResult.java index 9fadae2..c78123e 100644 --- a/java/com/google/gerrit/server/git/BanCommitResult.java +++ b/java/com/google/gerrit/server/git/BanCommitResult.java
@@ -18,6 +18,7 @@ import java.util.List; import org.eclipse.jgit.lib.ObjectId; +/** The outcome of the {@link com.google.gerrit.server.git.BanCommit} operation. */ public class BanCommitResult { private final List<ObjectId> newlyBannedCommits = new ArrayList<>(4); private final List<ObjectId> alreadyBannedCommits = new ArrayList<>(4);
diff --git a/java/com/google/gerrit/server/git/BranchOrderSection.java b/java/com/google/gerrit/server/git/BranchOrderSection.java index 4c77b61..0266655 100644 --- a/java/com/google/gerrit/server/git/BranchOrderSection.java +++ b/java/com/google/gerrit/server/git/BranchOrderSection.java
@@ -18,6 +18,13 @@ import com.google.gerrit.entities.RefNames; import java.util.List; +/** + * An ordering of branches by stability. + * + * <p>The REST API supports automatically checking if changes on development branches can be merged + * into stable branches. This is configured by the {@code branchOrder.branch} project setting. This + * class represents the ordered list of branches, by increasing stability. + */ public class BranchOrderSection { /**
diff --git a/java/com/google/gerrit/server/git/ChangeReportFormatter.java b/java/com/google/gerrit/server/git/ChangeReportFormatter.java index f897a1d..e0efaef 100644 --- a/java/com/google/gerrit/server/git/ChangeReportFormatter.java +++ b/java/com/google/gerrit/server/git/ChangeReportFormatter.java
@@ -18,6 +18,7 @@ import com.google.gerrit.common.Nullable; import com.google.gerrit.entities.Change; +/** Formatter for git command-line progress messages. */ public interface ChangeReportFormatter { @AutoValue public abstract static class Input {
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java index 476037b..393ccb7 100644 --- a/java/com/google/gerrit/server/git/CommitUtil.java +++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -146,12 +146,14 @@ revertCommitBuilder.setCommitter(authorIdent); Change changeToRevert = notes.getChange(); + String subject = changeToRevert.getSubject(); + if (subject.length() > 63) { + subject = subject.substring(0, 59) + "..."; + } if (message == null) { message = MessageFormat.format( - ChangeMessages.get().revertChangeDefaultMessage, - changeToRevert.getSubject(), - patch.commitId().name()); + ChangeMessages.get().revertChangeDefaultMessage, subject, patch.commitId().name()); } if (generatedChangeId != null) { revertCommitBuilder.setMessage(ChangeIdUtil.insertId(message, generatedChangeId, true));
diff --git a/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java b/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java index 5866c57..4f6094e 100644 --- a/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java +++ b/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
@@ -22,7 +22,7 @@ import com.google.inject.Inject; import java.util.Optional; -/** Print a change description for use in git command-line progress. */ +/** Default formatter for change descriptions for use in git command-line progress. */ public class DefaultChangeReportFormatter implements ChangeReportFormatter { private static final int SUBJECT_MAX_LENGTH = 80; private static final String SUBJECT_CROP_APPENDIX = "...";
diff --git a/java/com/google/gerrit/server/git/DefaultQueueOp.java b/java/com/google/gerrit/server/git/DefaultQueueOp.java index b30acfa..9fb1a9b 100644 --- a/java/com/google/gerrit/server/git/DefaultQueueOp.java +++ b/java/com/google/gerrit/server/git/DefaultQueueOp.java
@@ -17,6 +17,10 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +/** + * Wrapper class so a Runnable can schedule itself onto the Gerrit Workqueue. Subclasses must + * implement the {@code run} method. + */ public abstract class DefaultQueueOp implements Runnable { private final WorkQueue workQueue;
diff --git a/java/com/google/gerrit/server/git/DelegateRefDatabase.java b/java/com/google/gerrit/server/git/DelegateRefDatabase.java index 34dd6a9..decae05 100644 --- a/java/com/google/gerrit/server/git/DelegateRefDatabase.java +++ b/java/com/google/gerrit/server/git/DelegateRefDatabase.java
@@ -17,6 +17,9 @@ import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.Set; +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.RefRename; @@ -24,7 +27,8 @@ import org.eclipse.jgit.lib.Repository; /** - * Wrapper around {@link RefDatabase} that delegates all calls to the wrapped {@link RefDatabase}. + * Wrapper around {@link RefDatabase} that delegates all calls to the wrapped {@link Repository}'s + * {@link RefDatabase}. */ public class DelegateRefDatabase extends RefDatabase { @@ -41,7 +45,7 @@ @Override public void close() { - delegate.close(); + delegate.getRefDatabase().close(); } @Override @@ -71,6 +75,12 @@ } @Override + @NonNull + public Set<Ref> getTipsWithSha1(ObjectId id) throws IOException { + return delegate.getRefDatabase().getTipsWithSha1(id); + } + + @Override public List<Ref> getAdditionalRefs() throws IOException { return delegate.getRefDatabase().getAdditionalRefs(); }
diff --git a/java/com/google/gerrit/server/git/DelegateRepository.java b/java/com/google/gerrit/server/git/DelegateRepository.java index 800490d..b61488b 100644 --- a/java/com/google/gerrit/server/git/DelegateRepository.java +++ b/java/com/google/gerrit/server/git/DelegateRepository.java
@@ -81,7 +81,8 @@ @SuppressWarnings("rawtypes") private static BaseRepositoryBuilder toBuilder(Repository repo) { if (!repo.isBare()) { - throw new IllegalArgumentException("non-bare repository is not supported"); + throw new IllegalArgumentException( + "non-bare repository is not supported: " + repo.getIdentifier()); } return new BaseRepositoryBuilder<>().setFS(repo.getFS()).setGitDir(repo.getDirectory());
diff --git a/java/com/google/gerrit/server/git/GarbageCollection.java b/java/com/google/gerrit/server/git/GarbageCollection.java index 090d439..9b52f48 100644 --- a/java/com/google/gerrit/server/git/GarbageCollection.java +++ b/java/com/google/gerrit/server/git/GarbageCollection.java
@@ -16,6 +16,7 @@ import com.google.common.collect.Sets; import com.google.common.flogger.FluentLogger; +import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.GarbageCollectionResult; import com.google.gerrit.entities.Project; import com.google.gerrit.extensions.events.GarbageCollectorListener; @@ -37,6 +38,7 @@ import org.eclipse.jgit.lib.TextProgressMonitor; import org.eclipse.jgit.storage.pack.PackConfig; +/** Serial execution of GC on a list of repositories. */ public class GarbageCollection { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @@ -69,8 +71,9 @@ return run(projectNames, gcConfig.isAggressive(), writer); } + /** Runs GC on the given projects, serially. Progress is written to writer if non-null. */ public GarbageCollectionResult run( - List<Project.NameKey> projectNames, boolean aggressive, PrintWriter writer) { + List<Project.NameKey> projectNames, boolean aggressive, @Nullable PrintWriter writer) { GarbageCollectionResult result = new GarbageCollectionResult(); Set<Project.NameKey> projectsToGc = gcQueue.addAll(projectNames); for (Project.NameKey projectName :
diff --git a/java/com/google/gerrit/server/git/GarbageCollectionQueue.java b/java/com/google/gerrit/server/git/GarbageCollectionQueue.java index e3a923b..5df9ab5 100644 --- a/java/com/google/gerrit/server/git/GarbageCollectionQueue.java +++ b/java/com/google/gerrit/server/git/GarbageCollectionQueue.java
@@ -21,6 +21,7 @@ import java.util.HashSet; import java.util.Set; +/** A thread-safe list of projects scheduled for GC. */ @Singleton public class GarbageCollectionQueue { private final Set<Project.NameKey> projectsScheduledForGc = new HashSet<>();
diff --git a/java/com/google/gerrit/server/git/GitRepositoryManagerModule.java b/java/com/google/gerrit/server/git/GitRepositoryManagerModule.java index 6e8bab1..354b69f 100644 --- a/java/com/google/gerrit/server/git/GitRepositoryManagerModule.java +++ b/java/com/google/gerrit/server/git/GitRepositoryManagerModule.java
@@ -18,6 +18,10 @@ import com.google.gerrit.server.config.RepositoryConfig; import com.google.inject.Inject; +/** + * Module to install {@link MultiBaseLocalDiskRepositoryManager} rather than {@link + * LocalDiskRepositoryManager} if needed. + */ public class GitRepositoryManagerModule extends LifecycleModule { private final RepositoryConfig repoConfig;
diff --git a/java/com/google/gerrit/server/git/GroupCollector.java b/java/com/google/gerrit/server/git/GroupCollector.java index c284f7f4..1f0dcd4 100644 --- a/java/com/google/gerrit/server/git/GroupCollector.java +++ b/java/com/google/gerrit/server/git/GroupCollector.java
@@ -23,16 +23,18 @@ import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import com.google.common.collect.MultimapBuilder; -import com.google.common.collect.Multimaps; import com.google.common.collect.SetMultimap; import com.google.common.collect.Sets; import com.google.common.collect.SortedSetMultimap; import com.google.common.flogger.FluentLogger; import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.Project; +import com.google.gerrit.entities.RefNames; import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.change.RevisionResource; +import com.google.gerrit.server.git.receive.ReceivePackRefCache; import com.google.gerrit.server.notedb.ChangeNotes; +import java.io.IOException; import java.util.ArrayDeque; import java.util.Collection; import java.util.Deque; @@ -87,24 +89,29 @@ return rsrc.getPatchSet().groups(); } - private interface Lookup { + interface Lookup { List<String> lookup(PatchSet.Id psId); } - private final ListMultimap<ObjectId, PatchSet.Id> patchSetsBySha; + private final ReceivePackRefCache receivePackRefCache; private final ListMultimap<ObjectId, String> groups; private final SetMultimap<String, String> groupAliases; private final Lookup groupLookup; private boolean done; + /** + * Returns a new {@link GroupCollector} instance. + * + * @see GroupCollector for what this class does. + */ public static GroupCollector create( - ListMultimap<ObjectId, Ref> changeRefsById, + ReceivePackRefCache receivePackRefCache, PatchSetUtil psUtil, ChangeNotes.Factory notesFactory, Project.NameKey project) { return new GroupCollector( - transformRefs(changeRefsById), + receivePackRefCache, psId -> { // TODO(dborowitz): Reuse open repository from caller. ChangeNotes notes = notesFactory.createChecked(project, psId.changeId()); @@ -113,31 +120,32 @@ }); } - private GroupCollector(ListMultimap<ObjectId, PatchSet.Id> patchSetsBySha, Lookup groupLookup) { - this.patchSetsBySha = patchSetsBySha; + /** + * Returns a new {@link GroupCollector} instance. + * + * <p>Used in production code by using {@link com.google.gerrit.server.notedb.ChangeNotes.Factory} + * to get a group SHA1 (40 bytes string representation) from a {@link + * com.google.gerrit.entities.PatchSet.Id}. Unit tests use this method directly by passing their + * own lookup function. + * + * @see GroupCollector for what this class does. + */ + @VisibleForTesting + GroupCollector(ReceivePackRefCache receivePackRefCache, Lookup groupLookup) { + this.receivePackRefCache = receivePackRefCache; this.groupLookup = groupLookup; groups = MultimapBuilder.hashKeys().arrayListValues().build(); groupAliases = MultimapBuilder.hashKeys().hashSetValues().build(); } - private static ListMultimap<ObjectId, PatchSet.Id> transformRefs( - ListMultimap<ObjectId, Ref> refs) { - return Multimaps.transformValues(refs, r -> PatchSet.Id.fromRef(r.getName())); - } - - @VisibleForTesting - GroupCollector( - ListMultimap<ObjectId, PatchSet.Id> patchSetsBySha, - ListMultimap<PatchSet.Id, String> groupLookup) { - this( - patchSetsBySha, - psId -> { - List<String> groups = groupLookup.get(psId); - return !groups.isEmpty() ? groups : null; - }); - } - - public void visit(RevCommit c) { + /** + * Process the given {@link RevCommit}. Callers must call {@link #visit(RevCommit)} on all commits + * between the current branch tip and the tip of a push, in reverse topo order (parents before + * children). Once all commits have been visited, call {@link #getGroups()} for the result. + * + * @see GroupCollector for what this class does. + */ + public void visit(RevCommit c) throws IOException { checkState(!done, "visit() called after getGroups()"); Set<RevCommit> interestingParents = getInterestingParents(c); @@ -197,7 +205,10 @@ } } - public SortedSetMultimap<ObjectId, String> getGroups() { + /** + * Returns the groups that got collected from visiting commits using {@link #visit(RevCommit)}. + */ + public SortedSetMultimap<ObjectId, String> getGroups() throws IOException { done = true; SortedSetMultimap<ObjectId, String> result = MultimapBuilder.hashKeys(groups.keySet().size()).treeSetValues().build(); @@ -218,12 +229,13 @@ return result; } - private boolean isGroupFromExistingPatchSet(RevCommit commit, String group) { + private boolean isGroupFromExistingPatchSet(RevCommit commit, String group) throws IOException { ObjectId id = parseGroup(commit, group); - return id != null && patchSetsBySha.containsKey(id); + return id != null && !receivePackRefCache.tipsFromObjectId(id, RefNames.REFS_CHANGES).isEmpty(); } - private Set<String> resolveGroups(ObjectId forCommit, Collection<String> candidates) { + private Set<String> resolveGroups(ObjectId forCommit, Collection<String> candidates) + throws IOException { Set<String> actual = Sets.newTreeSet(); Set<String> done = Sets.newHashSetWithExpectedSize(candidates.size()); Set<String> seen = Sets.newHashSetWithExpectedSize(candidates.size()); @@ -258,16 +270,20 @@ } } - private Iterable<String> resolveGroup(ObjectId forCommit, String group) { + private Iterable<String> resolveGroup(ObjectId forCommit, String group) throws IOException { ObjectId id = parseGroup(forCommit, group); if (id != null) { - PatchSet.Id psId = Iterables.getFirst(patchSetsBySha.get(id), null); - if (psId != null) { - List<String> groups = groupLookup.lookup(psId); - // Group for existing patch set may be missing, e.g. if group has not - // been migrated yet. - if (groups != null && !groups.isEmpty()) { - return groups; + Ref ref = + Iterables.getFirst(receivePackRefCache.tipsFromObjectId(id, RefNames.REFS_CHANGES), null); + if (ref != null) { + PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName()); + if (psId != null) { + List<String> groups = groupLookup.lookup(psId); + // Group for existing patch set may be missing, e.g. if group has not + // been migrated yet. + if (groups != null && !groups.isEmpty()) { + return groups; + } } } }
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java index 8475d03..4495317 100644 --- a/java/com/google/gerrit/server/git/MergeUtil.java +++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -40,6 +40,7 @@ import com.google.gerrit.entities.LabelId; import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.PatchSetApproval; +import com.google.gerrit.exceptions.InvalidMergeStrategyException; import com.google.gerrit.exceptions.StorageException; import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.registration.DynamicSet; @@ -265,7 +266,8 @@ boolean ignoreIdenticalTree, boolean allowConflicts) throws MissingObjectException, IncorrectObjectTypeException, IOException, - MergeIdenticalTreeException, MergeConflictException, MethodNotAllowedException { + MergeIdenticalTreeException, MergeConflictException, MethodNotAllowedException, + InvalidMergeStrategyException { ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig); m.setBase(originalCommit.getParent(parentIndex)); @@ -431,7 +433,8 @@ PersonIdent committerIndent, String commitMsg, RevWalk rw) - throws IOException, MergeIdenticalTreeException, MergeConflictException { + throws IOException, MergeIdenticalTreeException, MergeConflictException, + InvalidMergeStrategyException { if (!MergeStrategy.THEIRS.getName().equals(mergeStrategy) && rw.isMergedInto(originalCommit, mergeTip)) { @@ -745,7 +748,7 @@ BranchNameKey destBranch, CodeReviewCommit mergeTip, CodeReviewCommit n) - throws IntegrationException { + throws IntegrationException, InvalidMergeStrategyException { ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig); try { if (m.merge(mergeTip, n)) { @@ -866,7 +869,8 @@ .collect(joining(",", "Merge changes ", merged.size() > 5 ? ", ..." : "")); } - public ThreeWayMerger newThreeWayMerger(ObjectInserter inserter, Config repoConfig) { + public ThreeWayMerger newThreeWayMerger(ObjectInserter inserter, Config repoConfig) + throws InvalidMergeStrategyException { return newThreeWayMerger(inserter, repoConfig, mergeStrategyName()); } @@ -890,7 +894,8 @@ } public static ThreeWayMerger newThreeWayMerger( - ObjectInserter inserter, Config repoConfig, String strategyName) { + ObjectInserter inserter, Config repoConfig, String strategyName) + throws InvalidMergeStrategyException { Merger m = newMerger(inserter, repoConfig, strategyName); checkArgument( m instanceof ThreeWayMerger, @@ -899,9 +904,12 @@ return (ThreeWayMerger) m; } - public static Merger newMerger(ObjectInserter inserter, Config repoConfig, String strategyName) { + public static Merger newMerger(ObjectInserter inserter, Config repoConfig, String strategyName) + throws InvalidMergeStrategyException { MergeStrategy strategy = MergeStrategy.get(strategyName); - checkArgument(strategy != null, "invalid merge strategy: %s", strategyName); + if (strategy == null) { + throw new InvalidMergeStrategyException(strategyName); + } return strategy.newMerger( new ObjectInserter.Filter() { @Override
diff --git a/java/com/google/gerrit/server/git/MergedByPushOp.java b/java/com/google/gerrit/server/git/MergedByPushOp.java index 858a55a..b272cba 100644 --- a/java/com/google/gerrit/server/git/MergedByPushOp.java +++ b/java/com/google/gerrit/server/git/MergedByPushOp.java
@@ -22,6 +22,7 @@ import com.google.gerrit.entities.LabelId; import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.PatchSetInfo; +import com.google.gerrit.entities.SubmissionId; import com.google.gerrit.server.ChangeMessagesUtil; import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.config.SendEmailExecutor; @@ -44,6 +45,12 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; +/** + * Operation to close a change on push. + * + * <p>When we find a change corresponding to a commit that is pushed to a branch directly, we close + * the change. This class marks the change as merged, and sends out the email notification. + */ public class MergedByPushOp implements BatchUpdateOp { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @@ -51,6 +58,7 @@ MergedByPushOp create( RequestScopePropagator requestScopePropagator, PatchSet.Id psId, + @Assisted SubmissionId submissionId, @Assisted("refName") String refName, @Assisted("mergeResultRevId") String mergeResultRevId); } @@ -64,6 +72,7 @@ private final ChangeMerged changeMerged; private final PatchSet.Id psId; + private final SubmissionId submissionId; private final String refName; private final String mergeResultRevId; @@ -83,6 +92,7 @@ ChangeMerged changeMerged, @Assisted RequestScopePropagator requestScopePropagator, @Assisted PatchSet.Id psId, + @Assisted SubmissionId submissionId, @Assisted("refName") String refName, @Assisted("mergeResultRevId") String mergeResultRevId) { this.patchSetInfoFactory = patchSetInfoFactory; @@ -92,6 +102,7 @@ this.sendEmailExecutor = sendEmailExecutor; this.changeMerged = changeMerged; this.requestScopePropagator = requestScopePropagator; + this.submissionId = submissionId; this.psId = psId; this.refName = refName; this.mergeResultRevId = mergeResultRevId; @@ -132,9 +143,10 @@ } change.setCurrentPatchSet(info); change.setStatus(Change.Status.MERGED); + change.setSubmissionId(submissionId.toString()); // we cannot reconstruct the submit records for when this change was - // submitted, this is why we must fix the status - update.fixStatus(Change.Status.MERGED); + // submitted, this is why we must fix the status and other details. + update.fixStatusToMerged(submissionId); update.setCurrentPatchSet(); if (change.isWorkInProgress()) { change.setWorkInProgress(false);
diff --git a/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java b/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java index 32b5bb8..fd6506a 100644 --- a/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java +++ b/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java
@@ -26,6 +26,12 @@ import java.nio.file.Path; import org.eclipse.jgit.lib.Config; +/** + * RepositoryManager that looks up repos stored across directories. + * + * <p>Each repository has a path configured in Gerrit server config, repository.NAME.basePath, + * indicating where the repo can be found + */ @Singleton public class MultiBaseLocalDiskRepositoryManager extends LocalDiskRepositoryManager {
diff --git a/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java b/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java index b80e543..060000a 100644 --- a/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java +++ b/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
@@ -15,23 +15,28 @@ package com.google.gerrit.server.git; import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions; import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.inject.Inject; import java.io.IOException; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefRename; import org.eclipse.jgit.lib.RefUpdate; @@ -74,10 +79,9 @@ return null; } - Map<String, Ref> result; + Collection<Ref> result; try { - result = - forProject.filter(ImmutableMap.of(name, ref), getDelegate(), RefFilterOptions.defaults()); + result = forProject.filter(ImmutableList.of(ref), getDelegate(), RefFilterOptions.defaults()); } catch (PermissionBackendException e) { if (e.getCause() instanceof IOException) { throw (IOException) e.getCause(); @@ -90,7 +94,7 @@ Preconditions.checkState( result.size() == 1, "Only one element expected, but was: " + result.size()); - return Iterables.getOnlyElement(result.values()); + return Iterables.getOnlyElement(result); } @SuppressWarnings("deprecation") @@ -101,37 +105,27 @@ return refs; } - Map<String, Ref> result; + Collection<Ref> result; try { // The security filtering assumes to receive the same refMap, independently from the ref // prefix offset - result = - forProject.filter( - prefixIndependentRefMap(prefix, refs), getDelegate(), RefFilterOptions.defaults()); + result = forProject.filter(refs.values(), getDelegate(), RefFilterOptions.defaults()); } catch (PermissionBackendException e) { throw new IOException(""); } - return applyPrefixRefMap(prefix, result); + return buildPrefixRefMap(prefix, result); } - private Map<String, Ref> prefixIndependentRefMap(String prefix, Map<String, Ref> refs) { - if (prefix.length() > 0) { - return refs.values().stream().collect(Collectors.toMap(Ref::getName, Function.identity())); - } - - return refs; - } - - private Map<String, Ref> applyPrefixRefMap(String prefix, Map<String, Ref> refs) { + private Map<String, Ref> buildPrefixRefMap(String prefix, Collection<Ref> refs) { int prefixSlashPos = prefix.lastIndexOf('/') + 1; if (prefixSlashPos > 0) { - return refs.values().stream() + return refs.stream() .collect( Collectors.toMap( (Ref ref) -> ref.getName().substring(prefixSlashPos), Function.identity())); } - return refs; + return refs.stream().collect(toMap(Ref::getName, r -> r)); } @Override @@ -182,4 +176,17 @@ } return null; } + + @Override + @NonNull + public Set<Ref> getTipsWithSha1(ObjectId id) throws IOException { + Set<Ref> unfiltered = super.getTipsWithSha1(id); + Set<Ref> result = new HashSet<>(unfiltered.size()); + for (Ref ref : unfiltered) { + if (exactRef(ref.getName()) != null) { + result.add(ref); + } + } + return result; + } }
diff --git a/java/com/google/gerrit/server/git/meta/TabFile.java b/java/com/google/gerrit/server/git/meta/TabFile.java index 4c0378a..f5d7037 100644 --- a/java/com/google/gerrit/server/git/meta/TabFile.java +++ b/java/com/google/gerrit/server/git/meta/TabFile.java
@@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; +/** (De)serializer for tab-delimited text files. */ public class TabFile { @FunctionalInterface public interface Parser {
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java index 7038736..7767fe2 100644 --- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java +++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -310,7 +310,8 @@ allRefsWatcher = new AllRefsWatcher(); receivePack.setAdvertiseRefsHook( - ReceiveCommitsAdvertiseRefsHookChain.create(allRefsWatcher, queryProvider, projectName)); + ReceiveCommitsAdvertiseRefsHookChain.create( + allRefsWatcher, queryProvider, projectName, user.getAccountId())); resultChangeIds = new ResultChangeIds(); receiveCommits = factory.create(
diff --git a/java/com/google/gerrit/server/git/receive/BUILD b/java/com/google/gerrit/server/git/receive/BUILD index d89bb63..7402a37 100644 --- a/java/com/google/gerrit/server/git/receive/BUILD +++ b/java/com/google/gerrit/server/git/receive/BUILD
@@ -2,15 +2,20 @@ java_library( name = "receive", - srcs = glob(["**/*.java"]), + srcs = glob( + ["**/*.java"], + exclude = ["ReceivePackRefCache.java"], + ), visibility = ["//visibility:public"], deps = [ + ":ref_cache", "//java/com/google/gerrit/common:annotations", "//java/com/google/gerrit/common:server", "//java/com/google/gerrit/entities", "//java/com/google/gerrit/exceptions", "//java/com/google/gerrit/extensions:api", "//java/com/google/gerrit/git", + "//java/com/google/gerrit/index", "//java/com/google/gerrit/metrics", "//java/com/google/gerrit/server", "//java/com/google/gerrit/server/logging", @@ -26,3 +31,14 @@ "//lib/guice:guice-assistedinject", ], ) + +java_library( + name = "ref_cache", + srcs = glob(["ReceivePackRefCache.java"]), + visibility = ["//visibility:public"], + deps = [ + "//java/com/google/gerrit/entities", + "//lib:guava", + "//lib:jgit", + ], +)
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java index c6c9b39..e03e0fc 100644 --- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java +++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -43,7 +43,6 @@ import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.google.common.base.Strings; -import com.google.common.base.Throwables; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.google.common.collect.ImmutableList; @@ -74,6 +73,7 @@ import com.google.gerrit.entities.PatchSetInfo; import com.google.gerrit.entities.Project; import com.google.gerrit.entities.RefNames; +import com.google.gerrit.entities.SubmissionId; import com.google.gerrit.exceptions.StorageException; import com.google.gerrit.extensions.api.changes.HashtagsInput; import com.google.gerrit.extensions.api.changes.NotifyHandling; @@ -166,8 +166,6 @@ import com.google.gerrit.server.update.RepoContext; import com.google.gerrit.server.update.RepoOnlyOp; import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryHelper.Action; -import com.google.gerrit.server.update.RetryHelper.ActionType; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.LabelVote; import com.google.gerrit.server.util.MagicBranch; @@ -343,7 +341,6 @@ private final SetPrivateOp.Factory setPrivateOpFactory; // Assisted injected fields. - private final AllRefsWatcher allRefsWatcher; private final ProjectState projectState; private final IdentifiedUser user; private final ReceivePack receivePack; @@ -363,12 +360,9 @@ private final ListMultimap<String, String> errors; private final ListMultimap<String, String> pushOptions; + private final ReceivePackRefCache receivePackRefCache; private final Map<Change.Id, ReplaceRequest> replaceByChange; - // Collections lazily populated during processing. - private ListMultimap<Change.Id, Ref> refsByChange; - private ListMultimap<ObjectId, Ref> refsById; - // Other settings populated during processing. private MagicBranchInput magicBranch; private boolean newChangeForAllNotInTarget; @@ -469,7 +463,6 @@ this.setPrivateOpFactory = setPrivateOpFactory; // Assisted injected fields. - this.allRefsWatcher = allRefsWatcher; this.projectState = projectState; this.user = user; this.receivePack = rp; @@ -501,6 +494,13 @@ this.messageSender = messageSender != null ? messageSender : new ReceivePackMessageSender(); this.resultChangeIds = resultChangeIds; this.loggingTags = ImmutableMap.of(); + + // TODO(hiesel): Make this decision implicit once vetted + boolean useRefCache = config.getBoolean("receive", "enableInMemoryRefCache", true); + receivePackRefCache = + useRefCache + ? ReceivePackRefCache.withAdvertisedRefs(() -> allRefsWatcher.getAllRefs()) + : ReceivePackRefCache.noCache(receivePack.getRepository().getRefDatabase()); } void init() { @@ -653,17 +653,27 @@ Task replaceProgress = progress.beginSubTask("updated", UNKNOWN); List<CreateRequest> newChanges = Collections.emptyList(); - if (magicBranch != null && magicBranch.cmd.getResult() == NOT_ATTEMPTED) { - newChanges = selectNewAndReplacedChangesFromMagicBranch(newProgress); + try { + if (magicBranch != null && magicBranch.cmd.getResult() == NOT_ATTEMPTED) { + try { + newChanges = selectNewAndReplacedChangesFromMagicBranch(newProgress); + } catch (IOException e) { + logger.atSevere().withCause(e).log( + "Failed to select new changes in %s", project.getName()); + return; + } + } + + // Commit validation has already happened, so any changes without Change-Id are for the + // deprecated feature. + warnAboutMissingChangeId(newChanges); + preparePatchSetsForReplace(newChanges); + insertChangesAndPatchSets(newChanges, replaceProgress); + } finally { + newProgress.end(); + replaceProgress.end(); } - // Commit validation has already happened, so any changes without Change-Id are for the - // deprecated feature. - warnAboutMissingChangeId(newChanges); - preparePatchSetsForReplace(newChanges); - insertChangesAndPatchSets(newChanges, replaceProgress); - newProgress.end(); - replaceProgress.end(); queueSuccessMessages(newChanges); logger.atFine().log( @@ -1418,7 +1428,6 @@ private final ProjectState projectState; private final boolean defaultPublishComments; - boolean deprecatedTopicSeen; final ReceiveCommand cmd; final LabelTypes labelTypes; /** @@ -1580,7 +1589,6 @@ IdentifiedUser user, ProjectState projectState, ReceiveCommand cmd, LabelTypes labelTypes) { this.user = user; this.projectState = projectState; - this.deprecatedTopicSeen = false; this.cmd = cmd; this.labelTypes = labelTypes; GeneralPreferencesInfo prefs = user.state().generalPreferences(); @@ -1644,8 +1652,7 @@ /** * returns the destination ref of the magic branch, and populates options in the cmdLineParser. */ - String parse(Repository repo, Set<String> refs, ListMultimap<String, String> pushOptions) - throws CmdLineException { + String parse(ListMultimap<String, String> pushOptions) throws CmdLineException { String ref = RefNames.fullName(MagicBranch.getDestBranchName(cmd.getRefName())); ListMultimap<String, String> options = LinkedListMultimap.create(pushOptions); @@ -1667,28 +1674,7 @@ if (!options.isEmpty()) { cmdLineParser.parseOptionMap(options); } - - // We accept refs/for/BRANCHNAME/TOPIC. Since we don't know - // for sure where the branch ends and the topic starts, look - // backward for a split that works. This behavior is deprecated. - String head = readHEAD(repo); - int split = ref.length(); - for (; ; ) { - String name = ref.substring(0, split); - if (refs.contains(name) || name.equals(head)) { - break; - } - - split = name.lastIndexOf('/', split - 1); - if (split <= Constants.R_REFS.length()) { - return ref; - } - } - if (split < ref.length()) { - topic = Strings.emptyToNull(ref.substring(split + 1)); - deprecatedTopicSeen = true; - } - return ref.substring(0, split); + return ref; } public boolean shouldSetWorkInProgressOnNewChanges() { @@ -1734,7 +1720,7 @@ * * <p>Assumes we are handling a magic branch here. */ - private void parseMagicBranch(ReceiveCommand cmd) throws PermissionBackendException { + private void parseMagicBranch(ReceiveCommand cmd) throws PermissionBackendException, IOException { try (TraceTimer traceTimer = newTimer("parseMagicBranch")) { logger.atFine().log("Found magic branch %s", cmd.getRefName()); MagicBranchInput magicBranch = new MagicBranchInput(user, projectState, cmd, labelTypes); @@ -1743,7 +1729,7 @@ magicBranch.cmdLineParser = optionParserFactory.create(magicBranch); try { - ref = magicBranch.parse(repo, receivePack.getAdvertisedRefs().keySet(), pushOptions); + ref = magicBranch.parse(pushOptions); } catch (CmdLineException e) { if (!magicBranch.cmdLineParser.wasHelpRequestedByOption()) { logger.atFine().log("Invalid branch syntax"); @@ -1775,7 +1761,7 @@ // review to these branches is allowed even if the branch does not exist yet. This allows to // push initial code for review to an empty repository and to review an initial project // configuration. - if (!receivePack.getAdvertisedRefs().containsKey(ref) + if (receivePackRefCache.exactRef(ref) == null && !ref.equals(readHEAD(repo)) && !ref.equals(RefNames.REFS_CONFIG)) { logger.atFine().log("Ref %s not found", ref); @@ -1850,11 +1836,12 @@ reject(cmd, "cannot use merged with base"); return; } - RevCommit branchTip = readBranchTip(magicBranch.dest); - if (branchTip == null) { + Ref refTip = receivePackRefCache.exactRef(magicBranch.dest.branch()); + if (refTip == null) { reject(cmd, magicBranch.dest.branch() + " not found"); return; } + RevCommit branchTip = receivePack.getRevWalk().parseCommit(refTip.getObjectId()); if (!walk.isMergedInto(tip, branchTip)) { reject(cmd, "not merged into branch"); return; @@ -1891,8 +1878,9 @@ } } } else if (newChangeForAllNotInTarget) { - RevCommit branchTip = readBranchTip(magicBranch.dest); - if (branchTip != null) { + Ref refTip = receivePackRefCache.exactRef(magicBranch.dest.branch()); + if (refTip != null) { + RevCommit branchTip = receivePack.getRevWalk().parseCommit(refTip.getObjectId()); magicBranch.baseCommit = Collections.singletonList(branchTip); logger.atFine().log("Set baseCommit = %s", magicBranch.baseCommit.get(0).name()); } else { @@ -1916,13 +1904,6 @@ return; } - if (magicBranch.deprecatedTopicSeen) { - messages.add( - new ValidationMessage( - "WARNING: deprecated topic syntax. Use -o topic=TOPIC instead", false)); - logger.atInfo().log("deprecated topic push seen for project %s", project.getName()); - } - if (validateConnected(magicBranch.cmd, magicBranch.dest, tip)) { this.magicBranch = magicBranch; this.resultChangeIds.setMagicPush(true); @@ -1939,7 +1920,7 @@ newTimer("validateConnected", Metadata.builder().branchName(dest.branch()))) { RevWalk walk = receivePack.getRevWalk(); try { - Ref targetRef = receivePack.getAdvertisedRefs().get(dest.branch()); + Ref targetRef = receivePackRefCache.exactRef(dest.branch()); if (targetRef == null || targetRef.getObjectId() == null) { // The destination branch does not yet exist. Assume the // history being sent for review will start it and thus @@ -1986,14 +1967,6 @@ } } - private RevCommit readBranchTip(BranchNameKey branch) throws IOException { - Ref r = allRefs().get(branch.branch()); - if (r == null) { - return null; - } - return receivePack.getRevWalk().parseCommit(r.getObjectId()); - } - /** * Update an existing change. If draft comments are to be published, these are validated and may * be withheld. @@ -2001,7 +1974,8 @@ * @return True if the command succeeded, false if it was rejected. */ private boolean requestReplaceAndValidateComments( - ReceiveCommand cmd, boolean checkMergedInto, Change change, RevCommit newCommit) { + ReceiveCommand cmd, boolean checkMergedInto, Change change, RevCommit newCommit) + throws IOException { try (TraceTimer traceTimer = newTimer("requestReplaceAndValidateComments")) { if (change.isClosed()) { reject( @@ -2063,14 +2037,14 @@ } } - private List<CreateRequest> selectNewAndReplacedChangesFromMagicBranch(Task newProgress) { + private List<CreateRequest> selectNewAndReplacedChangesFromMagicBranch(Task newProgress) + throws IOException { try (TraceTimer traceTimer = newTimer("selectNewAndReplacedChangesFromMagicBranch")) { logger.atFine().log("Finding new and replaced changes"); List<CreateRequest> newChanges = new ArrayList<>(); - ListMultimap<ObjectId, Ref> existing = changeRefsById(); GroupCollector groupCollector = - GroupCollector.create(changeRefsById(), psUtil, notesFactory, project.getNameKey()); + GroupCollector.create(receivePackRefCache, psUtil, notesFactory, project.getNameKey()); BranchCommitValidator validator = commitValidatorFactory.create(projectState, magicBranch.dest, user); @@ -2111,7 +2085,8 @@ receivePack.getRevWalk().parseBody(c); String name = c.name(); groupCollector.visit(c); - Collection<Ref> existingRefs = existing.get(c); + Collection<Ref> existingRefs = + receivePackRefCache.tipsFromObjectId(c, RefNames.REFS_CHANGES); if (rejectImplicitMerges) { Collections.addAll(mergedParents, c.getParents()); @@ -2275,7 +2250,8 @@ // In case the change look up from the index failed, // double check against the existing refs - if (foundInExistingRef(existing.get(p.commit))) { + if (foundInExistingRef( + receivePackRefCache.tipsFromObjectId(p.commit, RefNames.REFS_CHANGES))) { if (pending.size() == 1) { reject(magicBranch.cmd, "commit(s) already exists (as current patchset)"); return Collections.emptyList(); @@ -2382,7 +2358,7 @@ for (RevCommit c : magicBranch.baseCommit) { receivePack.getRevWalk().markUninteresting(c); } - Ref targetRef = allRefs().get(magicBranch.dest.branch()); + Ref targetRef = receivePackRefCache.exactRef(magicBranch.dest.branch()); if (targetRef != null) { logger.atFine().log( "Marking target ref %s (%s) uninteresting", @@ -2397,7 +2373,7 @@ private void rejectImplicitMerges(Set<RevCommit> mergedParents) throws IOException { try (TraceTimer traceTimer = newTimer("rejectImplicitMerges")) { if (!mergedParents.isEmpty()) { - Ref targetRef = allRefs().get(magicBranch.dest.branch()); + Ref targetRef = receivePackRefCache.exactRef(magicBranch.dest.branch()); if (targetRef != null) { RevWalk rw = receivePack.getRevWalk(); RevCommit tip = rw.parseCommit(targetRef.getObjectId()); @@ -2432,13 +2408,15 @@ // Mark all branch tips as uninteresting in the given revwalk, // so we get only the new commits when walking rw. - private void markHeadsAsUninteresting(RevWalk rw, @Nullable String forRef) { + private void markHeadsAsUninteresting(RevWalk rw, @Nullable String forRef) throws IOException { try (TraceTimer traceTimer = newTimer("markHeadsAsUninteresting", Metadata.builder().branchName(forRef))) { int i = 0; - for (Ref ref : allRefs().values()) { - if ((ref.getName().startsWith(R_HEADS) || ref.getName().equals(forRef)) - && ref.getObjectId() != null) { + for (Ref ref : + Iterables.concat( + receivePackRefCache.byPrefix(R_HEADS), + Collections.singletonList(receivePackRefCache.exactRef(forRef)))) { + if (ref != null && ref.getObjectId() != null) { try { rw.markUninteresting(rw.parseCommit(ref.getObjectId())); i++; @@ -2703,7 +2681,8 @@ ReplaceOp replaceOp; ReplaceRequest( - Change.Id toChange, RevCommit newCommit, ReceiveCommand cmd, boolean checkMergedInto) { + Change.Id toChange, RevCommit newCommit, ReceiveCommand cmd, boolean checkMergedInto) + throws IOException { this.ontoChange = toChange; this.newCommitId = newCommit.copy(); this.inputCommand = requireNonNull(cmd); @@ -2715,11 +2694,12 @@ revCommit = null; } revisions = HashBiMap.create(); - for (Ref ref : refs(toChange)) { + for (Ref ref : receivePackRefCache.byPrefix(RefNames.changeRefPrefix(toChange))) { try { - revisions.forcePut( - receivePack.getRevWalk().parseCommit(ref.getObjectId()), - PatchSet.Id.fromRef(ref.getName())); + PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName()); + if (psId != null) { + revisions.forcePut(receivePack.getRevWalk().parseCommit(ref.getObjectId()), psId); + } } catch (IOException err) { logger.atWarning().withCause(err).log( "Project %s contains invalid change ref %s", project.getName(), ref.getName()); @@ -2785,6 +2765,16 @@ Change change = notes.getChange(); priorPatchSet = change.currentPatchSetId(); if (!revisions.containsValue(priorPatchSet)) { + logger.atWarning().log( + "Change %d is missing revision for patch set %s" + + " (it has revisions for these patch sets: %s)", + change.getChangeId(), + priorPatchSet.getId(), + Iterables.toString( + revisions.values().stream() + .limit(100) // Enough for "normal" changes. + .map(PatchSet.Id::getId) + .collect(Collectors.toList()))); reject(inputCommand, "change " + ontoChange + " missing revisions"); return false; } @@ -2812,11 +2802,16 @@ return false; } - for (Ref r : receivePack.getRepository().getRefDatabase().getRefsByPrefix("refs/changes")) { - if (r.getObjectId().equals(newCommit)) { - reject(inputCommand, "commit already exists (in the project)"); - return false; - } + List<Ref> existingChangesWithSameCommit = + receivePackRefCache.tipsFromObjectId(newCommit, RefNames.REFS_CHANGES); + if (!existingChangesWithSameCommit.isEmpty()) { + // TODO(hiesel, hanwen): Remove this check entirely when Gerrit requires change IDs + // without the option to turn that off. + reject( + inputCommand, + "commit already exists (in the project): " + + existingChangesWithSameCommit.get(0).getName()); + return false; } try (TraceTimer traceTimer2 = newTimer("validateNewPatchSetNoteDb#isMergedInto")) { @@ -2950,14 +2945,20 @@ private void newPatchSet() throws IOException { try (TraceTimer traceTimer = newTimer("newPatchSet")) { RevCommit newCommit = receivePack.getRevWalk().parseCommit(newCommitId); - psId = - ChangeUtil.nextPatchSetIdFromAllRefsMap( - allRefs(), notes.getChange().currentPatchSetId()); + psId = nextPatchSetId(notes.getChange().currentPatchSetId()); info = patchSetInfoFactory.get(receivePack.getRevWalk(), newCommit, psId); cmd = new ReceiveCommand(ObjectId.zeroId(), newCommitId, psId.toRefName()); } } + private PatchSet.Id nextPatchSetId(PatchSet.Id psId) throws IOException { + PatchSet.Id next = ChangeUtil.nextPatchSetId(psId); + while (receivePackRefCache.exactRef(next.toRefName()) != null) { + next = ChangeUtil.nextPatchSetId(next); + } + return next; + } + void addOps(BatchUpdate bu, @Nullable Task progress) throws IOException { try (TraceTimer traceTimer = newTimer("addOps")) { if (magicBranch != null && magicBranch.edit) { @@ -2988,7 +2989,8 @@ info, groups, magicBranch, - receivePack.getPushCertificate()) + receivePack.getPushCertificate(), + notes.getChange()) .setRequestScopePropagator(requestScopePropagator); bu.addOp(notes.getChangeId(), replaceOp); if (progress != null) { @@ -3091,45 +3093,6 @@ } } - private List<Ref> refs(Change.Id changeId) { - return refsByChange().get(changeId); - } - - private void initChangeRefMaps() { - if (refsByChange != null) { - return; - } - - try (TraceTimer traceTimer = newTimer("initChangeRefMaps")) { - int estRefsPerChange = 4; - refsById = MultimapBuilder.hashKeys().arrayListValues().build(); - refsByChange = - MultimapBuilder.hashKeys(allRefs().size() / estRefsPerChange) - .arrayListValues(estRefsPerChange) - .build(); - for (Ref ref : allRefs().values()) { - ObjectId obj = ref.getObjectId(); - if (obj != null) { - PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName()); - if (psId != null) { - refsById.put(obj, ref); - refsByChange.put(psId.changeId(), ref); - } - } - } - } - } - - private ListMultimap<Change.Id, Ref> refsByChange() { - initChangeRefMaps(); - return refsByChange; - } - - private ListMultimap<ObjectId, Ref> changeRefsById() { - initChangeRefMaps(); - return refsById; - } - private static boolean parentsEqual(RevCommit a, RevCommit b) { if (a.getParentCount() != b.getParentCount()) { return false; @@ -3214,7 +3177,6 @@ if (!(parsedObject instanceof RevCommit)) { return; } - ListMultimap<ObjectId, Ref> existing = changeRefsById(); walk.markStart((RevCommit) parsedObject); markHeadsAsUninteresting(walk, cmd.getRefName()); int limit = receiveConfig.maxBatchCommits; @@ -3231,7 +3193,7 @@ "more than %d commits, and %s not set", limit, PUSH_OPTION_SKIP_VALIDATION)); return; } - if (existing.keySet().contains(c)) { + if (!receivePackRefCache.tipsFromObjectId(c, RefNames.REFS_CHANGES).isEmpty()) { continue; } @@ -3261,107 +3223,132 @@ // TODO(dborowitz): Combine this BatchUpdate with the main one in // handleRegularCommands try { - retryHelper.execute( - updateFactory -> { - try (BatchUpdate bu = - updateFactory.create(projectState.getNameKey(), user, TimeUtil.nowTs()); - ObjectInserter ins = repo.newObjectInserter(); - ObjectReader reader = ins.newReader(); - RevWalk rw = new RevWalk(reader)) { - bu.setRepository(repo, rw, ins); - // TODO(dborowitz): Teach BatchUpdate to ignore missing changes. + retryHelper + .changeUpdate( + "autoCloseChanges", + updateFactory -> { + try (BatchUpdate bu = + updateFactory.create(projectState.getNameKey(), user, TimeUtil.nowTs()); + ObjectInserter ins = repo.newObjectInserter(); + ObjectReader reader = ins.newReader(); + RevWalk rw = new RevWalk(reader)) { + bu.setRepository(repo, rw, ins); + // TODO(dborowitz): Teach BatchUpdate to ignore missing changes. - RevCommit newTip = rw.parseCommit(cmd.getNewId()); - BranchNameKey branch = BranchNameKey.create(project.getNameKey(), refName); + RevCommit newTip = rw.parseCommit(cmd.getNewId()); + BranchNameKey branch = BranchNameKey.create(project.getNameKey(), refName); - rw.reset(); - rw.sort(RevSort.REVERSE); - rw.markStart(newTip); - if (!ObjectId.zeroId().equals(cmd.getOldId())) { - rw.markUninteresting(rw.parseCommit(cmd.getOldId())); - } + rw.reset(); + rw.sort(RevSort.REVERSE); + rw.markStart(newTip); + if (!ObjectId.zeroId().equals(cmd.getOldId())) { + rw.markUninteresting(rw.parseCommit(cmd.getOldId())); + } - ListMultimap<ObjectId, Ref> byCommit = changeRefsById(); - Map<Change.Key, ChangeNotes> byKey = null; - List<ReplaceRequest> replaceAndClose = new ArrayList<>(); + Map<Change.Key, ChangeNotes> byKey = null; + List<ReplaceRequest> replaceAndClose = new ArrayList<>(); - int existingPatchSets = 0; - int newPatchSets = 0; - COMMIT: - for (RevCommit c; (c = rw.next()) != null; ) { - rw.parseBody(c); + int existingPatchSets = 0; + int newPatchSets = 0; + SubmissionId submissionId = null; + COMMIT: + for (RevCommit c; (c = rw.next()) != null; ) { + rw.parseBody(c); - for (Ref ref : byCommit.get(c.copy())) { - PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName()); - Optional<ChangeNotes> notes = getChangeNotes(psId.changeId()); - if (notes.isPresent() && notes.get().getChange().getDest().equals(branch)) { - existingPatchSets++; - bu.addOp(notes.get().getChangeId(), setPrivateOpFactory.create(false, null)); + for (Ref ref : + receivePackRefCache.tipsFromObjectId(c.copy(), RefNames.REFS_CHANGES)) { + PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName()); + Optional<ChangeNotes> notes = getChangeNotes(psId.changeId()); + if (notes.isPresent() && notes.get().getChange().getDest().equals(branch)) { + if (submissionId == null) { + submissionId = new SubmissionId(notes.get().getChange()); + } + existingPatchSets++; + bu.addOp( + notes.get().getChangeId(), setPrivateOpFactory.create(false, null)); + bu.addOp( + psId.changeId(), + mergedByPushOpFactory.create( + requestScopePropagator, + psId, + submissionId, + refName, + newTip.getId().getName())); + continue COMMIT; + } + } + + for (String changeId : c.getFooterLines(FooterConstants.CHANGE_ID)) { + if (byKey == null) { + byKey = + retryHelper + .changeIndexQuery( + "queryOpenChangesByKeyByBranch", + q -> openChangesByKeyByBranch(q, branch)) + .call(); + } + + ChangeNotes onto = byKey.get(Change.key(changeId.trim())); + if (onto != null) { + newPatchSets++; + // Hold onto this until we're done with the walk, as the call to + // req.validate below calls isMergedInto which resets the walk. + ReplaceRequest req = + new ReplaceRequest(onto.getChangeId(), c, cmd, false); + req.notes = onto; + replaceAndClose.add(req); + continue COMMIT; + } + } + } + + for (ReplaceRequest req : replaceAndClose) { + Change.Id id = req.notes.getChangeId(); + if (!req.validateNewPatchSetForAutoClose()) { + logger.atFine().log("Not closing %s because validation failed", id); + continue; + } + if (submissionId == null) { + submissionId = new SubmissionId(req.notes.getChange()); + } + req.addOps(bu, null); + bu.addOp(id, setPrivateOpFactory.create(false, null)); bu.addOp( - psId.changeId(), - mergedByPushOpFactory.create( - requestScopePropagator, psId, refName, newTip.getId().getName())); - continue COMMIT; - } - } - - for (String changeId : c.getFooterLines(FooterConstants.CHANGE_ID)) { - if (byKey == null) { - byKey = executeIndexQuery(() -> openChangesByKeyByBranch(branch)); + id, + mergedByPushOpFactory + .create( + requestScopePropagator, + req.psId, + submissionId, + refName, + newTip.getId().getName()) + .setPatchSetProvider(req.replaceOp::getPatchSet)); + bu.addOp(id, new ChangeProgressOp(progress)); + ids.add(id); } - ChangeNotes onto = byKey.get(Change.key(changeId.trim())); - if (onto != null) { - newPatchSets++; - // Hold onto this until we're done with the walk, as the call to - // req.validate below calls isMergedInto which resets the walk. - ReplaceRequest req = new ReplaceRequest(onto.getChangeId(), c, cmd, false); - req.notes = onto; - replaceAndClose.add(req); - continue COMMIT; - } + logger.atFine().log( + "Auto-closing %d changes with existing patch sets and %d with new patch sets", + existingPatchSets, newPatchSets); + bu.execute(); + } catch (IOException | StorageException | PermissionBackendException e) { + logger.atSevere().withCause(e).log("Failed to auto-close changes"); + return null; } - } - for (ReplaceRequest req : replaceAndClose) { - Change.Id id = req.notes.getChangeId(); - if (!req.validateNewPatchSetForAutoClose()) { - logger.atFine().log("Not closing %s because validation failed", id); - continue; - } - req.addOps(bu, null); - bu.addOp(id, setPrivateOpFactory.create(false, null)); - bu.addOp( - id, - mergedByPushOpFactory - .create( - requestScopePropagator, req.psId, refName, newTip.getId().getName()) - .setPatchSetProvider(req.replaceOp::getPatchSet)); - bu.addOp(id, new ChangeProgressOp(progress)); - ids.add(id); - } + // If we are here, we didn't throw UpdateException. Record the result. + // The ordering is indeterminate due to the HashSet; unfortunately, Change.Id + // doesn't + // fit into TreeSet. + ids.stream() + .forEach(id -> resultChangeIds.add(ResultChangeIds.Key.AUTOCLOSED, id)); - logger.atFine().log( - "Auto-closing %d changes with existing patch sets and %d with new patch sets", - existingPatchSets, newPatchSets); - bu.execute(); - } catch (IOException | StorageException | PermissionBackendException e) { - logger.atSevere().withCause(e).log("Failed to auto-close changes"); - return null; - } - - // If we are here, we didn't throw UpdateException. Record the result. - // The ordering is indeterminate due to the HashSet; unfortunately, Change.Id doesn't - // fit into TreeSet. - ids.stream().forEach(id -> resultChangeIds.add(ResultChangeIds.Key.AUTOCLOSED, id)); - - return null; - }, + return null; + }) // Use a multiple of the default timeout to account for inner retries that may otherwise // eat up the whole timeout so that no time is left to retry this outer action. - RetryHelper.options() - .timeout(retryHelper.getDefaultTimeout(ActionType.CHANGE_UPDATE).multipliedBy(5)) - .build()); + .defaultTimeoutMultiplier(5) + .call(); } catch (RestApiException e) { logger.atSevere().withCause(e).log("Can't insert patchset"); } catch (UpdateException e) { @@ -3378,21 +3365,12 @@ } } - private <T> T executeIndexQuery(Action<T> action) { - try (TraceTimer traceTimer = newTimer("executeIndexQuery")) { - return retryHelper.execute( - ActionType.INDEX_QUERY, action, StorageException.class::isInstance); - } catch (Exception e) { - Throwables.throwIfUnchecked(e); - throw new StorageException(e); - } - } - - private Map<Change.Key, ChangeNotes> openChangesByKeyByBranch(BranchNameKey branch) { + private Map<Change.Key, ChangeNotes> openChangesByKeyByBranch( + InternalChangeQuery internalChangeQuery, BranchNameKey branch) { try (TraceTimer traceTimer = newTimer("openChangesByKeyByBranch", Metadata.builder().branchName(branch.branch()))) { Map<Change.Key, ChangeNotes> r = new HashMap<>(); - for (ChangeData cd : queryProvider.get().byBranchOpen(branch)) { + for (ChangeData cd : internalChangeQuery.byBranchOpen(branch)) { try { r.put(cd.change().getKey(), cd.notes()); } catch (NoSuchChangeException e) { @@ -3403,13 +3381,6 @@ } } - // allRefsWatcher hooks into the protocol negotation to get a list of all known refs. - // This is used as a cache of ref -> sha1 values, and to build an inverse index - // of (change => list of refs) and a (SHA1 => refs). - private Map<String, Ref> allRefs() { - return allRefsWatcher.getAllRefs(); - } - private TraceTimer newTimer(String name) { return newTimer(getClass(), name); }
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java index 83bf554..6c1f097 100644 --- a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java +++ b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
@@ -18,14 +18,19 @@ import com.google.common.collect.Sets; import com.google.common.flogger.FluentLogger; +import com.google.gerrit.entities.Account; import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.Project; import com.google.gerrit.entities.RefNames; import com.google.gerrit.exceptions.StorageException; +import com.google.gerrit.index.query.Predicate; import com.google.gerrit.server.git.HookUtil; import com.google.gerrit.server.index.change.ChangeField; import com.google.gerrit.server.query.change.ChangeData; +import com.google.gerrit.server.query.change.ChangeStatusPredicate; import com.google.gerrit.server.query.change.InternalChangeQuery; +import com.google.gerrit.server.query.change.OwnerPredicate; +import com.google.gerrit.server.query.change.ProjectPredicate; import com.google.gerrit.server.util.MagicBranch; import com.google.inject.Provider; import java.io.IOException; @@ -65,11 +70,13 @@ private final Provider<InternalChangeQuery> queryProvider; private final Project.NameKey projectName; + private final Account.Id user; public ReceiveCommitsAdvertiseRefsHook( - Provider<InternalChangeQuery> queryProvider, Project.NameKey projectName) { + Provider<InternalChangeQuery> queryProvider, Project.NameKey projectName, Account.Id user) { this.queryProvider = queryProvider; this.projectName = projectName; + this.user = user; } @Override @@ -90,7 +97,9 @@ private Set<ObjectId> advertiseOpenChanges(Repository repo) throws ServiceMayNotContinueException { - // Advertise some recent open changes, in case a commit is based on one. + // Advertise the user's most recent open changes. It's likely that the user has one of these in + // their local repo and they can serve as starting points to figure out the common ancestor of + // what the client and server have in common. int limit = 32; try { Set<ObjectId> r = Sets.newHashSetWithExpectedSize(limit); @@ -105,7 +114,11 @@ ChangeField.PATCH_SET) .enforceVisibility(true) .setLimit(limit) - .byProjectOpen(projectName)) { + .query( + Predicate.and( + new ProjectPredicate(projectName.get()), + ChangeStatusPredicate.open(), + new OwnerPredicate(user)))) { PatchSet ps = cd.currentPatchSet(); if (ps != null) { // Ensure we actually observed a patch set ref pointing to this
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHookChain.java b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHookChain.java index 76f6b04..fae1401 100644 --- a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHookChain.java +++ b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHookChain.java
@@ -15,6 +15,7 @@ package com.google.gerrit.server.git.receive; import com.google.common.annotations.VisibleForTesting; +import com.google.gerrit.entities.Account; import com.google.gerrit.entities.Project; import com.google.gerrit.server.query.change.InternalChangeQuery; import com.google.inject.Provider; @@ -35,8 +36,9 @@ public static AdvertiseRefsHook create( AllRefsWatcher allRefsWatcher, Provider<InternalChangeQuery> queryProvider, - Project.NameKey projectName) { - return create(allRefsWatcher, queryProvider, projectName, false); + Project.NameKey projectName, + Account.Id user) { + return create(allRefsWatcher, queryProvider, projectName, user, false); } /** @@ -47,18 +49,19 @@ */ @VisibleForTesting public static AdvertiseRefsHook createForTest( - Provider<InternalChangeQuery> queryProvider, Project.NameKey projectName) { - return create(new AllRefsWatcher(), queryProvider, projectName, true); + Provider<InternalChangeQuery> queryProvider, Project.NameKey projectName, Account.Id user) { + return create(new AllRefsWatcher(), queryProvider, projectName, user, true); } private static AdvertiseRefsHook create( AllRefsWatcher allRefsWatcher, Provider<InternalChangeQuery> queryProvider, Project.NameKey projectName, + Account.Id user, boolean skipHackPushNegotiateHook) { List<AdvertiseRefsHook> advHooks = new ArrayList<>(); advHooks.add(allRefsWatcher); - advHooks.add(new ReceiveCommitsAdvertiseRefsHook(queryProvider, projectName)); + advHooks.add(new ReceiveCommitsAdvertiseRefsHook(queryProvider, projectName, user)); if (!skipHackPushNegotiateHook) { advHooks.add(new HackPushNegotiateHook()); }
diff --git a/java/com/google/gerrit/server/git/receive/ReceivePackRefCache.java b/java/com/google/gerrit/server/git/receive/ReceivePackRefCache.java new file mode 100644 index 0000000..376ab2d --- /dev/null +++ b/java/com/google/gerrit/server/git/receive/ReceivePackRefCache.java
@@ -0,0 +1,174 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.git.receive; + +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.MultimapBuilder; +import com.google.gerrit.entities.Change; +import com.google.gerrit.entities.RefNames; +import java.io.IOException; +import java.util.Map; +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; + +/** + * Simple cache for accessing refs by name, prefix or {@link ObjectId}. Intended to be used when + * processing a {@code git push}. + * + * <p>This class is not thread safe. + */ +public interface ReceivePackRefCache { + + /** + * Returns an instance that delegates all calls to the provided {@link RefDatabase}. To be used in + * tests or when the ref database is fast with forward (name to {@link ObjectId}) and inverse + * ({@code ObjectId} to name) lookups. + */ + static ReceivePackRefCache noCache(RefDatabase delegate) { + return new NoCache(delegate); + } + + /** + * Returns an instance that answers calls based on refs previously advertised and captured in + * {@link AllRefsWatcher}. Speeds up inverse lookups by building a {@code Map<ObjectId, + * List<Ref>>} and a {@code Map<Change.Id, List<Ref>>}. + * + * <p>This implementation speeds up lookups when the ref database does not support inverse ({@code + * ObjectId} to name) lookups. + */ + static ReceivePackRefCache withAdvertisedRefs(Supplier<Map<String, Ref>> allRefsSupplier) { + return new WithAdvertisedRefs(allRefsSupplier); + } + + /** Returns a list of refs whose name starts with {@code prefix} that point to {@code id}. */ + ImmutableList<Ref> tipsFromObjectId(ObjectId id, @Nullable String prefix) throws IOException; + + /** Returns all refs whose name starts with {@code prefix}. */ + ImmutableList<Ref> byPrefix(String prefix) throws IOException; + + /** Returns a ref whose name matches {@code ref} or {@code null} if such a ref does not exist. */ + @Nullable + Ref exactRef(String ref) throws IOException; + + class NoCache implements ReceivePackRefCache { + private final RefDatabase delegate; + + private NoCache(RefDatabase delegate) { + this.delegate = delegate; + } + + @Override + public ImmutableList<Ref> tipsFromObjectId(ObjectId id, @Nullable String prefix) + throws IOException { + return delegate.getTipsWithSha1(id).stream() + .filter(r -> prefix == null || r.getName().startsWith(prefix)) + .collect(toImmutableList()); + } + + @Override + public ImmutableList<Ref> byPrefix(String prefix) throws IOException { + return delegate.getRefsByPrefix(prefix).stream().collect(toImmutableList()); + } + + @Override + @Nullable + public Ref exactRef(String name) throws IOException { + return delegate.exactRef(name); + } + } + + class WithAdvertisedRefs implements ReceivePackRefCache { + /** We estimate that a change has an average of 4 patch sets plus the meta ref. */ + private static final int ESTIMATED_NUMBER_OF_REFS_PER_CHANGE = 5; + + private final Supplier<Map<String, Ref>> allRefsSupplier; + + // Collections lazily populated during processing. + private Map<String, Ref> allRefs; + /** Contains only patch set refs. */ + private ListMultimap<Change.Id, Ref> refsByChange; + /** Contains all refs. */ + private ListMultimap<ObjectId, Ref> refsByObjectId; + + private WithAdvertisedRefs(Supplier<Map<String, Ref>> allRefsSupplier) { + this.allRefsSupplier = allRefsSupplier; + } + + @Override + public ImmutableList<Ref> tipsFromObjectId(ObjectId id, String prefix) { + lazilyInitRefMaps(); + return refsByObjectId.get(id).stream() + .filter(r -> prefix == null || r.getName().startsWith(prefix)) + .collect(toImmutableList()); + } + + @Override + public ImmutableList<Ref> byPrefix(String prefix) { + lazilyInitRefMaps(); + if (RefNames.isRefsChanges(prefix)) { + Change.Id cId = Change.Id.fromRefPart(prefix); + if (cId != null) { + return refsByChange.get(cId).stream() + .filter(r -> r.getName().startsWith(prefix)) + .collect(toImmutableList()); + } + } + return allRefs().values().stream() + .filter(r -> r.getName().startsWith(prefix)) + .collect(toImmutableList()); + } + + @Override + @Nullable + public Ref exactRef(String name) { + return allRefs().get(name); + } + + private Map<String, Ref> allRefs() { + if (allRefs == null) { + allRefs = allRefsSupplier.get(); + } + return allRefs; + } + + private void lazilyInitRefMaps() { + if (refsByChange != null) { + return; + } + + refsByObjectId = MultimapBuilder.hashKeys().arrayListValues().build(); + refsByChange = + MultimapBuilder.hashKeys(allRefs().size() / ESTIMATED_NUMBER_OF_REFS_PER_CHANGE) + .arrayListValues(ESTIMATED_NUMBER_OF_REFS_PER_CHANGE) + .build(); + for (Ref ref : allRefs().values()) { + ObjectId objectId = ref.getObjectId(); + if (objectId != null) { + refsByObjectId.put(objectId, ref); + Change.Id changeId = Change.Id.fromRef(ref.getName()); + if (changeId != null) { + refsByChange.put(changeId, ref); + } + } + } + } + } +}
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java index e95cf3b..24154d60 100644 --- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java +++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -36,6 +36,7 @@ import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.PatchSetApproval; import com.google.gerrit.entities.PatchSetInfo; +import com.google.gerrit.entities.SubmissionId; import com.google.gerrit.extensions.api.changes.AddReviewerInput; import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.client.ChangeKind; @@ -109,7 +110,8 @@ PatchSetInfo info, List<String> groups, @Nullable MagicBranchInput magicBranch, - @Nullable PushCertificate pushCertificate); + @Nullable PushCertificate pushCertificate, + Change change); } private static final String CHANGE_IS_CLOSED = "change is closed"; @@ -142,6 +144,7 @@ private final PatchSetInfo info; private final MagicBranchInput magicBranch; private final PushCertificate pushCertificate; + private final Change change; private List<String> groups; private final Map<String, Short> approvals = new HashMap<>(); @@ -176,6 +179,7 @@ ProjectCache projectCache, @SendEmailExecutor ExecutorService sendEmailExecutor, ReviewerAdder reviewerAdder, + Change change, @Assisted ProjectState projectState, @Assisted BranchNameKey dest, @Assisted boolean checkMergedInto, @@ -217,6 +221,7 @@ this.groups = groups; this.magicBranch = magicBranch; this.pushCertificate = pushCertificate; + this.change = change; } @Override @@ -236,7 +241,11 @@ if (mergedInto != null) { mergedByPushOp = mergedByPushOpFactory.create( - requestScopePropagator, patchSetId, mergedInto, mergeResultRevId); + requestScopePropagator, + patchSetId, + new SubmissionId(change), + mergedInto, + mergeResultRevId); } }
diff --git a/java/com/google/gerrit/server/git/validators/AccountValidator.java b/java/com/google/gerrit/server/git/validators/AccountValidator.java index c6af49c..4755f5f 100644 --- a/java/com/google/gerrit/server/git/validators/AccountValidator.java +++ b/java/com/google/gerrit/server/git/validators/AccountValidator.java
@@ -36,6 +36,11 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevWalk; +/** + * Validator that is used to ensure that new commits on any ref in {@code refs/users} are conforming + * to the NoteDb format for accounts. Used when a user pushes to one of the refs in {@code + * refs/users} manually. + */ public class AccountValidator { private final Provider<IdentifiedUser> self; @@ -52,6 +57,10 @@ this.emailValidator = emailValidator; } + /** + * Returns a list of validation messages. An empty list means that there were no issues found. If + * the list is non-empty, the commit will be rejected. + */ public List<String> validate( Account.Id accountId, Repository allUsersRepo,
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidationException.java b/java/com/google/gerrit/server/git/validators/CommitValidationException.java index bffe382..4dd2951 100644 --- a/java/com/google/gerrit/server/git/validators/CommitValidationException.java +++ b/java/com/google/gerrit/server/git/validators/CommitValidationException.java
@@ -18,6 +18,11 @@ import com.google.gerrit.server.validators.ValidationException; import java.util.List; +/** + * Exception thrown when a Git commit fails validations. Gerrit supports a wide range of validations + * (for example it validates any commits pushed to NoteDb refs for format compliance or allows to + * enforce commit message lengths to not exceed a certain length). + */ public class CommitValidationException extends ValidationException { private static final long serialVersionUID = 1L; private final ImmutableList<CommitValidationMessage> messages; @@ -42,11 +47,12 @@ this.messages = ImmutableList.of(); } + /** Returns all validation messages individually. */ public ImmutableList<CommitValidationMessage> getMessages() { return messages; } - /** @return the reason string along with all validation messages. */ + /** Returns all validation as a single, formatted string. */ public String getFullMessage() { StringBuilder sb = new StringBuilder(getMessage()); if (!messages.isEmpty()) {
diff --git a/java/com/google/gerrit/server/git/validators/MergeValidators.java b/java/com/google/gerrit/server/git/validators/MergeValidators.java index 9caef4f..8abe5c6 100644 --- a/java/com/google/gerrit/server/git/validators/MergeValidators.java +++ b/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -52,6 +52,15 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevWalk; +/** + * Collection of validators that run inside Gerrit before a change is submitted. The main purpose is + * to ensure that NoteDb data is mutated in a controlled way. + * + * <p>The difference between this and {@link OnSubmitValidators} is that this validates the original + * commit. Depending on the {@link com.google.gerrit.server.submit.SubmitStrategy} that the project + * chooses, the resulting commit in the repo might differ from this original commit. In case you + * want to validate the resulting commit, use {@link OnSubmitValidators} + */ public class MergeValidators { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @@ -76,6 +85,10 @@ this.groupValidatorFactory = groupValidatorFactory; } + /** + * Runs all validators and throws a {@link MergeValidationException} for the first validator that + * failed. Only the first violation is propagated and processing is stopped thereafter. + */ public void validatePreMerge( Repository repo, CodeReviewCommit commit, @@ -96,6 +109,7 @@ } } + /** Validator for any commits to {@code refs/meta/config}. */ public static class ProjectConfigValidator implements MergeValidationListener { private static final String INVALID_CONFIG = "Change contains an invalid project configuration."; @@ -237,7 +251,7 @@ } } - /** Execute merge validation plug-ins */ + /** Validator that calls to plugins that provide additional validators. */ public static class PluginMergeValidationListener implements MergeValidationListener { private final PluginSetContext<MergeValidationListener> mergeValidationListeners; @@ -318,6 +332,7 @@ } } + /** Validator to ensure that group refs are not mutated. */ public static class GroupMergeValidator implements MergeValidationListener { public interface Factory { GroupMergeValidator create();
diff --git a/java/com/google/gerrit/server/git/validators/RefOperationValidationException.java b/java/com/google/gerrit/server/git/validators/RefOperationValidationException.java index d27cc38..9fbca00 100644 --- a/java/com/google/gerrit/server/git/validators/RefOperationValidationException.java +++ b/java/com/google/gerrit/server/git/validators/RefOperationValidationException.java
@@ -19,6 +19,10 @@ import com.google.common.collect.ImmutableList; import com.google.gerrit.server.validators.ValidationException; +/** + * Exception to be thrown when the validation of a ref operation fails and should be aborted. + * Examples of a ref operations include creating or updating refs. + */ public class RefOperationValidationException extends ValidationException { private static final long serialVersionUID = 1L; private final ImmutableList<ValidationMessage> messages;
diff --git a/java/com/google/gerrit/server/git/validators/RefOperationValidators.java b/java/com/google/gerrit/server/git/validators/RefOperationValidators.java index 3d1eeef..f3d8f4a 100644 --- a/java/com/google/gerrit/server/git/validators/RefOperationValidators.java +++ b/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
@@ -36,6 +36,12 @@ import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.transport.ReceiveCommand; +/** + * Collection of validation listeners that are called before a ref update is performed with the + * command to be run. This is called from the git push path as well as Gerrit's handlers for + * creating or deleting refs. Calls out to {@link RefOperationValidationListener} provided by + * plugins. + */ public class RefOperationValidators { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @@ -70,6 +76,11 @@ event.user = user; } + /** + * Returns informational validation messages and throws a {@link RefOperationValidationException} + * when the first validator fails. Will not process any more validators after the first failure + * was encountered. + */ public List<ValidationMessage> validateForRefOperation() throws RefOperationValidationException { List<ValidationMessage> messages = new ArrayList<>(); boolean withException = false;
diff --git a/java/com/google/gerrit/server/git/validators/UploadValidationException.java b/java/com/google/gerrit/server/git/validators/UploadValidationException.java index be264b64..da2fb98 100644 --- a/java/com/google/gerrit/server/git/validators/UploadValidationException.java +++ b/java/com/google/gerrit/server/git/validators/UploadValidationException.java
@@ -16,6 +16,7 @@ import org.eclipse.jgit.transport.ServiceMayNotContinueException; +/** Exception to be thrown when an {@link UploadValidationListener} fails. */ public class UploadValidationException extends ServiceMayNotContinueException { private static final long serialVersionUID = 1L;
diff --git a/java/com/google/gerrit/server/git/validators/UploadValidators.java b/java/com/google/gerrit/server/git/validators/UploadValidators.java index 6847a28..60360a2 100644 --- a/java/com/google/gerrit/server/git/validators/UploadValidators.java +++ b/java/com/google/gerrit/server/git/validators/UploadValidators.java
@@ -26,6 +26,7 @@ import org.eclipse.jgit.transport.ServiceMayNotContinueException; import org.eclipse.jgit.transport.UploadPack; +/** Collection of validators to run before Gerrit sends pack data to a client. */ public class UploadValidators implements PreUploadHook { private final PluginSetContext<UploadValidationListener> uploadValidationListeners;
diff --git a/java/com/google/gerrit/server/git/validators/ValidationMessage.java b/java/com/google/gerrit/server/git/validators/ValidationMessage.java index db59492..faf29fe 100644 --- a/java/com/google/gerrit/server/git/validators/ValidationMessage.java +++ b/java/com/google/gerrit/server/git/validators/ValidationMessage.java
@@ -14,6 +14,10 @@ package com.google.gerrit.server.git.validators; +/** + * Message used as result of a validation that run during a git operation (for example {@code git + * push}. Intended to be shown to users. + */ public class ValidationMessage { public enum Type { ERROR("ERROR: "), @@ -35,24 +39,34 @@ private final String message; private final Type type; + /** @see ValidationMessage */ public ValidationMessage(String message, Type type) { this.message = message; this.type = type; } + // TODO: Remove and move callers to ValidationMessage(String message, Type type) public ValidationMessage(String message, boolean isError) { this.message = message; this.type = (isError ? Type.ERROR : Type.OTHER); } + /** Returns the message to be shown to the user. */ public String getMessage() { return message; } + /** + * Returns the {@link Type}. Used to as prefix for the message in the git CLI and to color + * messages. + */ public Type getType() { return type; } + /** + * Returns {@true} if this message is an error. Used to decide if the operation should be aborted. + */ public boolean isError() { return type == Type.ERROR; }
diff --git a/java/com/google/gerrit/server/group/db/GroupsUpdate.java b/java/com/google/gerrit/server/group/db/GroupsUpdate.java index 7f1ba6a..4ec5c36 100644 --- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java +++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -26,7 +26,6 @@ import com.google.gerrit.entities.Project; import com.google.gerrit.exceptions.DuplicateKeyException; import com.google.gerrit.exceptions.NoSuchGroupException; -import com.google.gerrit.git.LockFailureException; import com.google.gerrit.git.RefUpdateUtil; import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.IdentifiedUser; @@ -309,10 +308,9 @@ InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate) throws IOException, ConfigInvalidException, DuplicateKeyException { try { - return retryHelper.execute( - RetryHelper.ActionType.GROUP_UPDATE, - () -> createGroupInNoteDb(groupCreation, groupUpdate), - LockFailureException.class::isInstance); + return retryHelper + .groupUpdate("createGroup", () -> createGroupInNoteDb(groupCreation, groupUpdate)) + .call(); } catch (Exception e) { Throwables.throwIfUnchecked(e); Throwables.throwIfInstanceOf(e, IOException.class); @@ -349,10 +347,9 @@ AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate) throws IOException, ConfigInvalidException, DuplicateKeyException, NoSuchGroupException { try { - return retryHelper.execute( - RetryHelper.ActionType.GROUP_UPDATE, - () -> updateGroupInNoteDb(groupUuid, groupUpdate), - LockFailureException.class::isInstance); + return retryHelper + .groupUpdate("updateGroup", () -> updateGroupInNoteDb(groupUuid, groupUpdate)) + .call(); } catch (Exception e) { Throwables.throwIfUnchecked(e); Throwables.throwIfInstanceOf(e, IOException.class);
diff --git a/java/com/google/gerrit/server/index/AbstractIndexModule.java b/java/com/google/gerrit/server/index/AbstractIndexModule.java index 995b4b6..352971f 100644 --- a/java/com/google/gerrit/server/index/AbstractIndexModule.java +++ b/java/com/google/gerrit/server/index/AbstractIndexModule.java
@@ -28,6 +28,10 @@ import java.util.Map; import org.eclipse.jgit.lib.Config; +/** + * Base class to establish implementation-independent index bindings. To be subclassed by concrete + * index implementations, such as {@link com.google.gerrit.lucene.LuceneIndexModule}. + */ public abstract class AbstractIndexModule extends AbstractModule { private final int threads;
diff --git a/java/com/google/gerrit/server/index/DummyIndexModule.java b/java/com/google/gerrit/server/index/DummyIndexModule.java deleted file mode 100644 index 8b450bc..0000000 --- a/java/com/google/gerrit/server/index/DummyIndexModule.java +++ /dev/null
@@ -1,70 +0,0 @@ -// Copyright (C) 2014 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.index; - -import com.google.gerrit.index.Index; -import com.google.gerrit.index.IndexConfig; -import com.google.gerrit.index.Schema; -import com.google.gerrit.index.project.ProjectData; -import com.google.gerrit.index.project.ProjectIndex; -import com.google.gerrit.server.account.AccountState; -import com.google.gerrit.server.group.InternalGroup; -import com.google.gerrit.server.index.account.AccountIndex; -import com.google.gerrit.server.index.change.ChangeIndex; -import com.google.gerrit.server.index.change.DummyChangeIndex; -import com.google.gerrit.server.index.group.GroupIndex; -import com.google.gerrit.server.query.change.ChangeData; -import com.google.inject.AbstractModule; - -public class DummyIndexModule extends AbstractModule { - private static class DummyChangeIndexFactory implements ChangeIndex.Factory { - @Override - public ChangeIndex create(Schema<ChangeData> schema) { - throw new UnsupportedOperationException(); - } - } - - private static class DummyAccountIndexFactory implements AccountIndex.Factory { - @Override - public AccountIndex create(Schema<AccountState> schema) { - throw new UnsupportedOperationException(); - } - } - - private static class DummyGroupIndexFactory implements GroupIndex.Factory { - @Override - public GroupIndex create(Schema<InternalGroup> schema) { - throw new UnsupportedOperationException(); - } - } - - private static class DummyProjectIndexFactory implements ProjectIndex.Factory { - @Override - public ProjectIndex create(Schema<ProjectData> schema) { - throw new UnsupportedOperationException(); - } - } - - @Override - protected void configure() { - install(new IndexModule(1, true)); - bind(IndexConfig.class).toInstance(IndexConfig.createDefault()); - bind(Index.class).toInstance(new DummyChangeIndex()); - bind(AccountIndex.Factory.class).toInstance(new DummyAccountIndexFactory()); - bind(ChangeIndex.Factory.class).toInstance(new DummyChangeIndexFactory()); - bind(GroupIndex.Factory.class).toInstance(new DummyGroupIndexFactory()); - bind(ProjectIndex.Factory.class).toInstance(new DummyProjectIndexFactory()); - } -}
diff --git a/java/com/google/gerrit/server/index/GerritIndexStatus.java b/java/com/google/gerrit/server/index/GerritIndexStatus.java index d835227..6d59100 100644 --- a/java/com/google/gerrit/server/index/GerritIndexStatus.java +++ b/java/com/google/gerrit/server/index/GerritIndexStatus.java
@@ -22,6 +22,10 @@ import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.util.FS; +/** + * Status to decide if a specific index version (e.g. change v55) is initialized and ready for use. + * An index version is ready for use after documents for all entities were created. + */ public class GerritIndexStatus { private static final String SECTION = "index"; private static final String KEY_READY = "ready";
diff --git a/java/com/google/gerrit/server/index/IndexUtils.java b/java/com/google/gerrit/server/index/IndexUtils.java index a45777e..9e3d91c 100644 --- a/java/com/google/gerrit/server/index/IndexUtils.java +++ b/java/com/google/gerrit/server/index/IndexUtils.java
@@ -19,7 +19,6 @@ import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID_STR; import static com.google.gerrit.server.index.change.ChangeField.PROJECT; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.gerrit.exceptions.StorageException; @@ -36,10 +35,10 @@ import java.util.Set; import org.eclipse.jgit.errors.ConfigInvalidException; +/** Set of index-related utility methods. */ public final class IndexUtils { - public static final ImmutableMap<String, String> CUSTOM_CHAR_MAPPING = - ImmutableMap.of("_", " ", ".", " "); + /** Mark an index version as ready to serve queries. */ public static void setReady(SitePaths sitePaths, String name, int version, boolean ready) { try { GerritIndexStatus cfg = new GerritIndexStatus(sitePaths); @@ -50,25 +49,31 @@ } } - public static boolean getReady(SitePaths sitePaths, String name, int version) { - try { - GerritIndexStatus cfg = new GerritIndexStatus(sitePaths); - return cfg.getReady(name, version); - } catch (ConfigInvalidException | IOException e) { - throw new StorageException(e); - } - } - + /** + * Returns a sanitized set of fields for account index queries by removing fields that the current + * index version doesn't support and accounting for numeric vs. string primary keys. The primary + * key situation is temporary and should be removed after the migration is done. + */ public static Set<String> accountFields(QueryOptions opts, boolean useLegacyNumericFields) { return accountFields(opts.fields(), useLegacyNumericFields); } + /** + * Returns a sanitized set of fields for account index queries by removing fields that the current + * index version doesn't support and accounting for numeric vs. string primary keys. The primary + * key situation is temporary and should be removed after the migration is done. + */ public static Set<String> accountFields(Set<String> fields, boolean useLegacyNumericFields) { String idFieldName = useLegacyNumericFields ? AccountField.ID.getName() : AccountField.ID_STR.getName(); return fields.contains(idFieldName) ? fields : Sets.union(fields, ImmutableSet.of(idFieldName)); } + /** + * Returns a sanitized set of fields for change index queries by removing fields that the current + * index version doesn't support and accounting for numeric vs. string primary keys. The primary + * key situation is temporary and should be removed after the migration is done. + */ public static Set<String> changeFields(QueryOptions opts, boolean useLegacyNumericFields) { FieldDef<ChangeData, ?> idField = useLegacyNumericFields ? LEGACY_ID : LEGACY_ID_STR; // Ensure we request enough fields to construct a ChangeData. We need both @@ -85,6 +90,11 @@ return Sets.union(fs, ImmutableSet.of(idField.getName(), PROJECT.getName())); } + /** + * Returns a sanitized set of fields for group index queries by removing fields that the index + * doesn't support and accounting for numeric vs. string primary keys. The primary key situation + * is temporary and should be removed after the migration is done. + */ public static Set<String> groupFields(QueryOptions opts) { Set<String> fs = opts.fields(); return fs.contains(GroupField.UUID.getName()) @@ -92,6 +102,7 @@ : Sets.union(fs, ImmutableSet.of(GroupField.UUID.getName())); } + /** Returns a index-friendly representation of a {@link CurrentUser} to be used in queries. */ public static String describe(CurrentUser user) { if (user.isIdentifiedUser()) { return user.getAccountId().toString(); @@ -102,6 +113,10 @@ return user.toString(); } + /** + * Returns a sanitized set of fields for project index queries by removing fields that the index + * doesn't support. + */ public static Set<String> projectFields(QueryOptions opts) { Set<String> fs = opts.fields(); return fs.contains(ProjectField.NAME.getName())
diff --git a/java/com/google/gerrit/server/index/OnlineReindexMode.java b/java/com/google/gerrit/server/index/OnlineReindexMode.java index 123229a..d935186 100644 --- a/java/com/google/gerrit/server/index/OnlineReindexMode.java +++ b/java/com/google/gerrit/server/index/OnlineReindexMode.java
@@ -16,6 +16,7 @@ import java.util.Optional; +/** Per-thread singleton to signal if online reindexing is in progress. */ public class OnlineReindexMode { private static ThreadLocal<Boolean> isOnlineReindex = new ThreadLocal<>();
diff --git a/java/com/google/gerrit/server/index/OnlineReindexer.java b/java/com/google/gerrit/server/index/OnlineReindexer.java index 37677bdd..eef394d 100644 --- a/java/com/google/gerrit/server/index/OnlineReindexer.java +++ b/java/com/google/gerrit/server/index/OnlineReindexer.java
@@ -26,6 +26,11 @@ import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +/** + * Background thread for running an index schema upgrade by reindexing all documents in an index + * using the new version. Intended to be run while Gerrit is serving traffic to prepare for a + * near-zero downtime upgrade. + */ public class OnlineReindexer<K, V, I extends Index<K, V>> { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @@ -51,37 +56,38 @@ this.listeners = listeners; } + /** Starts the background process. */ public void start() { if (running.compareAndSet(false, true)) { Thread t = - new Thread() { - @Override - public void run() { - boolean ok = false; - try { - reindex(); - ok = true; - } catch (RuntimeException e) { - logger.atSevere().withCause(e).log( - "Online reindex of %s schema version %s failed", name, version(index)); - } finally { - running.set(false); - if (!ok) { - listeners.runEach(listener -> listener.onFailure(name, oldVersion, newVersion)); + new Thread( + () -> { + boolean ok = false; + try { + reindex(); + ok = true; + } catch (RuntimeException e) { + logger.atSevere().withCause(e).log( + "Online reindex of %s schema version %s failed", name, version(index)); + } finally { + running.set(false); + if (!ok) { + listeners.runEach(listener -> listener.onFailure(name, oldVersion, newVersion)); + } } - } - } - }; + }); t.setName( String.format("Reindex %s v%d-v%d", name, version(indexes.getSearchIndex()), newVersion)); t.start(); } } + /** Returns {@code true} if the background indexer is currently running. */ public boolean isRunning() { return running.get(); } + /** Returns the index version that this indexer is creating documents for. */ public int getVersion() { return newVersion; } @@ -116,6 +122,10 @@ listeners.runEach(listener -> listener.onSuccess(name, oldVersion, newVersion)); } + /** + * Switches the search index from the old version to the new version. This method should be called + * when the new version is fully ready. + */ public void activateIndex() { indexes.setSearchIndex(index); logger.atInfo().log("Using %s schema version %s", name, version(index));
diff --git a/java/com/google/gerrit/server/index/ReindexerAlreadyRunningException.java b/java/com/google/gerrit/server/index/ReindexerAlreadyRunningException.java index 8bf99a5..1b5404b 100644 --- a/java/com/google/gerrit/server/index/ReindexerAlreadyRunningException.java +++ b/java/com/google/gerrit/server/index/ReindexerAlreadyRunningException.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.index; +/** Exception to be thrown when attempting to start a second reindex job. */ public class ReindexerAlreadyRunningException extends Exception { private static final long serialVersionUID = 1L;
diff --git a/java/com/google/gerrit/server/index/SingleVersionModule.java b/java/com/google/gerrit/server/index/SingleVersionModule.java index e3f9d7a..50dc4e9 100644 --- a/java/com/google/gerrit/server/index/SingleVersionModule.java +++ b/java/com/google/gerrit/server/index/SingleVersionModule.java
@@ -33,6 +33,10 @@ import java.util.Set; import org.eclipse.jgit.lib.Config; +/** + * Module that installs a listener to Gerrit's lifecycle events to specify which index versions to + * use. + */ @Singleton public class SingleVersionModule extends LifecycleModule { public static final String SINGLE_VERSIONS = "IndexModule/SingleVersions"; @@ -51,6 +55,7 @@ .toProvider(Providers.of(singleVersions)); } + /** Listener to Gerrit's lifecycle events to specify which index versions to use. */ @Singleton public static class SingleVersionListener implements LifecycleListener { private final Set<String> disabled;
diff --git a/java/com/google/gerrit/server/index/StalenessCheckResult.java b/java/com/google/gerrit/server/index/StalenessCheckResult.java new file mode 100644 index 0000000..fe35e6e --- /dev/null +++ b/java/com/google/gerrit/server/index/StalenessCheckResult.java
@@ -0,0 +1,41 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.index; + +import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.FormatMethod; +import java.util.Optional; + +/** Structured result of a staleness check. */ +@AutoValue +public abstract class StalenessCheckResult { + + public static StalenessCheckResult notStale() { + return new AutoValue_StalenessCheckResult(false, Optional.empty()); + } + + public static StalenessCheckResult stale(String reason) { + return new AutoValue_StalenessCheckResult(true, Optional.of(reason)); + } + + @FormatMethod + public static StalenessCheckResult stale(String reason, Object... args) { + return stale(String.format(reason, args)); + } + + public abstract boolean isStale(); + + public abstract Optional<String> reason(); +}
diff --git a/java/com/google/gerrit/server/index/VersionManager.java b/java/com/google/gerrit/server/index/VersionManager.java index 3417379..56ce604 100644 --- a/java/com/google/gerrit/server/index/VersionManager.java +++ b/java/com/google/gerrit/server/index/VersionManager.java
@@ -37,6 +37,7 @@ import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.Config; +/** Trigger for online reindexing in case the index version in use is not the latest. */ public abstract class VersionManager implements LifecycleListener { public static boolean getOnlineUpgrade(Config cfg) { return cfg.getBoolean("index", null, "onlineUpgrade", true);
diff --git a/java/com/google/gerrit/server/index/account/AccountIndex.java b/java/com/google/gerrit/server/index/account/AccountIndex.java index 1838edf..ca7264c 100644 --- a/java/com/google/gerrit/server/index/account/AccountIndex.java +++ b/java/com/google/gerrit/server/index/account/AccountIndex.java
@@ -21,9 +21,12 @@ import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.query.account.AccountPredicates; +/** + * Index for Gerrit accounts. This class is mainly used for typing the generic parent class that + * contains actual implementations. + */ public interface AccountIndex extends Index<Account.Id, AccountState> { - public interface Factory - extends IndexDefinition.IndexFactory<Account.Id, AccountState, AccountIndex> {} + interface Factory extends IndexDefinition.IndexFactory<Account.Id, AccountState, AccountIndex> {} @Override default Predicate<AccountState> keyPredicate(Account.Id id) {
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexCollection.java b/java/com/google/gerrit/server/index/account/AccountIndexCollection.java index eb1f784..9d03e3b 100644 --- a/java/com/google/gerrit/server/index/account/AccountIndexCollection.java +++ b/java/com/google/gerrit/server/index/account/AccountIndexCollection.java
@@ -20,6 +20,7 @@ import com.google.gerrit.server.account.AccountState; import com.google.inject.Singleton; +/** Collection of active account indices. See {@link IndexCollection} for details on collections. */ @Singleton public class AccountIndexCollection extends IndexCollection<Account.Id, AccountState, AccountIndex> {
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexDefinition.java b/java/com/google/gerrit/server/index/account/AccountIndexDefinition.java index 3a34d47e..efc9c67 100644 --- a/java/com/google/gerrit/server/index/account/AccountIndexDefinition.java +++ b/java/com/google/gerrit/server/index/account/AccountIndexDefinition.java
@@ -20,6 +20,7 @@ import com.google.gerrit.server.account.AccountState; import com.google.inject.Inject; +/** Bundle of service classes that make up the account index. */ public class AccountIndexDefinition extends IndexDefinition<Account.Id, AccountState, AccountIndex> {
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexRewriter.java b/java/com/google/gerrit/server/index/account/AccountIndexRewriter.java index 643c249..8b95f7b 100644 --- a/java/com/google/gerrit/server/index/account/AccountIndexRewriter.java +++ b/java/com/google/gerrit/server/index/account/AccountIndexRewriter.java
@@ -28,6 +28,7 @@ import com.google.inject.Singleton; import org.eclipse.jgit.util.MutableInteger; +/** Rewriter for the account index. See {@link IndexRewriter} for details. */ @Singleton public class AccountIndexRewriter implements IndexRewriter<AccountState> { private final AccountIndexCollection indexes;
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexer.java b/java/com/google/gerrit/server/index/account/AccountIndexer.java index 8ced005..a055113 100644 --- a/java/com/google/gerrit/server/index/account/AccountIndexer.java +++ b/java/com/google/gerrit/server/index/account/AccountIndexer.java
@@ -16,6 +16,7 @@ import com.google.gerrit.entities.Account; +/** Interface for indexing a Gerrit account. */ public interface AccountIndexer { /**
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java index b908846..025a1f9 100644 --- a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java +++ b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
@@ -23,6 +23,7 @@ import com.google.gerrit.index.Index; import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountState; +import com.google.gerrit.server.index.StalenessCheckResult; import com.google.gerrit.server.logging.Metadata; import com.google.gerrit.server.logging.TraceContext; import com.google.gerrit.server.logging.TraceContext.TraceTimer; @@ -34,9 +35,14 @@ import java.util.Collections; import java.util.Optional; +/** + * Implementation for indexing a Gerrit account. The account will be loaded from {@link + * AccountCache}. + */ public class AccountIndexerImpl implements AccountIndexer { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + /** Factory for creating an instance. */ public interface Factory { AccountIndexerImpl create(AccountIndexCollection indexes); @@ -97,6 +103,12 @@ .indexVersion(i.getSchema().getVersion()) .build())) { i.replace(accountState.get()); + } catch (RuntimeException e) { + throw new StorageException( + String.format( + "Failed to replace account %d in index version %d", + id.get(), i.getSchema().getVersion()), + e); } } else { try (TraceTimer traceTimer = @@ -107,6 +119,12 @@ .indexVersion(i.getSchema().getVersion()) .build())) { i.delete(id); + } catch (RuntimeException e) { + throw new StorageException( + String.format( + "Failed to delete account %d from index version %d", + id.get(), i.getSchema().getVersion()), + e); } } } @@ -116,7 +134,9 @@ @Override public boolean reindexIfStale(Account.Id id) { try { - if (stalenessChecker.isStale(id)) { + StalenessCheckResult stalenessCheckResult = stalenessChecker.check(id); + if (stalenessCheckResult.isStale()) { + logger.atInfo().log("Reindexing stale document %s", stalenessCheckResult); index(id); return true; }
diff --git a/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java b/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java index 157e290..5de3ba4 100644 --- a/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java +++ b/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
@@ -20,6 +20,7 @@ import com.google.gerrit.index.SchemaDefinitions; import com.google.gerrit.server.account.AccountState; +/** Definition of account index versions (schemata). See {@link SchemaDefinitions}. */ public class AccountSchemaDefinitions extends SchemaDefinitions<AccountState> { @Deprecated static final Schema<AccountState> V4 = @@ -62,7 +63,12 @@ .legacyNumericFields(false) .build(); + /** + * Name of the account index to be used when contacting index backends or loading configurations. + */ public static final String NAME = "accounts"; + + /** Singleton instance of the schema definitions. This is one per JVM. */ public static final AccountSchemaDefinitions INSTANCE = new AccountSchemaDefinitions(); private AccountSchemaDefinitions() {
diff --git a/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java b/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java index c1077b1..5f498f9 100644 --- a/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java +++ b/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
@@ -39,6 +39,10 @@ import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.TextProgressMonitor; +/** + * Implementation that can index all accounts on a host. Used by Gerrit's initialization and upgrade + * programs as well as by REST API endpoints that offer this functionality. + */ @Singleton public class AllAccountsIndexer extends SiteIndexer<Account.Id, AccountState, AccountIndex> { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @@ -67,7 +71,7 @@ ids = collectAccounts(progress); } catch (IOException e) { logger.atSevere().withCause(e).log("Error collecting accounts"); - return new SiteIndexer.Result(sw, false, 0, 0); + return SiteIndexer.Result.create(sw, false, 0, 0); } return reindexAccounts(index, ids, progress); } @@ -109,11 +113,11 @@ Futures.successfulAsList(futures).get(); } catch (ExecutionException | InterruptedException e) { logger.atSevere().withCause(e).log("Error waiting on account futures"); - return new SiteIndexer.Result(sw, false, 0, 0); + return SiteIndexer.Result.create(sw, false, 0, 0); } progress.endTask(); - return new SiteIndexer.Result(sw, ok.get(), done.get(), failed.get()); + return SiteIndexer.Result.create(sw, ok.get(), done.get(), failed.get()); } private List<Account.Id> collectAccounts(ProgressMonitor progress) throws IOException {
diff --git a/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java b/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java index b3d961a0..9e6db44 100644 --- a/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java +++ b/java/com/google/gerrit/server/index/account/IndexedAccountQuery.java
@@ -26,6 +26,10 @@ import com.google.gerrit.index.query.QueryParseException; import com.google.gerrit.server.account.AccountState; +/** + * Wrapper around {@link Predicate}s that is returned by the {@link + * com.google.gerrit.index.IndexRewriter}. See {@link IndexedQuery}. + */ public class IndexedAccountQuery extends IndexedQuery<Account.Id, AccountState> implements DataSource<AccountState>, Matchable<AccountState> {
diff --git a/java/com/google/gerrit/server/index/account/StalenessChecker.java b/java/com/google/gerrit/server/index/account/StalenessChecker.java index aad9527..50fdcde 100644 --- a/java/com/google/gerrit/server/index/account/StalenessChecker.java +++ b/java/com/google/gerrit/server/index/account/StalenessChecker.java
@@ -35,6 +35,7 @@ import com.google.gerrit.server.config.AllUsersNameProvider; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.index.IndexUtils; +import com.google.gerrit.server.index.StalenessCheckResult; import com.google.inject.Inject; import com.google.inject.Singleton; import java.io.IOException; @@ -87,16 +88,16 @@ this.indexConfig = indexConfig; } - public boolean isStale(Account.Id id) throws IOException { + public StalenessCheckResult check(Account.Id id) throws IOException { AccountIndex i = indexes.getSearchIndex(); if (i == null) { // No index; caller couldn't do anything if it is stale. - return false; + return StalenessCheckResult.notStale(); } if (!i.getSchema().hasField(AccountField.REF_STATE) || !i.getSchema().hasField(AccountField.EXTERNAL_ID_STATE)) { // Index version not new enough for this check. - return false; + return StalenessCheckResult.notStale(); } boolean useLegacyNumericFields = i.getSchema().useLegacyNumericFields(); @@ -112,7 +113,11 @@ Ref ref = repo.exactRef(RefNames.refsUsers(id)); // Stale if the account actually exists. - return ref != null; + if (ref == null) { + return StalenessCheckResult.notStale(); + } + return StalenessCheckResult.stale( + "Document missing in index, but found %s in the repo", ref); } } @@ -124,8 +129,9 @@ e.getKey().get().equals(AllUsersNameProvider.DEFAULT) ? allUsersName : e.getKey(); try (Repository repo = repoManager.openRepository(repoName)) { if (!e.getValue().match(repo)) { - // Ref was modified since the account was indexed. - return true; + return StalenessCheckResult.stale( + "Ref was modified since the account was indexed (%s != %s)", + e.getValue(), repo.exactRef(e.getValue().ref())); } } } @@ -134,17 +140,22 @@ ListMultimap<ObjectId, ObjectId> extIdStates = parseExternalIdStates(result.get().getValue(AccountField.EXTERNAL_ID_STATE)); if (extIdStates.size() != extIds.size()) { - // External IDs of the account were modified since the account was indexed. - return true; + return StalenessCheckResult.stale( + "External IDs of the account were modified since the account was indexed. (%s != %s)", + extIdStates.size(), extIds.size()); } for (ExternalId extId : extIds) { + if (!extIdStates.containsKey(extId.key().sha1())) { + return StalenessCheckResult.stale("External ID missing: %s", extId.key().sha1()); + } if (!extIdStates.containsEntry(extId.key().sha1(), extId.blobId())) { - // External IDs of the account were modified since the account was indexed. - return true; + return StalenessCheckResult.stale( + "External ID has unexpected value. (%s != %s)", + extIdStates.get(extId.key().sha1()), extId.blobId()); } } - return false; + return StalenessCheckResult.notStale(); } public static ListMultimap<ObjectId, ObjectId> parseExternalIdStates(
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java index 4f23e88..2b826ac 100644 --- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java +++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -55,6 +55,11 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.TextProgressMonitor; +/** + * Implementation that can index all changes on a host or within a project. Used by Gerrit's + * initialization and upgrade programs as well as by REST API endpoints that offer this + * functionality. + */ public class AllChangesIndexer extends SiteIndexer<Change.Id, ChangeData, ChangeIndex> { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @@ -118,7 +123,7 @@ projectsFailed++; if (projectsFailed > projects.size() / 2) { logger.atSevere().log("Over 50%% of the projects could not be collected: aborted"); - return new Result(sw, false, 0, 0); + return Result.create(sw, false, 0, 0); } } pm.update(1); @@ -187,7 +192,7 @@ nFailed, nTotal, Math.round(pctFailed)); ok.set(false); } - return new Result(sw, ok.get(), nDone, nFailed); + return Result.create(sw, ok.get(), nDone, nFailed); } public Callable<Void> reindexProject(
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndex.java b/java/com/google/gerrit/server/index/change/ChangeIndex.java index 29bff0a..49d0d4e 100644 --- a/java/com/google/gerrit/server/index/change/ChangeIndex.java +++ b/java/com/google/gerrit/server/index/change/ChangeIndex.java
@@ -22,9 +22,12 @@ import com.google.gerrit.server.query.change.LegacyChangeIdPredicate; import com.google.gerrit.server.query.change.LegacyChangeIdStrPredicate; +/** + * Index for Gerrit changes. This class is mainly used for typing the generic parent class that + * contains actual implementations. + */ public interface ChangeIndex extends Index<Change.Id, ChangeData> { - public interface Factory - extends IndexDefinition.IndexFactory<Change.Id, ChangeData, ChangeIndex> {} + interface Factory extends IndexDefinition.IndexFactory<Change.Id, ChangeData, ChangeIndex> {} @Override default Predicate<ChangeData> keyPredicate(Change.Id id) {
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexCollection.java b/java/com/google/gerrit/server/index/change/ChangeIndexCollection.java index b8e2f3e..817ec0e 100644 --- a/java/com/google/gerrit/server/index/change/ChangeIndexCollection.java +++ b/java/com/google/gerrit/server/index/change/ChangeIndexCollection.java
@@ -20,6 +20,7 @@ import com.google.gerrit.server.query.change.ChangeData; import com.google.inject.Singleton; +/** Collection of active change indices. See {@link IndexCollection} for details on collections. */ @Singleton public class ChangeIndexCollection extends IndexCollection<Change.Id, ChangeData, ChangeIndex> { @VisibleForTesting
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexDefinition.java b/java/com/google/gerrit/server/index/change/ChangeIndexDefinition.java index 7de9e74..dde9d86 100644 --- a/java/com/google/gerrit/server/index/change/ChangeIndexDefinition.java +++ b/java/com/google/gerrit/server/index/change/ChangeIndexDefinition.java
@@ -20,6 +20,7 @@ import com.google.gerrit.server.query.change.ChangeData; import com.google.inject.Inject; +/** Bundle of service classes that make up the change index. */ public class ChangeIndexDefinition extends IndexDefinition<Change.Id, ChangeData, ChangeIndex> { @Inject
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/java/com/google/gerrit/server/index/change/ChangeIndexer.java index f6d86bf..e0f6bec 100644 --- a/java/com/google/gerrit/server/index/change/ChangeIndexer.java +++ b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -24,10 +24,12 @@ import com.google.gerrit.common.Nullable; import com.google.gerrit.entities.Change; import com.google.gerrit.entities.Project; +import com.google.gerrit.exceptions.StorageException; import com.google.gerrit.extensions.events.ChangeIndexedListener; import com.google.gerrit.index.Index; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.index.IndexExecutor; +import com.google.gerrit.server.index.StalenessCheckResult; import com.google.gerrit.server.logging.Metadata; import com.google.gerrit.server.logging.TraceContext; import com.google.gerrit.server.logging.TraceContext.TraceTimer; @@ -207,6 +209,12 @@ .indexVersion(i.getSchema().getVersion()) .build())) { i.replace(cd); + } catch (RuntimeException e) { + throw new StorageException( + String.format( + "Failed to replace change %d in index version %d (current patch set = %d)", + cd.getId().get(), i.getSchema().getVersion(), cd.currentPatchSet().number()), + e); } } fireChangeIndexedEvent(cd.project().get(), cd.getId().get()); @@ -416,6 +424,12 @@ .indexVersion(i.getSchema().getVersion()) .build())) { i.delete(id); + } catch (RuntimeException e) { + throw new StorageException( + String.format( + "Failed to delete change %d from index version %d", + id.get(), i.getSchema().getVersion()), + e); } } fireChangeDeletedFromIndexEvent(id.get()); @@ -432,7 +446,9 @@ public Boolean callImpl() throws Exception { remove(); try { - if (stalenessChecker.isStale(id)) { + StalenessCheckResult stalenessCheckResult = stalenessChecker.check(id); + if (stalenessCheckResult.isStale()) { + logger.atInfo().log("Reindexing stale document %s", stalenessCheckResult); indexImpl(changeDataFactory.create(project, id)); return true; }
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java index 72153c4..55fe11d 100644 --- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java +++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -20,6 +20,7 @@ import com.google.gerrit.index.SchemaDefinitions; import com.google.gerrit.server.query.change.ChangeData; +/** Definition of change index versions (schemata). See {@link SchemaDefinitions}. */ public class ChangeSchemaDefinitions extends SchemaDefinitions<ChangeData> { @Deprecated static final Schema<ChangeData> V55 = @@ -98,7 +99,12 @@ .legacyNumericFields(false) .build(); + /** + * Name of the change index to be used when contacting index backends or loading configurations. + */ public static final String NAME = "changes"; + + /** Singleton instance of the schema definitions. This is one per JVM. */ public static final ChangeSchemaDefinitions INSTANCE = new ChangeSchemaDefinitions(); private ChangeSchemaDefinitions() {
diff --git a/java/com/google/gerrit/server/index/change/DummyChangeIndex.java b/java/com/google/gerrit/server/index/change/DummyChangeIndex.java deleted file mode 100644 index ae3b729..0000000 --- a/java/com/google/gerrit/server/index/change/DummyChangeIndex.java +++ /dev/null
@@ -1,53 +0,0 @@ -// Copyright (C) 2014 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.index.change; - -import com.google.gerrit.entities.Change; -import com.google.gerrit.index.QueryOptions; -import com.google.gerrit.index.Schema; -import com.google.gerrit.index.query.Predicate; -import com.google.gerrit.server.query.change.ChangeData; -import com.google.gerrit.server.query.change.ChangeDataSource; - -public class DummyChangeIndex implements ChangeIndex { - @Override - public Schema<ChangeData> getSchema() { - throw new UnsupportedOperationException(); - } - - @Override - public void close() {} - - @Override - public void replace(ChangeData cd) {} - - @Override - public void delete(Change.Id id) {} - - @Override - public void deleteAll() {} - - @Override - public ChangeDataSource getSource(Predicate<ChangeData> p, QueryOptions opts) { - throw new UnsupportedOperationException(); - } - - @Override - public void markReady(boolean ready) {} - - public int getMaxLimit() { - return Integer.MAX_VALUE; - } -}
diff --git a/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java b/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java index f6d3b6f..4e27859 100644 --- a/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java +++ b/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
@@ -44,6 +44,15 @@ import java.util.concurrent.Future; import org.eclipse.jgit.lib.Config; +/** + * Listener for ref update events that reindexes entities in case the updated Git reference was used + * to compute contents of an index document. + * + * <p>Reindexes any open changes that has a destination branch that was updated to ensure that + * 'mergeable' is still current. + * + * <p>Will reindex accounts when the account's NoteDb ref changes. + */ public class ReindexAfterRefUpdate implements GitReferenceUpdatedListener { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @@ -76,7 +85,7 @@ this.accountCache = accountCache; this.indexer = indexer; this.executor = executor; - this.enabled = cfg.getBoolean("index", null, "reindexAfterRefUpdate", true); + this.enabled = cfg.getBoolean("index", "change", "indexMergeable", true); } @Override
diff --git a/java/com/google/gerrit/server/index/change/StalenessChecker.java b/java/com/google/gerrit/server/index/change/StalenessChecker.java index 47fd7ba..a1c6286 100644 --- a/java/com/google/gerrit/server/index/change/StalenessChecker.java +++ b/java/com/google/gerrit/server/index/change/StalenessChecker.java
@@ -35,6 +35,7 @@ import com.google.gerrit.index.IndexConfig; import com.google.gerrit.index.RefState; import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.index.StalenessCheckResult; import com.google.gerrit.server.query.change.ChangeData; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -46,6 +47,10 @@ import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; +/** + * Checker that compares values stored in the change index to metadata in NoteDb to detect index + * documents that should have been updated (= stale). + */ @Singleton public class StalenessChecker { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @@ -68,27 +73,36 @@ this.indexConfig = indexConfig; } - public boolean isStale(Change.Id id) { + /** + * Returns a {@link StalenessCheckResult} with structured information about staleness of the + * provided {@link com.google.gerrit.entities.Change.Id}. + */ + public StalenessCheckResult check(Change.Id id) { ChangeIndex i = indexes.getSearchIndex(); if (i == null) { - return false; // No index; caller couldn't do anything if it is stale. + return StalenessCheckResult + .notStale(); // No index; caller couldn't do anything if it is stale. } if (!i.getSchema().hasField(ChangeField.REF_STATE) || !i.getSchema().hasField(ChangeField.REF_STATE_PATTERN)) { - return false; // Index version not new enough for this check. + return StalenessCheckResult.notStale(); // Index version not new enough for this check. } Optional<ChangeData> result = i.get(id, IndexedChangeQuery.createOptions(indexConfig, 0, 1, FIELDS)); if (!result.isPresent()) { - return true; // Not in index, but caller wants it to be. + return StalenessCheckResult.stale("Document %s missing from index", id); } ChangeData cd = result.get(); - return isStale(repoManager, id, parseStates(cd), parsePatterns(cd)); + return check(repoManager, id, parseStates(cd), parsePatterns(cd)); } + /** + * Returns a {@link StalenessCheckResult} with structured information about staleness of the + * provided change. + */ @UsedAt(UsedAt.Project.GOOGLE) - public static boolean isStale( + public static StalenessCheckResult check( GitRepositoryManager repoManager, Change.Id id, SetMultimap<Project.NameKey, RefState> states, @@ -97,7 +111,7 @@ } @VisibleForTesting - static boolean refsAreStale( + static StalenessCheckResult refsAreStale( GitRepositoryManager repoManager, Change.Id id, SetMultimap<Project.NameKey, RefState> states, @@ -105,12 +119,13 @@ Set<Project.NameKey> projects = Sets.union(states.keySet(), patterns.keySet()); for (Project.NameKey p : projects) { - if (refsAreStale(repoManager, id, p, states, patterns)) { - return true; + StalenessCheckResult result = refsAreStale(repoManager, id, p, states, patterns); + if (result.isStale()) { + return result; } } - return false; + return StalenessCheckResult.notStale(); } private SetMultimap<Project.NameKey, RefState> parseStates(ChangeData cd) { @@ -121,6 +136,10 @@ return parsePatterns(cd.getRefStatePatterns()); } + /** + * Returns a map containing the parsed version of {@link RefStatePattern}. See {@link + * RefStatePattern}. + */ public static ListMultimap<Project.NameKey, RefStatePattern> parsePatterns( Iterable<byte[]> patterns) { RefStatePattern.check(patterns != null, null); @@ -136,7 +155,7 @@ return result; } - private static boolean refsAreStale( + private static StalenessCheckResult refsAreStale( GitRepositoryManager repoManager, Change.Id id, Project.NameKey project, @@ -146,18 +165,22 @@ Set<RefState> states = allStates.get(project); for (RefState state : states) { if (!state.match(repo)) { - return true; + return StalenessCheckResult.stale( + "Ref states don't match for document %s (%s != %s)", + id, state, repo.exactRef(state.ref())); } } for (RefStatePattern pattern : allPatterns.get(project)) { if (!pattern.match(repo, states)) { - return true; + return StalenessCheckResult.stale( + "Ref patterns don't match for document %s. Pattern: %s States: %s", + id, pattern, states); } } - return false; + return StalenessCheckResult.notStale(); } catch (IOException e) { logger.atWarning().withCause(e).log("error checking staleness of %s in %s", id, project); - return true; + return StalenessCheckResult.stale("Exceptions while processing document %s", e.getMessage()); } }
diff --git a/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java b/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java index 0dbbbc5..51c7730 100644 --- a/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java +++ b/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
@@ -43,6 +43,10 @@ import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.TextProgressMonitor; +/** + * Implementation that can index all groups on a host. Used by Gerrit's initialization and upgrade + * programs as well as by REST API endpoints that offer this functionality. + */ @Singleton public class AllGroupsIndexer extends SiteIndexer<AccountGroup.UUID, InternalGroup, GroupIndex> { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @@ -71,7 +75,7 @@ uuids = collectGroups(progress); } catch (IOException | ConfigInvalidException e) { logger.atSevere().withCause(e).log("Error collecting groups"); - return new SiteIndexer.Result(sw, false, 0, 0); + return SiteIndexer.Result.create(sw, false, 0, 0); } return reindexGroups(index, uuids, progress); } @@ -117,11 +121,11 @@ Futures.successfulAsList(futures).get(); } catch (ExecutionException | InterruptedException e) { logger.atSevere().withCause(e).log("Error waiting on group futures"); - return new SiteIndexer.Result(sw, false, 0, 0); + return SiteIndexer.Result.create(sw, false, 0, 0); } progress.endTask(); - return new SiteIndexer.Result(sw, ok.get(), done.get(), failed.get()); + return SiteIndexer.Result.create(sw, ok.get(), done.get(), failed.get()); } private List<AccountGroup.UUID> collectGroups(ProgressMonitor progress)
diff --git a/java/com/google/gerrit/server/index/group/GroupIndex.java b/java/com/google/gerrit/server/index/group/GroupIndex.java index daa2131..be569df 100644 --- a/java/com/google/gerrit/server/index/group/GroupIndex.java +++ b/java/com/google/gerrit/server/index/group/GroupIndex.java
@@ -21,8 +21,12 @@ import com.google.gerrit.server.group.InternalGroup; import com.google.gerrit.server.query.group.GroupPredicates; +/** + * Index for internal Gerrit groups. This class is mainly used for typing the generic parent class + * that contains actual implementations. + */ public interface GroupIndex extends Index<AccountGroup.UUID, InternalGroup> { - public interface Factory + interface Factory extends IndexDefinition.IndexFactory<AccountGroup.UUID, InternalGroup, GroupIndex> {} @Override
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexCollection.java b/java/com/google/gerrit/server/index/group/GroupIndexCollection.java index 9d74b7d..6c36a97 100644 --- a/java/com/google/gerrit/server/index/group/GroupIndexCollection.java +++ b/java/com/google/gerrit/server/index/group/GroupIndexCollection.java
@@ -20,6 +20,7 @@ import com.google.gerrit.server.group.InternalGroup; import com.google.inject.Singleton; +/** Collection of active group indices. See {@link IndexCollection} for details on collections. */ @Singleton public class GroupIndexCollection extends IndexCollection<AccountGroup.UUID, InternalGroup, GroupIndex> {
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexDefinition.java b/java/com/google/gerrit/server/index/group/GroupIndexDefinition.java index e403752..a9cd856 100644 --- a/java/com/google/gerrit/server/index/group/GroupIndexDefinition.java +++ b/java/com/google/gerrit/server/index/group/GroupIndexDefinition.java
@@ -20,6 +20,7 @@ import com.google.gerrit.server.group.InternalGroup; import com.google.inject.Inject; +/** Bundle of service classes that make up the group index. */ public class GroupIndexDefinition extends IndexDefinition<AccountGroup.UUID, InternalGroup, GroupIndex> {
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexRewriter.java b/java/com/google/gerrit/server/index/group/GroupIndexRewriter.java index 157c01a..a7b9497 100644 --- a/java/com/google/gerrit/server/index/group/GroupIndexRewriter.java +++ b/java/com/google/gerrit/server/index/group/GroupIndexRewriter.java
@@ -24,6 +24,7 @@ import com.google.inject.Inject; import com.google.inject.Singleton; +/** Rewriter for the group index. See {@link IndexRewriter} for details. */ @Singleton public class GroupIndexRewriter implements IndexRewriter<InternalGroup> { private final GroupIndexCollection indexes;
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexer.java b/java/com/google/gerrit/server/index/group/GroupIndexer.java index 25d5840..d6b9186 100644 --- a/java/com/google/gerrit/server/index/group/GroupIndexer.java +++ b/java/com/google/gerrit/server/index/group/GroupIndexer.java
@@ -16,6 +16,7 @@ import com.google.gerrit.entities.AccountGroup; +/** Interface for indexing an internal Gerrit group. */ public interface GroupIndexer { /**
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java index 790066d..e9897e8 100644 --- a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java +++ b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
@@ -23,6 +23,7 @@ import com.google.gerrit.index.Index; import com.google.gerrit.server.account.GroupCache; import com.google.gerrit.server.group.InternalGroup; +import com.google.gerrit.server.index.StalenessCheckResult; import com.google.gerrit.server.logging.Metadata; import com.google.gerrit.server.logging.TraceContext; import com.google.gerrit.server.logging.TraceContext.TraceTimer; @@ -34,6 +35,10 @@ import java.util.Collections; import java.util.Optional; +/** + * Implementation for indexing an internal Gerrit group. The group will be loaded from {@link + * GroupCache}. + */ public class GroupIndexerImpl implements GroupIndexer { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @@ -97,6 +102,12 @@ .indexVersion(i.getSchema().getVersion()) .build())) { i.replace(internalGroup.get()); + } catch (RuntimeException e) { + throw new StorageException( + String.format( + "Failed to replace group %s in index version %d", + uuid.get(), i.getSchema().getVersion()), + e); } } else { try (TraceTimer traceTimer = @@ -107,6 +118,12 @@ .indexVersion(i.getSchema().getVersion()) .build())) { i.delete(uuid); + } catch (RuntimeException e) { + throw new StorageException( + String.format( + "Failed to delete group %s from index version %d", + uuid.get(), i.getSchema().getVersion()), + e); } } } @@ -116,7 +133,9 @@ @Override public boolean reindexIfStale(AccountGroup.UUID uuid) { try { - if (stalenessChecker.isStale(uuid)) { + StalenessCheckResult stalenessCheckResult = stalenessChecker.check(uuid); + if (stalenessCheckResult.isStale()) { + logger.atInfo().log("Reindexing stale document %s", stalenessCheckResult); index(uuid); return true; }
diff --git a/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java b/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java index e64e2eb..40e0d8e 100644 --- a/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java +++ b/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
@@ -20,6 +20,7 @@ import com.google.gerrit.index.SchemaDefinitions; import com.google.gerrit.server.group.InternalGroup; +/** Definition of group index versions (schemata). See {@link SchemaDefinitions}. */ public class GroupSchemaDefinitions extends SchemaDefinitions<InternalGroup> { @Deprecated static final Schema<InternalGroup> V2 = @@ -49,6 +50,7 @@ // to offer fast single- and multi-dimensional numeric range. static final Schema<InternalGroup> V8 = schema(V7, false); + /** Singleton instance of the schema definitions. This is one per JVM. */ public static final GroupSchemaDefinitions INSTANCE = new GroupSchemaDefinitions(); private GroupSchemaDefinitions() {
diff --git a/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java b/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java index dfdf3ca..680db12 100644 --- a/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java +++ b/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java
@@ -26,6 +26,10 @@ import java.util.HashSet; import java.util.Set; +/** + * Wrapper around {@link Predicate}s that is returned by the {@link + * com.google.gerrit.index.IndexRewriter}. See {@link IndexedQuery}. + */ public class IndexedGroupQuery extends IndexedQuery<AccountGroup.UUID, InternalGroup> implements DataSource<InternalGroup> {
diff --git a/java/com/google/gerrit/server/index/group/StalenessChecker.java b/java/com/google/gerrit/server/index/group/StalenessChecker.java index 3a721c3..4ce3f5b 100644 --- a/java/com/google/gerrit/server/index/group/StalenessChecker.java +++ b/java/com/google/gerrit/server/index/group/StalenessChecker.java
@@ -21,6 +21,7 @@ import com.google.gerrit.index.query.FieldBundle; import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.index.StalenessCheckResult; import com.google.inject.Inject; import com.google.inject.Singleton; import java.io.IOException; @@ -34,8 +35,6 @@ * * <p>An index document is considered stale if the stored SHA1 differs from the HEAD SHA1 of the * groups branch. - * - * <p>Note: This only applies to NoteDb. */ @Singleton public class StalenessChecker { @@ -59,10 +58,11 @@ this.allUsers = allUsers; } - public boolean isStale(AccountGroup.UUID uuid) throws IOException { + public StalenessCheckResult check(AccountGroup.UUID uuid) throws IOException { GroupIndex i = indexes.getSearchIndex(); if (i == null) { - return false; // No index; caller couldn't do anything if it is stale. + // No index; caller couldn't do anything if it is stale. + return StalenessCheckResult.notStale(); } Optional<FieldBundle> result = @@ -73,14 +73,23 @@ Ref ref = repo.exactRef(RefNames.refsGroups(uuid)); // Stale if the group actually exists. - return ref != null; + if (ref == null) { + return StalenessCheckResult.notStale(); + } + return StalenessCheckResult.stale( + "Document missing in index, but found %s in the repo", ref); } } try (Repository repo = repoManager.openRepository(allUsers)) { Ref ref = repo.exactRef(RefNames.refsGroups(uuid)); ObjectId head = ref == null ? ObjectId.zeroId() : ref.getObjectId(); - return !head.equals(ObjectId.fromString(result.get().getValue(GroupField.REF_STATE), 0)); + ObjectId idFromIndex = ObjectId.fromString(result.get().getValue(GroupField.REF_STATE), 0); + if (head.equals(idFromIndex)) { + return StalenessCheckResult.notStale(); + } + return StalenessCheckResult.stale( + "Document has unexpected ref state (%s != %s)", head, idFromIndex); } } }
diff --git a/java/com/google/gerrit/server/index/project/AllProjectsIndexer.java b/java/com/google/gerrit/server/index/project/AllProjectsIndexer.java index b760fd7..2c37716 100644 --- a/java/com/google/gerrit/server/index/project/AllProjectsIndexer.java +++ b/java/com/google/gerrit/server/index/project/AllProjectsIndexer.java
@@ -37,6 +37,10 @@ import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.TextProgressMonitor; +/** + * Implementation that can index all projects on a host. Used by Gerrit's initialization and upgrade + * programs as well as by REST API endpoints that offer this functionality. + */ @Singleton public class AllProjectsIndexer extends SiteIndexer<Project.NameKey, ProjectData, ProjectIndex> { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @@ -91,11 +95,11 @@ Futures.successfulAsList(futures).get(); } catch (ExecutionException | InterruptedException e) { logger.atSevere().withCause(e).log("Error waiting on project futures"); - return new SiteIndexer.Result(sw, false, 0, 0); + return SiteIndexer.Result.create(sw, false, 0, 0); } progress.endTask(); - return new SiteIndexer.Result(sw, ok.get(), done.get(), failed.get()); + return SiteIndexer.Result.create(sw, ok.get(), done.get(), failed.get()); } private List<Project.NameKey> collectProjects(ProgressMonitor progress) {
diff --git a/java/com/google/gerrit/server/index/project/ProjectIndexDefinition.java b/java/com/google/gerrit/server/index/project/ProjectIndexDefinition.java index 6a844b5..d93dfe7 100644 --- a/java/com/google/gerrit/server/index/project/ProjectIndexDefinition.java +++ b/java/com/google/gerrit/server/index/project/ProjectIndexDefinition.java
@@ -23,6 +23,7 @@ import com.google.gerrit.index.project.ProjectSchemaDefinitions; import com.google.inject.Inject; +/** Bundle of service classes that make up the project index. */ public class ProjectIndexDefinition extends IndexDefinition<Project.NameKey, ProjectData, ProjectIndex> {
diff --git a/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java b/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java index 4de83be..22517ad 100644 --- a/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java +++ b/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java
@@ -18,6 +18,7 @@ import com.google.common.flogger.FluentLogger; import com.google.gerrit.common.Nullable; import com.google.gerrit.entities.Project; +import com.google.gerrit.exceptions.StorageException; import com.google.gerrit.extensions.events.ProjectIndexedListener; import com.google.gerrit.index.project.ProjectData; import com.google.gerrit.index.project.ProjectIndex; @@ -34,6 +35,10 @@ import java.util.Collection; import java.util.Collections; +/** + * Implementation for indexing a Gerrit-managed repository (project). The project will be loaded + * from {@link ProjectCache}. + */ public class ProjectIndexerImpl implements ProjectIndexer { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @@ -85,6 +90,12 @@ .indexVersion(i.getSchema().getVersion()) .build())) { i.replace(projectData); + } catch (RuntimeException e) { + throw new StorageException( + String.format( + "Failed to replace project %s in index version %d", + nameKey.get(), i.getSchema().getVersion()), + e); } } fireProjectIndexedEvent(nameKey.get()); @@ -99,6 +110,12 @@ .indexVersion(i.getSchema().getVersion()) .build())) { i.delete(nameKey); + } catch (RuntimeException e) { + throw new StorageException( + String.format( + "Failed to delete project %s from index version %d", + nameKey.get(), i.getSchema().getVersion()), + e); } } }
diff --git a/java/com/google/gerrit/server/index/project/StalenessChecker.java b/java/com/google/gerrit/server/index/project/StalenessChecker.java index e4c1a7d..1e10b7c 100644 --- a/java/com/google/gerrit/server/index/project/StalenessChecker.java +++ b/java/com/google/gerrit/server/index/project/StalenessChecker.java
@@ -27,10 +27,15 @@ import com.google.gerrit.index.project.ProjectIndex; import com.google.gerrit.index.project.ProjectIndexCollection; import com.google.gerrit.index.query.FieldBundle; +import com.google.gerrit.server.index.StalenessCheckResult; import com.google.gerrit.server.project.ProjectCache; import com.google.inject.Inject; import java.util.Optional; +/** + * Checker that compares values stored in the project index to metadata in NoteDb to detect index + * documents that should have been updated (= stale). + */ public class StalenessChecker { private static final ImmutableSet<String> FIELDS = ImmutableSet.of(ProjectField.NAME.getName(), ProjectField.REF_STATE.getName()); @@ -47,17 +52,22 @@ this.indexConfig = indexConfig; } - public boolean isStale(Project.NameKey project) { + /** + * Returns a {@link StalenessCheckResult} with structured information about staleness of the + * provided {@link com.google.gerrit.entities.Project.NameKey}. + */ + public StalenessCheckResult check(Project.NameKey project) { ProjectData projectData = projectCache.get(project).toProjectData(); ProjectIndex i = indexes.getSearchIndex(); if (i == null) { - return false; // No index; caller couldn't do anything if it is stale. + return StalenessCheckResult + .notStale(); // No index; caller couldn't do anything if it is stale. } Optional<FieldBundle> result = i.getRaw(project, QueryOptions.create(indexConfig, 0, 1, FIELDS)); if (!result.isPresent()) { - return true; + return StalenessCheckResult.stale("Document %s missing from index", project); } SetMultimap<Project.NameKey, RefState> indexedRefStates = @@ -73,6 +83,10 @@ p.getProject().getNameKey(), RefState.create(RefNames.REFS_CONFIG, p.getProject().getConfigRefState()))); - return !currentRefStates.equals(indexedRefStates); + if (currentRefStates.equals(indexedRefStates)) { + return StalenessCheckResult.notStale(); + } + return StalenessCheckResult.stale( + "Document has unexpected ref states (%s != %s)", currentRefStates, indexedRefStates); } }
diff --git a/java/com/google/gerrit/server/logging/Metadata.java b/java/com/google/gerrit/server/logging/Metadata.java index 7af204e..d312530 100644 --- a/java/com/google/gerrit/server/logging/Metadata.java +++ b/java/com/google/gerrit/server/logging/Metadata.java
@@ -59,6 +59,9 @@ // The type of change ID which the user used to identify a change (e.g. numeric ID, triplet etc.). public abstract Optional<String> changeIdType(); + // The cause of an error. + public abstract Optional<String> cause(); + // The type of an event. public abstract Optional<String> eventType(); @@ -155,12 +158,12 @@ * Metadata{accountId=Optional.empty, actionType=Optional.empty, authDomainName=Optional.empty, * branchName=Optional.empty, cacheKey=Optional.empty, cacheName=Optional.empty, * className=Optional.empty, changeId=Optional[9212550], changeIdType=Optional.empty, - * eventType=Optional.empty, exportValue=Optional.empty, filePath=Optional.empty, - * garbageCollectorName=Optional.empty, gitOperation=Optional.empty, groupId=Optional.empty, - * groupName=Optional.empty, groupUuid=Optional.empty, httpStatus=Optional.empty, - * indexName=Optional.empty, indexVersion=Optional[0], methodName=Optional.empty, - * multiple=Optional.empty, operationName=Optional.empty, partial=Optional.empty, - * noteDbFilePath=Optional.empty, noteDbRefName=Optional.empty, + * cause=Optional.empty, eventType=Optional.empty, exportValue=Optional.empty, + * filePath=Optional.empty, garbageCollectorName=Optional.empty, gitOperation=Optional.empty, + * groupId=Optional.empty, groupName=Optional.empty, groupUuid=Optional.empty, + * httpStatus=Optional.empty, indexName=Optional.empty, indexVersion=Optional[0], + * methodName=Optional.empty, multiple=Optional.empty, operationName=Optional.empty, + * partial=Optional.empty, noteDbFilePath=Optional.empty, noteDbRefName=Optional.empty, * noteDbSequenceType=Optional.empty, noteDbTable=Optional.empty, patchSetId=Optional.empty, * pluginMetadata=[], pluginName=Optional.empty, projectName=Optional.empty, * pushType=Optional.empty, resourceCount=Optional.empty, restViewName=Optional.empty, @@ -273,6 +276,8 @@ public abstract Builder changeIdType(@Nullable String changeIdType); + public abstract Builder cause(@Nullable String cause); + public abstract Builder eventType(@Nullable String eventType); public abstract Builder exportValue(@Nullable String exportValue);
diff --git a/java/com/google/gerrit/server/mail/SignedToken.java b/java/com/google/gerrit/server/mail/SignedToken.java index 436b854..ba064df 100644 --- a/java/com/google/gerrit/server/mail/SignedToken.java +++ b/java/com/google/gerrit/server/mail/SignedToken.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.mail; +import com.google.common.io.BaseEncoding; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; @@ -21,7 +22,6 @@ import javax.crypto.Mac; import javax.crypto.ShortBufferException; import javax.crypto.spec.SecretKeySpec; -import org.apache.commons.codec.binary.Base64; /** * Utility function to compute and verify XSRF tokens. @@ -164,11 +164,11 @@ } private static byte[] decodeBase64(final String s) { - return Base64.decodeBase64(toBytes(s)); + return BaseEncoding.base64().decode(s); } private static String encodeBase64(final byte[] buf) { - return toString(Base64.encodeBase64(buf)); + return BaseEncoding.base64().encode(buf); } private static void encodeInt(final byte[] buf, final int o, final int v) { @@ -202,12 +202,4 @@ } return r; } - - private static String toString(final byte[] b) { - final StringBuilder r = new StringBuilder(b.length); - for (int i = 0; i < b.length; i++) { - r.append((char) b[i]); - } - return r.toString(); - } }
diff --git a/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java b/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java index 8b5cc92..8ae06f8 100644 --- a/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java +++ b/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
@@ -17,6 +17,7 @@ import static com.google.common.base.Preconditions.checkState; import static java.nio.charset.StandardCharsets.UTF_8; +import com.google.common.io.BaseEncoding; import com.google.gerrit.entities.Account; import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.mail.send.RegisterNewEmailSender; @@ -25,7 +26,6 @@ import com.google.inject.Singleton; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.eclipse.jgit.util.Base64; /** Verifies the token sent by {@link RegisterNewEmailSender}. */ @Singleton @@ -50,7 +50,7 @@ try { String payload = String.format("%s:%s", accountId, emailAddress); byte[] utf8 = payload.getBytes(UTF_8); - String base64 = Base64.encodeBytes(utf8); + String base64 = BaseEncoding.base64().encode(utf8); return emailRegistrationToken.newToken(base64); } catch (XsrfException e) { throw new IllegalArgumentException(e); @@ -70,7 +70,7 @@ throw new InvalidTokenException(); } - String payload = new String(Base64.decode(token.getData()), UTF_8); + String payload = new String(BaseEncoding.base64().decode(token.getData()), UTF_8); Matcher matcher = Pattern.compile("^([0-9]+):(.+@.+)$").matcher(payload); if (!matcher.matches()) { throw new InvalidTokenException();
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java index 158db1c..71d8c15 100644 --- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java +++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -156,11 +156,14 @@ * @param message {@link MailMessage} to process */ public void process(MailMessage message) throws RestApiException, UpdateException { - retryHelper.execute( - buf -> { - processImpl(buf, message); - return null; - }); + retryHelper + .changeUpdate( + "processCommentsReceivedByEmail", + buf -> { + processImpl(buf, message); + return null; + }) + .call(); } private void processImpl(BatchUpdate.Factory buf, MailMessage message)
diff --git a/java/com/google/gerrit/server/mail/send/AddKeySender.java b/java/com/google/gerrit/server/mail/send/AddKeySender.java index 8b3d3f7..35b0027 100644 --- a/java/com/google/gerrit/server/mail/send/AddKeySender.java +++ b/java/com/google/gerrit/server/mail/send/AddKeySender.java
@@ -24,6 +24,7 @@ import com.google.inject.assistedinject.AssistedInject; import java.util.List; +/** Sender that informs a user by email about the addition of an SSH or GPG key to their account. */ public class AddKeySender extends OutgoingEmail { public interface Factory { AddKeySender create(IdentifiedUser user, AccountSshKey sshKey); @@ -78,15 +79,26 @@ } } - public String getEmail() { + @Override + protected void setupSoyContext() { + super.setupSoyContext(); + soyContextEmailData.put("email", getEmail()); + soyContextEmailData.put("gpgKeys", getGpgKeys()); + soyContextEmailData.put("keyType", getKeyType()); + soyContextEmailData.put("sshKey", getSshKey()); + soyContextEmailData.put("userNameEmail", getUserNameEmailFor(user.getAccountId())); + } + + @Override + protected boolean supportsHtml() { + return true; + } + + private String getEmail() { return user.getAccount().preferredEmail(); } - public String getUserNameEmail() { - return getUserNameEmailFor(user.getAccountId()); - } - - public String getKeyType() { + private String getKeyType() { if (sshKey != null) { return "SSH"; } else if (gpgKeys != null) { @@ -95,29 +107,14 @@ return "Unknown"; } - public String getSshKey() { + private String getSshKey() { return (sshKey != null) ? sshKey.sshPublicKey() + "\n" : null; } - public String getGpgKeys() { + private String getGpgKeys() { if (gpgKeys != null) { return Joiner.on("\n").join(gpgKeys); } return null; } - - @Override - protected void setupSoyContext() { - super.setupSoyContext(); - soyContextEmailData.put("email", getEmail()); - soyContextEmailData.put("gpgKeys", getGpgKeys()); - soyContextEmailData.put("keyType", getKeyType()); - soyContextEmailData.put("sshKey", getSshKey()); - soyContextEmailData.put("userNameEmail", getUserNameEmail()); - } - - @Override - protected boolean supportsHtml() { - return true; - } }
diff --git a/java/com/google/gerrit/server/mail/send/DeleteKeySender.java b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java index c9bb1e4..c26e336 100644 --- a/java/com/google/gerrit/server/mail/send/DeleteKeySender.java +++ b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
@@ -25,6 +25,9 @@ import java.util.Collections; import java.util.List; +/** + * Sender that informs a user by email about the removal of an SSH or GPG key from their account. + */ public class DeleteKeySender extends OutgoingEmail { public interface Factory { DeleteKeySender create(IdentifiedUser user, AccountSshKey sshKey); @@ -76,15 +79,26 @@ } } - public String getEmail() { + @Override + protected void setupSoyContext() { + super.setupSoyContext(); + soyContextEmailData.put("email", getEmail()); + soyContextEmailData.put("gpgKeyFingerprints", getGpgKeyFingerprints()); + soyContextEmailData.put("keyType", getKeyType()); + soyContextEmailData.put("sshKey", getSshKey()); + soyContextEmailData.put("userNameEmail", getUserNameEmailFor(user.getAccountId())); + } + + @Override + protected boolean supportsHtml() { + return true; + } + + private String getEmail() { return user.getAccount().preferredEmail(); } - public String getUserNameEmail() { - return getUserNameEmailFor(user.getAccountId()); - } - - public String getKeyType() { + private String getKeyType() { if (sshKey != null) { return "SSH"; } else if (gpgKeyFingerprints != null) { @@ -93,29 +107,14 @@ throw new IllegalStateException("key type is not SSH or GPG"); } - public String getSshKey() { + private String getSshKey() { return (sshKey != null) ? sshKey.sshPublicKey() + "\n" : null; } - public String getGpgKeyFingerprints() { + private String getGpgKeyFingerprints() { if (!gpgKeyFingerprints.isEmpty()) { return Joiner.on("\n").join(gpgKeyFingerprints); } return null; } - - @Override - protected void setupSoyContext() { - super.setupSoyContext(); - soyContextEmailData.put("email", getEmail()); - soyContextEmailData.put("gpgKeyFingerprints", getGpgKeyFingerprints()); - soyContextEmailData.put("keyType", getKeyType()); - soyContextEmailData.put("sshKey", getSshKey()); - soyContextEmailData.put("userNameEmail", getUserNameEmail()); - } - - @Override - protected boolean supportsHtml() { - return true; - } }
diff --git a/java/com/google/gerrit/server/mail/send/EmailArguments.java b/java/com/google/gerrit/server/mail/send/EmailArguments.java index ede5765..808d6a4 100644 --- a/java/com/google/gerrit/server/mail/send/EmailArguments.java +++ b/java/com/google/gerrit/server/mail/send/EmailArguments.java
@@ -45,11 +45,24 @@ import com.google.gerrit.server.validators.OutgoingEmailValidationListener; import com.google.inject.Inject; import com.google.inject.Provider; +import com.google.inject.Singleton; import com.google.template.soy.jbcsrc.api.SoySauce; import java.util.List; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.PersonIdent; +/** + * Arguments used for sending notification emails. + * + * <p>Notification emails are sent by out by {@link OutgoingEmail} and it's subclasses, so called + * senders. To construct an email the sender class needs to get various other classes injected. + * Instead of injecting these classes into the sender classes directly, they only get {@code + * EmailArguments} injected and {@code EmailArguments} provides them all dependencies that they + * need. + * + * <p>This class is public because plugins need access to it for sending emails. + */ +@Singleton @UsedAt(UsedAt.Project.PLUGINS_ALL) public class EmailArguments { final GitRepositoryManager server; @@ -60,22 +73,22 @@ final PatchListCache patchListCache; final PatchSetUtil patchSetUtil; final ApprovalsUtil approvalsUtil; - final FromAddressGenerator fromAddressGenerator; + final Provider<FromAddressGenerator> fromAddressGenerator; final EmailSender emailSender; final PatchSetInfoFactory patchSetInfoFactory; final IdentifiedUser.GenericFactory identifiedUserFactory; final ChangeNotes.Factory changeNotesFactory; - final AnonymousUser anonymousUser; + final Provider<AnonymousUser> anonymousUser; final String anonymousCowardName; - final PersonIdent gerritPersonIdent; + final Provider<PersonIdent> gerritPersonIdent; final DynamicItem<UrlFormatter> urlFormatter; final AllProjectsName allProjectsName; final List<String> sshAddresses; final SitePaths site; - final ChangeQueryBuilder queryBuilder; + final Provider<ChangeQueryBuilder> queryBuilder; final ChangeData.Factory changeDataFactory; - final SoySauce soySauce; + final Provider<SoySauce> soySauce; final EmailSettings settings; final DynamicSet<OutgoingEmailValidationListener> outgoingEmailValidationListeners; final Provider<InternalAccountQuery> accountQueryProvider; @@ -93,19 +106,19 @@ PatchListCache patchListCache, PatchSetUtil patchSetUtil, ApprovalsUtil approvalsUtil, - FromAddressGenerator fromAddressGenerator, + Provider<FromAddressGenerator> fromAddressGenerator, EmailSender emailSender, PatchSetInfoFactory patchSetInfoFactory, GenericFactory identifiedUserFactory, ChangeNotes.Factory changeNotesFactory, - AnonymousUser anonymousUser, + Provider<AnonymousUser> anonymousUser, @AnonymousCowardName String anonymousCowardName, - GerritPersonIdentProvider gerritPersonIdentProvider, + GerritPersonIdentProvider gerritPersonIdent, DynamicItem<UrlFormatter> urlFormatter, AllProjectsName allProjectsName, - ChangeQueryBuilder queryBuilder, + Provider<ChangeQueryBuilder> queryBuilder, ChangeData.Factory changeDataFactory, - @MailTemplates SoySauce soySauce, + @MailTemplates Provider<SoySauce> soySauce, EmailSettings settings, @SshAdvertisedAddresses List<String> sshAddresses, SitePaths site, @@ -129,7 +142,7 @@ this.changeNotesFactory = changeNotesFactory; this.anonymousUser = anonymousUser; this.anonymousCowardName = anonymousCowardName; - this.gerritPersonIdent = gerritPersonIdentProvider.get(); + this.gerritPersonIdent = gerritPersonIdent; this.urlFormatter = urlFormatter; this.allProjectsName = allProjectsName; this.queryBuilder = queryBuilder;
diff --git a/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java b/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java index bdfe2e8..6a34786 100644 --- a/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java +++ b/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java
@@ -16,6 +16,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; +import com.google.common.io.BaseEncoding; import com.google.gerrit.common.data.ParameterizedString; import com.google.gerrit.entities.Account; import com.google.gerrit.mail.Address; @@ -32,7 +33,6 @@ import java.security.NoSuchAlgorithmException; import java.util.Optional; import java.util.regex.Pattern; -import org.apache.commons.codec.binary.Base64; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.PersonIdent; @@ -232,7 +232,7 @@ try { MessageDigest hash = MessageDigest.getInstance("MD5"); byte[] bytes = hash.digest(data.getBytes(UTF_8)); - return Base64.encodeBase64URLSafeString(bytes); + return BaseEncoding.base64Url().encode(bytes); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("No MD5 available", e); }
diff --git a/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java index 2db2d6d..d792b48 100644 --- a/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java +++ b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
@@ -21,6 +21,7 @@ import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; +/** Sender that informs a user by email that the HTTP password of their account was updated. */ public class HttpPasswordUpdateSender extends OutgoingEmail { public interface Factory { HttpPasswordUpdateSender create(IdentifiedUser user, String operation); @@ -58,19 +59,11 @@ } } - public String getEmail() { - return user.getAccount().preferredEmail(); - } - - public String getUserNameEmail() { - return getUserNameEmailFor(user.getAccountId()); - } - @Override protected void setupSoyContext() { super.setupSoyContext(); soyContextEmailData.put("email", getEmail()); - soyContextEmailData.put("userNameEmail", getUserNameEmail()); + soyContextEmailData.put("userNameEmail", getUserNameEmailFor(user.getAccountId())); soyContextEmailData.put("operation", operation); } @@ -78,4 +71,8 @@ protected boolean supportsHtml() { return true; } + + private String getEmail() { + return user.getAccount().preferredEmail(); + } }
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java index e588158..d48ed59 100644 --- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java +++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -52,6 +52,7 @@ import java.util.Set; import java.util.StringJoiner; import org.apache.james.mime4j.dom.field.FieldName; +import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.util.SystemReader; /** Sends an email to one or more interested parties. */ @@ -127,11 +128,19 @@ // this message so they can always review and audit what we sent // on their behalf to others. // + logger.atFine().log( + "CC email sender %s because the email strategy of this user is %s", + fromUser.get().account().id(), CC_ON_OWN_COMMENTS); add(RecipientType.CC, fromId); } else if (!notify.accounts().containsValue(fromId) && rcptTo.remove(fromId)) { // If they don't want a copy, but we queued one up anyway, // drop them from the recipient lists. // + logger.atFine().log( + "Not CCing email sender %s because the email strategy of this user is not %s but %s", + fromUser.get().account().id(), + CC_ON_OWN_COMMENTS, + senderPrefs != null ? senderPrefs.getEmailStrategy() : null); removeUser(fromUser.get().account()); } } @@ -145,8 +154,12 @@ Account thisUserAccount = thisUser.get().account(); GeneralPreferencesInfo prefs = thisUser.get().generalPreferences(); if (prefs == null || prefs.getEmailStrategy() == DISABLED) { + logger.atFine().log( + "Not emailing account %s because user has set email strategy to %s", id, DISABLED); removeUser(thisUserAccount); } else if (useHtml() && prefs.getEmailFormat() == EmailFormat.PLAINTEXT) { + logger.atFine().log( + "Removing account %s from HTML email because user prefers plain text emails", id); removeUser(thisUserAccount); smtpRcptToPlaintextOnly.add( new Address(thisUserAccount.fullName(), thisUserAccount.preferredEmail())); @@ -206,12 +219,13 @@ if (!va.smtpRcptTo.isEmpty()) { // Send multipart message - logger.atFine().log("Sending multipart '%s'", messageClass); + logger.atFine().log( + "Sending multipart '%s' from %s to %s", + messageClass, va.smtpFromAddress, va.smtpRcptTo); args.emailSender.send(va.smtpFromAddress, va.smtpRcptTo, va.headers, va.body, va.htmlBody); } if (!smtpRcptToPlaintextOnly.isEmpty()) { - logger.atFine().log("Sending plaintext '%s'", messageClass); // Send plaintext message Map<String, EmailHeader> shallowCopy = new HashMap<>(); shallowCopy.putAll(headers); @@ -224,6 +238,9 @@ to.add(a); shallowCopy.put(FieldName.TO, to); } + logger.atFine().log( + "Sending plaintext '%s' from %s to %s", + messageClass, va.smtpFromAddress, smtpRcptToPlaintextOnly); args.emailSender.send(va.smtpFromAddress, smtpRcptToPlaintextOnly, shallowCopy, va.body); } } @@ -240,7 +257,7 @@ protected void init() throws EmailException { setupSoyContext(); - smtpFromAddress = args.fromAddressGenerator.from(fromId); + smtpFromAddress = args.fromAddressGenerator.get().from(fromId); setHeader(FieldName.DATE, new Date()); headers.put(FieldName.FROM, new EmailHeader.AddressList(smtpFromAddress)); headers.put(FieldName.TO, new EmailHeader.AddressList()); @@ -257,7 +274,7 @@ textBody = new StringBuilder(); htmlBody = new StringBuilder(); - if (fromId != null && args.fromAddressGenerator.isGenericAddress(fromId)) { + if (fromId != null && args.fromAddressGenerator.get().isGenericAddress(fromId)) { appendText(getFromLine()); } } @@ -337,7 +354,7 @@ /** Lookup a human readable name for an account, usually the "full name". */ protected String getNameFor(@Nullable Account.Id accountId) { if (accountId == null) { - return args.gerritPersonIdent.getName(); + return args.gerritPersonIdent.get().getName(); } Optional<Account> account = args.accountCache.get(accountId).map(AccountState::account); @@ -363,10 +380,8 @@ */ protected String getNameEmailFor(@Nullable Account.Id accountId) { if (accountId == null) { - return args.gerritPersonIdent.getName() - + " <" - + args.gerritPersonIdent.getEmailAddress() - + ">"; + PersonIdent gerritIdent = args.gerritPersonIdent.get(); + return gerritIdent.getName() + " <" + gerritIdent.getEmailAddress() + ">"; } Optional<Account> account = args.accountCache.get(accountId).map(AccountState::account); @@ -575,7 +590,10 @@ /** Configures a soy renderer for the given template name and rendering data map. */ private SoySauce.Renderer configureRenderer(String templateName) { - return args.soySauce.renderTemplate(SOY_TEMPLATE_NAMESPACE + templateName).setData(soyContext); + return args.soySauce + .get() + .renderTemplate(SOY_TEMPLATE_NAMESPACE + templateName) + .setData(soyContext); } protected void removeUser(Account user) {
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmailValidator.java b/java/com/google/gerrit/server/mail/send/OutgoingEmailValidator.java index bc6c89e..e28a77d 100644 --- a/java/com/google/gerrit/server/mail/send/OutgoingEmailValidator.java +++ b/java/com/google/gerrit/server/mail/send/OutgoingEmailValidator.java
@@ -24,6 +24,13 @@ import org.apache.commons.validator.routines.EmailValidator; import org.eclipse.jgit.lib.Config; +/** + * Validator that checks if an email address is valid and allowed for receiving notification emails. + * + * <p>An email address is valid if it is syntactically correct. + * + * <p>An email address is allowed if its top level domain is allowed by Gerrit's configuration. + */ @Singleton public class OutgoingEmailValidator { private static final FluentLogger logger = FluentLogger.forEnclosingClass();
diff --git a/java/com/google/gerrit/server/mail/send/ProjectWatch.java b/java/com/google/gerrit/server/mail/send/ProjectWatch.java index 934a0a0..be36f55 100644 --- a/java/com/google/gerrit/server/mail/send/ProjectWatch.java +++ b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -241,9 +241,9 @@ Predicate<ChangeData> p = null; if (user == null) { - qb = args.queryBuilder.asUser(args.anonymousUser); + qb = args.queryBuilder.get().asUser(args.anonymousUser.get()); } else { - qb = args.queryBuilder.asUser(user); + qb = args.queryBuilder.get().asUser(user); p = qb.is_visible(); }
diff --git a/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java b/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java index 91d8e81..e1daec6 100644 --- a/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java +++ b/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java
@@ -24,6 +24,10 @@ import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; +/** + * Sender that informs a user by email about the registration of a new email address for their + * account. + */ public class RegisterNewEmailSender extends OutgoingEmail { public interface Factory { RegisterNewEmailSender create(String address); @@ -58,17 +62,6 @@ appendText(textTemplate("RegisterNewEmail")); } - public String getUserNameEmail() { - return getUserNameEmailFor(user.getAccountId()); - } - - public String getEmailRegistrationToken() { - if (emailToken == null) { - emailToken = requireNonNull(tokenVerifier.encode(user.getAccountId(), addr), "token"); - } - return emailToken; - } - public boolean isAllowed() { return args.emailSender.canEmail(addr); } @@ -77,6 +70,13 @@ protected void setupSoyContext() { super.setupSoyContext(); soyContextEmailData.put("emailRegistrationToken", getEmailRegistrationToken()); - soyContextEmailData.put("userNameEmail", getUserNameEmail()); + soyContextEmailData.put("userNameEmail", getUserNameEmailFor(user.getAccountId())); + } + + private String getEmailRegistrationToken() { + if (emailToken == null) { + emailToken = requireNonNull(tokenVerifier.encode(user.getAccountId(), addr), "token"); + } + return emailToken; } }
diff --git a/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java b/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java index 850f775..2b1e362 100644 --- a/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java +++ b/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java
@@ -22,6 +22,13 @@ import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; +/** + * Sender that informs a user by email that they were set as assignee on a change. + * + * <p>In contrast to other change emails this email is not sent to the change authors (owner, patch + * set uploader, author). This is why this class extends {@link ChangeEmail} directly, instead of + * extending {@link ReplyToChangeSender}. + */ public class SetAssigneeSender extends ChangeEmail { public interface Factory { SetAssigneeSender create(Project.NameKey project, Change.Id changeId, Account.Id assignee); @@ -54,14 +61,10 @@ } } - public String getAssigneeName() { - return getNameFor(assignee); - } - @Override protected void setupSoyContext() { super.setupSoyContext(); - soyContextEmailData.put("assigneeName", getAssigneeName()); + soyContextEmailData.put("assigneeName", getNameFor(assignee)); } @Override
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java index 0acf20e..9a1ba35 100644 --- a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java +++ b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -76,6 +76,7 @@ } } + /** An {@link AutoCloseable} for parsing a single commit into ChangeNotesCommits. */ public static class LoadHandle implements AutoCloseable { private final Repository repo; private final ObjectId id;
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java index ce88f07..d301d34 100644 --- a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java +++ b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -51,11 +51,11 @@ private final Change change; protected final PersonIdent serverIdent; - protected PatchSet.Id psId; + @Nullable protected PatchSet.Id psId; private ObjectId result; - protected boolean rootOnly; + boolean rootOnly; - protected AbstractChangeUpdate( + AbstractChangeUpdate( ChangeNotes notes, CurrentUser user, PersonIdent serverIdent, @@ -72,7 +72,7 @@ this.when = when; } - protected AbstractChangeUpdate( + AbstractChangeUpdate( ChangeNoteUtil noteUtil, PersonIdent serverIdent, @Nullable ChangeNotes notes, @@ -172,7 +172,7 @@ public abstract boolean isEmpty(); /** Wether this update can only be a root commit. */ - public boolean isRootOnly() { + boolean isRootOnly() { return rootOnly; } @@ -256,7 +256,7 @@ protected abstract CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins, ObjectId curr) throws IOException; - protected static final CommitBuilder NO_OP_UPDATE = new CommitBuilder(); + static final CommitBuilder NO_OP_UPDATE = new CommitBuilder(); ObjectId getResult() { return result; @@ -270,7 +270,7 @@ return ins.insert(Constants.OBJ_TREE, new byte[] {}); } - protected void verifyComment(Comment c) { + void verifyComment(Comment c) { checkArgument(c.getCommitId() != null, "commit ID required for comment: %s", c); checkArgument( c.author.getId().equals(getAccountId()),
diff --git a/java/com/google/gerrit/server/notedb/AllUsersAsyncUpdate.java b/java/com/google/gerrit/server/notedb/AllUsersAsyncUpdate.java index 5d909d0..030cfb2 100644 --- a/java/com/google/gerrit/server/notedb/AllUsersAsyncUpdate.java +++ b/java/com/google/gerrit/server/notedb/AllUsersAsyncUpdate.java
@@ -91,7 +91,7 @@ executor.submit( () -> { try (OpenRepo allUsersRepo = OpenRepo.open(repoManager, allUsersName)) { - allUsersRepo.addUpdates(draftUpdates); + allUsersRepo.addUpdatesNoLimits(draftUpdates); allUsersRepo.flush(); BatchRefUpdate bru = allUsersRepo.repo.getRefDatabase().newBatchUpdate(); bru.setPushCertificate(pushCert);
diff --git a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java index b55ce49..72a460c 100644 --- a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java +++ b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -203,6 +203,7 @@ cache.get(k.commitId()).deleteComment(k.key()); } + // keyed by commit ID. Map<ObjectId, RevisionNoteBuilder> builders = cache.getBuilders(); boolean touchedAnyRevs = false; for (Map.Entry<ObjectId, RevisionNoteBuilder> e : builders.entrySet()) { @@ -233,7 +234,8 @@ return null; } - cb.setTreeId(rnm.noteMap.writeTree(ins)); + ObjectId treeId = rnm.noteMap.writeTree(ins); + cb.setTreeId(treeId); return cb; }
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java b/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java index 78f6afc..71cb8c9 100644 --- a/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java +++ b/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java
@@ -43,6 +43,8 @@ * </ul> */ public class ChangeNotesCommit extends RevCommit { + + /** A {@link RevWalk} producing {@link ChangeNotesCommit}s. */ public static ChangeNotesRevWalk newRevWalk(Repository repo) { return new ChangeNotesRevWalk(repo); } @@ -62,6 +64,7 @@ }; } + /** A {@link RevWalk} that creates {@link ChangeNotesCommit}s rather than {@link RevCommit}s */ public static class ChangeNotesRevWalk extends RevWalk { private ChangeNotesRevWalk(Repository repo) { super(repo);
diff --git a/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java b/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java index b6443f1..4e52093 100644 --- a/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java +++ b/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java
@@ -28,6 +28,7 @@ import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.util.MutableInteger; +/** Implements the parsing of comment data, handling JSON decoding and push certificates. */ class ChangeRevisionNote extends RevisionNote<Comment> { private final ChangeNoteJson noteJson; private final Comment.Status status;
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java index b2f85fc..6a900c0 100644 --- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java +++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -55,11 +55,11 @@ import com.google.gerrit.entities.Comment; import com.google.gerrit.entities.Project; import com.google.gerrit.entities.RobotComment; +import com.google.gerrit.entities.SubmissionId; import com.google.gerrit.exceptions.StorageException; import com.google.gerrit.mail.Address; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.GerritPersonIdent; -import com.google.gerrit.server.logging.RequestId; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.util.LabelVote; import com.google.inject.assistedinject.Assisted; @@ -220,8 +220,10 @@ this.status = status; } - public void fixStatus(Change.Status status) { - this.status = status; + public void fixStatusToMerged(SubmissionId submissionId) { + checkArgument(submissionId != null, "submission id must be set for merged changes"); + this.status = Change.Status.MERGED; + this.submissionId = submissionId.toString(); } public void putApproval(String label, short value) { @@ -240,9 +242,9 @@ approvals.put(label, reviewer, Optional.empty()); } - public void merge(RequestId submissionId, Iterable<SubmitRecord> submitRecords) { + public void merge(SubmissionId submissionId, Iterable<SubmitRecord> submitRecords) { this.status = Change.Status.MERGED; - this.submissionId = submissionId.toStringForStorage(); + this.submissionId = submissionId.toString(); this.submitRecords = ImmutableList.copyOf(submitRecords); checkArgument(!this.submitRecords.isEmpty(), "no submit records specified at submit time"); }
diff --git a/java/com/google/gerrit/server/notedb/LimitExceededException.java b/java/com/google/gerrit/server/notedb/LimitExceededException.java new file mode 100644 index 0000000..69f9241 --- /dev/null +++ b/java/com/google/gerrit/server/notedb/LimitExceededException.java
@@ -0,0 +1,34 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.notedb; + +import com.google.gerrit.exceptions.StorageException; + +/** + * A write operation was rejected because a limit would be exceeded. Limits are currently imposed + * on: + * + * <ul> + * <li>The number of NoteDb updates per change. + * <li>The number of patch sets per change. + * </ul> + */ +public class LimitExceededException extends StorageException { + private static final long serialVersionUID = 1L; + + LimitExceededException(String message) { + super(message); + } +}
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java index 7022cdc..1b92c0e 100644 --- a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java +++ b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -64,6 +64,10 @@ * {@link #stage()}. */ public class NoteDbUpdateManager implements AutoCloseable { + private static final int MAX_UPDATES_DEFAULT = 1000; + /** Limits the number of patch sets that can be created. Can be overridden in the config. */ + private static final int MAX_PATCH_SETS_DEFAULT = 1500; + public interface Factory { NoteDbUpdateManager create(Project.NameKey projectName); } @@ -74,11 +78,12 @@ private final NoteDbMetrics metrics; private final Project.NameKey projectName; private final int maxUpdates; + private final int maxPatchSets; private final ListMultimap<String, ChangeUpdate> changeUpdates; private final ListMultimap<String, ChangeDraftUpdate> draftUpdates; private final ListMultimap<String, RobotCommentUpdate> robotCommentUpdates; private final ListMultimap<String, NoteDbRewriter> rewriters; - private final Set<Change.Id> toDelete; + private final Set<Change.Id> changesToDelete; private OpenRepo changeRepo; private OpenRepo allUsersRepo; @@ -103,12 +108,13 @@ this.metrics = metrics; this.updateAllUsersAsync = updateAllUsersAsync; this.projectName = projectName; - maxUpdates = cfg.getInt("change", null, "maxUpdates", 1000); + maxUpdates = cfg.getInt("change", null, "maxUpdates", MAX_UPDATES_DEFAULT); + maxPatchSets = cfg.getInt("change", null, "maxPatchSets", MAX_PATCH_SETS_DEFAULT); changeUpdates = MultimapBuilder.hashKeys().arrayListValues().build(); draftUpdates = MultimapBuilder.hashKeys().arrayListValues().build(); robotCommentUpdates = MultimapBuilder.hashKeys().arrayListValues().build(); rewriters = MultimapBuilder.hashKeys().arrayListValues().build(); - toDelete = new HashSet<>(); + changesToDelete = new HashSet<>(); } @Override @@ -181,7 +187,7 @@ && draftUpdates.isEmpty() && robotCommentUpdates.isEmpty() && rewriters.isEmpty() - && toDelete.isEmpty() + && changesToDelete.isEmpty() && !hasCommands(changeRepo) && !hasCommands(allUsersRepo) && updateAllUsersAsync.isEmpty(); @@ -258,7 +264,7 @@ public void deleteChange(Change.Id id) { checkNotExecuted(); - toDelete.add(id); + changesToDelete.add(id); } /** @@ -273,7 +279,7 @@ } initChangeRepo(); - if (!draftUpdates.isEmpty() || !toDelete.isEmpty()) { + if (!draftUpdates.isEmpty() || !changesToDelete.isEmpty()) { initAllUsersRepo(); } addCommands(); @@ -351,23 +357,23 @@ } private void addCommands() throws IOException { - changeRepo.addUpdates(changeUpdates, Optional.of(maxUpdates)); + changeRepo.addUpdates(changeUpdates, Optional.of(maxUpdates), Optional.of(maxPatchSets)); if (!draftUpdates.isEmpty()) { boolean publishOnly = draftUpdates.values().stream().allMatch(ChangeDraftUpdate::canRunAsync); if (publishOnly) { updateAllUsersAsync.setDraftUpdates(draftUpdates); } else { - allUsersRepo.addUpdates(draftUpdates); + allUsersRepo.addUpdatesNoLimits(draftUpdates); } } if (!robotCommentUpdates.isEmpty()) { - changeRepo.addUpdates(robotCommentUpdates); + changeRepo.addUpdatesNoLimits(robotCommentUpdates); } if (!rewriters.isEmpty()) { addRewrites(rewriters, changeRepo); } - for (Change.Id id : toDelete) { + for (Change.Id id : changesToDelete) { doDelete(id); } } @@ -375,17 +381,16 @@ private void doDelete(Change.Id id) throws IOException { String metaRef = RefNames.changeMetaRef(id); Optional<ObjectId> old = changeRepo.cmds.get(metaRef); - if (old.isPresent()) { - changeRepo.cmds.add(new ReceiveCommand(old.get(), ObjectId.zeroId(), metaRef)); - } + old.ifPresent( + objectId -> changeRepo.cmds.add(new ReceiveCommand(objectId, ObjectId.zeroId(), metaRef))); // Just scan repo for ref names, but get "old" values from cmds. for (Ref r : allUsersRepo.repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftCommentsPrefix(id))) { old = allUsersRepo.cmds.get(r.getName()); - if (old.isPresent()) { - allUsersRepo.cmds.add(new ReceiveCommand(old.get(), ObjectId.zeroId(), r.getName())); - } + old.ifPresent( + objectId -> + allUsersRepo.cmds.add(new ReceiveCommand(objectId, ObjectId.zeroId(), r.getName()))); } }
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUtil.java b/java/com/google/gerrit/server/notedb/NoteDbUtil.java index 58a33c8..396e29b 100644 --- a/java/com/google/gerrit/server/notedb/NoteDbUtil.java +++ b/java/com/google/gerrit/server/notedb/NoteDbUtil.java
@@ -20,7 +20,6 @@ import com.google.common.primitives.Ints; import com.google.gerrit.entities.Account; import com.google.gerrit.extensions.restapi.RestModifyView; -import com.google.gerrit.server.update.RetryingRestModifyView; import java.sql.Timestamp; import java.util.Optional; import org.eclipse.jgit.lib.PersonIdent; @@ -34,8 +33,7 @@ private static final ImmutableList<String> PACKAGE_PREFIXES = ImmutableList.of("com.google.gerrit.server.", "com.google.gerrit."); private static final ImmutableSet<String> SERVLET_NAMES = - ImmutableSet.of( - "com.google.gerrit.httpd.restapi.RestApiServlet", RetryingRestModifyView.class.getName()); + ImmutableSet.of("com.google.gerrit.httpd.restapi.RestApiServlet"); /** Returns an AccountId for the given email address. */ public static Optional<Account.Id> parseIdent(PersonIdent ident) { @@ -74,13 +72,16 @@ StackTraceElement[] trace = Thread.currentThread().getStackTrace(); int i = findRestApiServlet(trace); if (i < 0) { + i = findApiImpl(trace); + } + if (i < 0) { return null; } try { for (i--; i >= 0; i--) { String cn = trace[i].getClassName(); Class<?> cls = Class.forName(cn); - if (RestModifyView.class.isAssignableFrom(cls) && cls != RetryingRestModifyView.class) { + if (RestModifyView.class.isAssignableFrom(cls)) { return viewName(cn); } } @@ -110,6 +111,16 @@ return -1; } + private static int findApiImpl(StackTraceElement[] trace) { + for (int i = 0; i < trace.length; i++) { + String clazz = trace[i].getClassName(); + if (clazz.startsWith("com.google.gerrit.server.api.") && clazz.endsWith("ApiImpl")) { + return i; + } + } + return -1; + } + private static String viewName(String cn) { String impl = cn.replace('$', '.'); for (String p : PACKAGE_PREFIXES) {
diff --git a/java/com/google/gerrit/server/notedb/OpenRepo.java b/java/com/google/gerrit/server/notedb/OpenRepo.java index de88684..351f31d 100644 --- a/java/com/google/gerrit/server/notedb/OpenRepo.java +++ b/java/com/google/gerrit/server/notedb/OpenRepo.java
@@ -43,6 +43,15 @@ * objects that are jointly closed when invoking {@link #close}. */ class OpenRepo implements AutoCloseable { + final Repository repo; + final RevWalk rw; + final ChainedReceiveCommands cmds; + final ObjectInserter tempIns; + + private final InMemoryInserter inMemIns; + @Nullable private final ObjectInserter finalIns; + private final boolean close; + /** Returns a {@link OpenRepo} wrapping around an open {@link Repository}. */ static OpenRepo open(GitRepositoryManager repoManager, Project.NameKey project) throws IOException { @@ -60,15 +69,6 @@ } } - final Repository repo; - final RevWalk rw; - final ChainedReceiveCommands cmds; - final ObjectInserter tempIns; - - private final InMemoryInserter inMemIns; - @Nullable private final ObjectInserter finalIns; - private final boolean close; - OpenRepo( Repository repo, RevWalk rw, @@ -125,12 +125,15 @@ return updates.iterator().next().allowWriteToNewRef(); } - <U extends AbstractChangeUpdate> void addUpdates(ListMultimap<String, U> all) throws IOException { - addUpdates(all, Optional.empty()); + <U extends AbstractChangeUpdate> void addUpdatesNoLimits(ListMultimap<String, U> all) + throws IOException { + addUpdates( + all, Optional.empty() /* unlimited updates */, Optional.empty() /* unlimited patch sets */); } <U extends AbstractChangeUpdate> void addUpdates( - ListMultimap<String, U> all, Optional<Integer> maxUpdates) throws IOException { + ListMultimap<String, U> all, Optional<Integer> maxUpdates, Optional<Integer> maxPatchSets) + throws IOException { for (Map.Entry<String, Collection<U>> e : all.asMap().entrySet()) { String refName = e.getKey(); Collection<U> updates = e.getValue(); @@ -142,29 +145,43 @@ continue; } - int updateCount; + int updateCount = 0; U first = updates.iterator().next(); if (maxUpdates.isPresent()) { checkState(first.getNotes() != null, "expected ChangeNotes on %s", first); updateCount = first.getNotes().getUpdateCount(); - } else { - updateCount = 0; } ObjectId curr = old; - for (U u : updates) { - if (u.isRootOnly() && !old.equals(ObjectId.zeroId())) { + for (U update : updates) { + if (maxPatchSets.isPresent() && update.psId != null) { + // Patch set IDs are assigned consecutively. Patch sets may have been deleted, but the ID + // is still a good estimate and an upper bound. + if (update.psId.get() > maxPatchSets.get()) { + throw new LimitExceededException( + String.format( + "Change %d may not exceed %d patch sets. To continue working on this change, " + + "recreate it with a new Change-Id, then abandon this one.", + update.getId().get(), maxPatchSets.get())); + } + } + if (update.isRootOnly() && !old.equals(ObjectId.zeroId())) { throw new StorageException("Given ChangeUpdate is only allowed on initial commit"); } - ObjectId next = u.apply(rw, tempIns, curr); + ObjectId next = update.apply(rw, tempIns, curr); if (next == null) { continue; } if (maxUpdates.isPresent() && !Objects.equals(next, curr) && ++updateCount > maxUpdates.get() - && !u.bypassMaxUpdates()) { - throw new TooManyUpdatesException(u.getId(), maxUpdates.get()); + && !update.bypassMaxUpdates()) { + throw new LimitExceededException( + String.format( + "Change %s may not exceed %d updates. It may still be abandoned or submitted. To" + + " continue working on this change, recreate it with a new Change-Id, then" + + " abandon this one.", + update.getId(), maxUpdates.get())); } curr = next; }
diff --git a/java/com/google/gerrit/server/notedb/RepoSequence.java b/java/com/google/gerrit/server/notedb/RepoSequence.java index 8096f89..11ba8cd 100644 --- a/java/com/google/gerrit/server/notedb/RepoSequence.java +++ b/java/com/google/gerrit/server/notedb/RepoSequence.java
@@ -291,4 +291,48 @@ ObjectId newId = ins.insert(OBJ_BLOB, Integer.toString(val).getBytes(UTF_8)); return new ReceiveCommand(ObjectId.zeroId(), newId, RefNames.REFS_SEQUENCES + name); } + + public void storeNew(int value) { + counterLock.lock(); + try (Repository repo = repoManager.openRepository(projectName); + RevWalk rw = new RevWalk(repo)) { + Optional<IntBlob> blob = IntBlob.parse(repo, refName, rw); + afterReadRef.run(); + ObjectId oldId; + if (!blob.isPresent()) { + oldId = ObjectId.zeroId(); + } else { + oldId = blob.get().id(); + } + RefUpdate refUpdate = + IntBlob.tryStore(repo, rw, projectName, refName, oldId, value, gitRefUpdated); + RefUpdateUtil.checkResult(refUpdate); + counter = value; + limit = counter + batchSize; + acquireCount++; + } catch (IOException e) { + throw new StorageException(e); + } finally { + counterLock.unlock(); + } + } + + public int current() { + counterLock.lock(); + try (Repository repo = repoManager.openRepository(projectName); + RevWalk rw = new RevWalk(repo)) { + Optional<IntBlob> blob = IntBlob.parse(repo, refName, rw); + int current; + if (!blob.isPresent()) { + current = seed.get(); + } else { + current = blob.get().value(); + } + return current; + } catch (IOException e) { + throw new StorageException(e); + } finally { + counterLock.unlock(); + } + } }
diff --git a/java/com/google/gerrit/server/notedb/RevisionNote.java b/java/com/google/gerrit/server/notedb/RevisionNote.java index ff649a9..cd11e1b 100644 --- a/java/com/google/gerrit/server/notedb/RevisionNote.java +++ b/java/com/google/gerrit/server/notedb/RevisionNote.java
@@ -26,6 +26,10 @@ import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.util.MutableInteger; +/** + * Data stored in a note, parsed on demand. The data type to parse into is a generic list of type T. + * The source of the data is a array of raw bytes + */ @UsedAt(UsedAt.Project.PLUGIN_CHECKS) public abstract class RevisionNote<T> { static final int MAX_NOTE_SZ = 25 << 20; @@ -64,6 +68,7 @@ return entities; } + /** Reads the raw data, and delegates parsing to the {@link #parse(byte[], int)} method. */ public void parse() throws IOException, ConfigInvalidException { raw = reader.open(noteId, OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ); MutableInteger p = new MutableInteger();
diff --git a/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java b/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java index e63737c..81273dc 100644 --- a/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java +++ b/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
@@ -36,6 +36,7 @@ import org.eclipse.jgit.lib.ObjectId; class RevisionNoteBuilder { + /** Construct a new RevisionNoteMap, seeding it with an existing (immutable) RevisionNoteMap */ static class Cache { private final RevisionNoteMap<? extends RevisionNote<? extends Comment>> revisionNoteMap; private final Map<ObjectId, RevisionNoteBuilder> builders;
diff --git a/java/com/google/gerrit/server/notedb/RevisionNoteMap.java b/java/com/google/gerrit/server/notedb/RevisionNoteMap.java index 3e1bad1..cf16073 100644 --- a/java/com/google/gerrit/server/notedb/RevisionNoteMap.java +++ b/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
@@ -25,8 +25,16 @@ import org.eclipse.jgit.notes.Note; import org.eclipse.jgit.notes.NoteMap; +/** + * A utility class that parses a NoteMap into commit => comment list data. + * + * @param <T> the RevisionNote for the comment type. + */ class RevisionNoteMap<T extends RevisionNote<? extends Comment>> { + // CommitID => blob ID final NoteMap noteMap; + + // CommitID => parsed data, immutable map. final ImmutableMap<ObjectId, T> revisionNotes; static RevisionNoteMap<ChangeRevisionNote> parse( @@ -36,6 +44,7 @@ for (Note note : noteMap) { ChangeRevisionNote rn = new ChangeRevisionNote(noteJson, reader, note.getData(), status); rn.parse(); + result.put(note.copy(), rn); } return new RevisionNoteMap<>(noteMap, ImmutableMap.copyOf(result));
diff --git a/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNote.java b/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNote.java index 97a8ad4..fc4c9fd 100644 --- a/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNote.java +++ b/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNote.java
@@ -26,6 +26,7 @@ import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; +/** Like {@link RevisionNote} but for robot comments. */ public class RobotCommentsRevisionNote extends RevisionNote<RobotComment> { private final ChangeNoteJson noteUtil;
diff --git a/java/com/google/gerrit/server/notedb/Sequences.java b/java/com/google/gerrit/server/notedb/Sequences.java index 73cc600..be68592 100644 --- a/java/com/google/gerrit/server/notedb/Sequences.java +++ b/java/com/google/gerrit/server/notedb/Sequences.java
@@ -128,4 +128,28 @@ return groupSeq.next(); } } + + public int currentChangeId() { + return changeSeq.current(); + } + + public int currentAccountId() { + return accountSeq.current(); + } + + public int currentGroupId() { + return groupSeq.current(); + } + + public void setChangeIdValue(int value) { + changeSeq.storeNew(value); + } + + public void setAccountIdValue(int value) { + accountSeq.storeNew(value); + } + + public void setGroupIdValue(int value) { + groupSeq.storeNew(value); + } }
diff --git a/java/com/google/gerrit/server/notedb/TooManyUpdatesException.java b/java/com/google/gerrit/server/notedb/TooManyUpdatesException.java deleted file mode 100644 index 9c6faaf..0000000 --- a/java/com/google/gerrit/server/notedb/TooManyUpdatesException.java +++ /dev/null
@@ -1,41 +0,0 @@ -// Copyright (C) 2019 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.gerrit.server.notedb; - -import com.google.common.annotations.VisibleForTesting; -import com.google.gerrit.entities.Change; -import com.google.gerrit.exceptions.StorageException; - -/** - * Exception indicating that the change has received too many updates. Further actions apart from - * {@code abandon} or {@code submit} are blocked. - */ -public class TooManyUpdatesException extends StorageException { - @VisibleForTesting - public static String message(Change.Id id, int maxUpdates) { - return "Change " - + id - + " may not exceed " - + maxUpdates - + " updates. It may still be abandoned or submitted. To continue working on this " - + "change, recreate it with a new Change-Id, then abandon this one."; - } - - private static final long serialVersionUID = 1L; - - TooManyUpdatesException(Change.Id id, int maxUpdates) { - super(message(id, maxUpdates)); - } -}
diff --git a/java/com/google/gerrit/server/patch/AutoMerger.java b/java/com/google/gerrit/server/patch/AutoMerger.java index a9cc9b5..2e0214c 100644 --- a/java/com/google/gerrit/server/patch/AutoMerger.java +++ b/java/com/google/gerrit/server/patch/AutoMerger.java
@@ -16,14 +16,17 @@ import static com.google.common.base.Preconditions.checkArgument; -import com.google.common.flogger.FluentLogger; +import com.google.common.base.Throwables; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.UsedAt; import com.google.gerrit.entities.RefNames; +import com.google.gerrit.git.LockFailureException; import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.git.InMemoryInserter; import com.google.gerrit.server.git.MergeUtil; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryableAction.ActionType; import com.google.inject.Inject; import java.io.IOException; import org.eclipse.jgit.dircache.DirCache; @@ -42,28 +45,54 @@ import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevWalk; +/** + * Utility class for creating an auto-merge commit of a merge commit. + * + * <p>An auto-merge commit is the result of merging the 2 parents of a merge commit automatically. + * If there are conflicts the auto-merge commit contains Git conflict markers that indicate these + * conflicts. + * + * <p>Creating auto-merge commits for octopus merges (merge commits with more than 2 parents) is not + * supported. In this case the auto-merge is created between the first 2 parent commits. + * + * <p>All created auto-merge commits are stored in the repository of their merge commit as {@code + * refs/cache-automerge/} branches. These branches serve: + * + * <ul> + * <li>as a cache so that the each auto-merge gets computed only once + * <li>as base for merge commits on which users can comment + * </ul> + * + * <p>The second point means that these commits are referenced from NoteDb. The consequence of this + * is that these refs should never be deleted. + */ public class AutoMerger { - private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - @UsedAt(UsedAt.Project.GOOGLE) public static boolean cacheAutomerge(Config cfg) { return cfg.getBoolean("change", null, "cacheAutomerge", true); } + private final RetryHelper retryHelper; private final PersonIdent gerritIdent; private final boolean save; @Inject - AutoMerger(@GerritServerConfig Config cfg, @GerritPersonIdent PersonIdent gerritIdent) { + AutoMerger( + RetryHelper retryHelper, + @GerritServerConfig Config cfg, + @GerritPersonIdent PersonIdent gerritIdent) { + this.retryHelper = retryHelper; save = cacheAutomerge(cfg); this.gerritIdent = gerritIdent; } /** - * Perform an auto-merge of the parents of the given merge commit. + * Creates an auto-merge commit of the parents of the given merge commit. * - * @return auto-merge commit or {@code null} if an auto-merge commit couldn't be created. Headers - * of the returned RevCommit are parsed. + * <p>In case of an exception the creation of the auto-merge commit is retried a few times. E.g. + * this allows the operation to succeed if a Git update fails due to a temporary issue. + * + * @return auto-merge commit. Headers of the returned RevCommit are parsed. */ public RevCommit merge( Repository repo, @@ -72,7 +101,34 @@ RevCommit merge, ThreeWayMergeStrategy mergeStrategy) throws IOException { + try { + return retryHelper + .action( + ActionType.GIT_UPDATE, + "createAutoMerge", + () -> createAutoMergeCommit(repo, rw, ins, merge, mergeStrategy)) + .call(); + } catch (Exception e) { + Throwables.throwIfUnchecked(e); + Throwables.throwIfInstanceOf(e, IOException.class); + throw new IllegalStateException(e); + } + } + + /** + * Creates an auto-merge commit of the parents of the given merge commit. + * + * @return auto-merge commit. Headers of the returned RevCommit are parsed. + */ + private RevCommit createAutoMergeCommit( + Repository repo, + RevWalk rw, + ObjectInserter ins, + RevCommit merge, + ThreeWayMergeStrategy mergeStrategy) + throws IOException { checkArgument(rw.getObjectReader().getCreatedFromInserter() == ins); + InMemoryInserter tmpIns = null; if (ins instanceof InMemoryInserter) { // Caller gave us an in-memory inserter, so ensure anything we write from @@ -102,17 +158,7 @@ m.setDirCache(dc); m.setObjectInserter(tmpIns == null ? new NonFlushingWrapper(ins) : tmpIns); - boolean couldMerge; - try { - couldMerge = m.merge(merge.getParents()); - } catch (IOException | RuntimeException e) { - // It is not safe to continue further down in this method as throwing - // an exception most likely means that the merge tree was not created - // and m.getMergeResults() is empty. This would mean that all paths are - // unmerged and Gerrit UI would show all paths in the patch list. - logger.atWarning().withCause(e).log("Error attempting automerge %s", refName); - return null; - } + boolean couldMerge = m.merge(merge.getParents()); ObjectId treeId; if (couldMerge) { @@ -173,8 +219,28 @@ RefUpdate ru = repo.updateRef(refName); ru.setNewObjectId(commitId); ru.disableRefLog(); - ru.forceUpdate(); - return rw.parseCommit(commitId); + switch (ru.forceUpdate()) { + case FAST_FORWARD: + case FORCED: + case NEW: + case NO_CHANGE: + return rw.parseCommit(commitId); + case LOCK_FAILURE: + throw new LockFailureException( + String.format("Failed to create auto-merge of %s", merge.name()), ru); + case IO_FAILURE: + case NOT_ATTEMPTED: + case REJECTED: + case REJECTED_CURRENT_BRANCH: + case REJECTED_MISSING_OBJECT: + case REJECTED_OTHER_REASON: + case RENAMED: + default: + throw new IOException( + String.format( + "Failed to create auto-merge of %s: Cannot write %s (%s)", + merge.name(), refName, ru.getResult())); + } } private static class NonFlushingWrapper extends ObjectInserter.Filter {
diff --git a/java/com/google/gerrit/server/patch/DiffContentCalculator.java b/java/com/google/gerrit/server/patch/DiffContentCalculator.java new file mode 100644 index 0000000..53f7ca6 --- /dev/null +++ b/java/com/google/gerrit/server/patch/DiffContentCalculator.java
@@ -0,0 +1,417 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.patch; + +import static java.util.Comparator.comparing; + +import com.google.common.collect.ImmutableList; +import com.google.gerrit.common.data.CommentDetail; +import com.google.gerrit.entities.Comment; +import com.google.gerrit.extensions.client.DiffPreferencesInfo; +import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace; +import com.google.gerrit.jgit.diff.ReplaceEdit; +import com.google.gerrit.prettify.common.EditList; +import com.google.gerrit.prettify.common.SparseFileContent; +import com.google.gerrit.prettify.common.SparseFileContentBuilder; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import org.eclipse.jgit.diff.Edit; + +/** Collects all lines and their content to be displayed in diff view. */ +class DiffContentCalculator { + private static final int MAX_CONTEXT = 5000000; + + private static final Comparator<Edit> EDIT_SORT = comparing(Edit::getBeginA); + + private final DiffPreferencesInfo diffPrefs; + + DiffContentCalculator(DiffPreferencesInfo diffPrefs) { + this.diffPrefs = diffPrefs; + } + + /** + * Gather information necessary to display line-by-line difference between 2 texts. + * + * <p>The method returns instance of {@link DiffCalculatorResult} with the following data: + * + * <ul> + * <li>All changed lines + * <li>Additional lines to be displayed above and below the changed lines + * <li>All changed and unchanged lines with comments + * <li>Additional lines to be displayed above and below lines with commentsEdits with special + * "fake" edits for unchanged lines with comments + * </ul> + * + * <p>More details can be found in {@link DiffCalculatorResult}. + * + * @param srcA Original text content + * @param srcB New text content + * @param edits List of edits which was applied to srcA to produce srcB + * @param comments Existing comments for srcA and srcB + * @return an instance of {@link DiffCalculatorResult}. + */ + DiffCalculatorResult calculateDiffContent( + TextSource srcA, TextSource srcB, ImmutableList<Edit> edits, CommentDetail comments) { + int context = getContext(); + if (srcA.src == srcB.src && srcA.size() <= context && edits.isEmpty()) { + // Odd special case; the files are identical (100% rename or copy) + // and the user has asked for context that is larger than the file. + // Send them the entire file, with an empty edit after the last line. + // + SparseFileContentBuilder diffA = new SparseFileContentBuilder(srcA.size()); + for (int i = 0; i < srcA.size(); i++) { + srcA.copyLineTo(diffA, i); + } + DiffContent diffContent = + new DiffContent(diffA.build(), SparseFileContent.create(ImmutableList.of(), srcB.size())); + Edit emptyEdit = new Edit(srcA.size(), srcA.size()); + return new DiffCalculatorResult(diffContent, ImmutableList.of(emptyEdit)); + } + ImmutableList.Builder<Edit> builder = ImmutableList.builder(); + + builder.addAll(correctForDifferencesInNewlineAtEnd(srcA, srcB, edits)); + + boolean nonsortedEdits = false; + if (comments != null) { + ImmutableList<Edit> commentEdits = ensureCommentsVisible(comments, edits); + builder.addAll(commentEdits); + nonsortedEdits = !commentEdits.isEmpty(); + } + + ImmutableList<Edit> sortedEdits = builder.build(); + if (nonsortedEdits) { + sortedEdits = ImmutableList.sortedCopyOf(EDIT_SORT, sortedEdits); + } + + // In order to expand the skipped common lines or syntax highlight the + // file properly we need to give the client the complete file contents. + // So force our context temporarily to the complete file size. + // + DiffContent diffContent = + packContent( + srcA, + srcB, + diffPrefs.ignoreWhitespace != Whitespace.IGNORE_NONE, + sortedEdits, + MAX_CONTEXT); + return new DiffCalculatorResult(diffContent, sortedEdits); + } + + private int getContext() { + if (diffPrefs.context == DiffPreferencesInfo.WHOLE_FILE_CONTEXT) { + return MAX_CONTEXT; + } + return Math.min(diffPrefs.context, MAX_CONTEXT); + } + + private ImmutableList<Edit> correctForDifferencesInNewlineAtEnd( + TextSource a, TextSource b, ImmutableList<Edit> edits) { + // a.src.size() is the size ignoring a newline at the end whereas a.size() considers it. + int aSize = a.src.size(); + int bSize = b.src.size(); + + if (edits.isEmpty() && (aSize == 0 || bSize == 0)) { + // The diff was requested for a file which was either added or deleted but which JGit doesn't + // consider a file addition/deletion (e.g. requesting a diff for the old file name of a + // renamed file looks like a deletion). + return edits; + } + + if (edits.isEmpty() && (aSize != bSize)) { + // Only edits due to rebase were present. If we now added the edits for the newlines, the + // code which later assembles the file contents would fail. + return edits; + } + + Optional<Edit> lastEdit = getLast(edits); + if (isNewlineAtEndDeleted(a, b)) { + Optional<Edit> lastLineEdit = lastEdit.filter(edit -> edit.getEndA() == aSize); + + if (lastLineEdit.isPresent()) { + Edit edit = lastLineEdit.get(); + Edit updatedLastLineEdit = + edit instanceof ReplaceEdit + ? new ReplaceEdit( + edit.getBeginA(), + edit.getEndA() + 1, + edit.getBeginB(), + edit.getEndB(), + ((ReplaceEdit) edit).getInternalEdits()) + : new Edit(edit.getBeginA(), edit.getEndA() + 1, edit.getBeginB(), edit.getEndB()); + + ImmutableList.Builder<Edit> newEditsBuilder = + ImmutableList.builderWithExpectedSize(edits.size()); + return newEditsBuilder + .addAll(edits.subList(0, edits.size() - 1)) + .add(updatedLastLineEdit) + .build(); + } + ImmutableList.Builder<Edit> newEditsBuilder = + ImmutableList.builderWithExpectedSize(edits.size() + 1); + Edit newlineEdit = new Edit(aSize, aSize + 1, bSize, bSize); + return newEditsBuilder.addAll(edits).add(newlineEdit).build(); + + } else if (isNewlineAtEndAdded(a, b)) { + Optional<Edit> lastLineEdit = lastEdit.filter(edit -> edit.getEndB() == bSize); + if (lastLineEdit.isPresent()) { + Edit edit = lastLineEdit.get(); + Edit updatedLastLineEdit = + edit instanceof ReplaceEdit + ? new ReplaceEdit( + edit.getBeginA(), + edit.getEndA(), + edit.getBeginB(), + edit.getEndB() + 1, + ((ReplaceEdit) edit).getInternalEdits()) + : new Edit(edit.getBeginA(), edit.getEndA(), edit.getBeginB(), edit.getEndB() + 1); + + ImmutableList.Builder<Edit> newEditsBuilder = + ImmutableList.builderWithExpectedSize(edits.size()); + return newEditsBuilder + .addAll(edits.subList(0, edits.size() - 1)) + .add(updatedLastLineEdit) + .build(); + } + ImmutableList.Builder<Edit> newEditsBuilder = + ImmutableList.builderWithExpectedSize(edits.size() + 1); + Edit newlineEdit = new Edit(aSize, aSize, bSize, bSize + 1); + return newEditsBuilder.addAll(edits).add(newlineEdit).build(); + } + return edits; + } + + private static <T> Optional<T> getLast(List<T> list) { + return list.isEmpty() ? Optional.empty() : Optional.ofNullable(list.get(list.size() - 1)); + } + + private boolean isNewlineAtEndDeleted(TextSource a, TextSource b) { + return !a.src.isMissingNewlineAtEnd() && b.src.isMissingNewlineAtEnd(); + } + + private boolean isNewlineAtEndAdded(TextSource a, TextSource b) { + return a.src.isMissingNewlineAtEnd() && !b.src.isMissingNewlineAtEnd(); + } + + private ImmutableList<Edit> ensureCommentsVisible( + CommentDetail comments, ImmutableList<Edit> edits) { + if (comments.getCommentsA().isEmpty() && comments.getCommentsB().isEmpty()) { + // No comments, no additional dummy edits are required. + // + return ImmutableList.of(); + } + + // Construct empty Edit blocks around each location where a comment is. + // This will force the later packContent method to include the regions + // containing comments, potentially combining those regions together if + // they have overlapping contexts. UI renders will also be able to make + // correct hunks from this, but because the Edit is empty they will not + // style it specially. + // + final ImmutableList.Builder<Edit> commmentEdits = ImmutableList.builder(); + int lastLine; + + lastLine = -1; + for (Comment c : comments.getCommentsA()) { + final int a = c.lineNbr; + if (lastLine != a) { + final int b = mapA2B(a - 1, edits); + if (0 <= b) { + getNewEditForComment(edits, new Edit(a - 1, b)).ifPresent(commmentEdits::add); + } + lastLine = a; + } + } + + lastLine = -1; + for (Comment c : comments.getCommentsB()) { + int b = c.lineNbr; + if (lastLine != b) { + final int a = mapB2A(b - 1, edits); + if (0 <= a) { + getNewEditForComment(edits, new Edit(a, b - 1)).ifPresent(commmentEdits::add); + } + lastLine = b; + } + } + return commmentEdits.build(); + } + + private Optional<Edit> getNewEditForComment(ImmutableList<Edit> edits, Edit toAdd) { + final int a = toAdd.getBeginA(); + final int b = toAdd.getBeginB(); + for (Edit e : edits) { + if (e.getBeginA() <= a && a <= e.getEndA()) { + return Optional.empty(); + } + if (e.getBeginB() <= b && b <= e.getEndB()) { + return Optional.empty(); + } + } + return Optional.of(toAdd); + } + + private int mapA2B(int a, ImmutableList<Edit> edits) { + if (edits.isEmpty()) { + // Magic special case of an unmodified file. + // + return a; + } + + for (int i = 0; i < edits.size(); i++) { + final Edit e = edits.get(i); + if (a < e.getBeginA()) { + if (i == 0) { + // Special case of context at start of file. + // + return a; + } + return e.getBeginB() - (e.getBeginA() - a); + } + if (e.getBeginA() <= a && a <= e.getEndA()) { + return -1; + } + } + + final Edit last = edits.get(edits.size() - 1); + return last.getEndB() + (a - last.getEndA()); + } + + private int mapB2A(int b, ImmutableList<Edit> edits) { + if (edits.isEmpty()) { + // Magic special case of an unmodified file. + // + return b; + } + + for (int i = 0; i < edits.size(); i++) { + final Edit e = edits.get(i); + if (b < e.getBeginB()) { + if (i == 0) { + // Special case of context at start of file. + // + return b; + } + return e.getBeginA() - (e.getBeginB() - b); + } + if (e.getBeginB() <= b && b <= e.getEndB()) { + return -1; + } + } + + final Edit last = edits.get(edits.size() - 1); + return last.getEndA() + (b - last.getEndB()); + } + + private DiffContent packContent( + TextSource a, + TextSource b, + boolean ignoredWhitespace, + ImmutableList<Edit> edits, + int context) { + SparseFileContentBuilder diffA = new SparseFileContentBuilder(a.size()); + SparseFileContentBuilder diffB = new SparseFileContentBuilder(b.size()); + EditList list = new EditList(edits, context, a.size(), b.size()); + for (EditList.Hunk hunk : list.getHunks()) { + while (hunk.next()) { + if (hunk.isContextLine()) { + String lineA = a.getSourceLine(hunk.getCurA()); + diffA.addLine(hunk.getCurA(), lineA); + + if (ignoredWhitespace) { + // If we ignored whitespace in some form, also get the line + // from b when it does not exactly match the line from a. + // + String lineB = b.getSourceLine(hunk.getCurB()); + if (!lineA.equals(lineB)) { + diffB.addLine(hunk.getCurB(), lineB); + } + } + hunk.incBoth(); + continue; + } + + if (hunk.isDeletedA()) { + a.copyLineTo(diffA, hunk.getCurA()); + hunk.incA(); + } + + if (hunk.isInsertedB()) { + b.copyLineTo(diffB, hunk.getCurB()); + hunk.incB(); + } + } + } + return new DiffContent(diffA.build(), diffB.build()); + } + + /** Contains information to be displayed in line-by-line diff view. */ + static class DiffCalculatorResult { + // This class is not @AutoValue, because Edit is mutable + + /** Lines to be displayed */ + final DiffContent diffContent; + /** List of edits including "fake" edits for unchanged lines with comments. */ + final ImmutableList<Edit> edits; + + DiffCalculatorResult(DiffContent diffContent, ImmutableList<Edit> edits) { + this.diffContent = diffContent; + this.edits = edits; + } + } + + /** Lines to be displayed in line-by-line diff view. */ + static class DiffContent { + /* All lines from the original text (i.e. srcA) to be displayed. */ + final SparseFileContent a; + /** + * All lines from the new text (i.e. srcB) which are different than in original text. Lines are: + * a) All changed lines (i.e. if the content of the line was replaced with the new line) b) All + * inserted lines Note, that deleted lines are added to the a and are not added to b + */ + final SparseFileContent b; + + DiffContent(SparseFileContent a, SparseFileContent b) { + this.a = a; + this.b = b; + } + } + + static class TextSource { + final Text src; + + TextSource(Text src) { + this.src = src; + } + + int size() { + if (src == null) { + return 0; + } + if (src.isMissingNewlineAtEnd()) { + return src.size(); + } + return src.size() + 1; + } + + void copyLineTo(SparseFileContentBuilder target, int lineNumber) { + target.addLine(lineNumber, getSourceLine(lineNumber)); + } + + private String getSourceLine(int lineNumber) { + return lineNumber >= src.size() ? "" : src.getString(lineNumber); + } + } +}
diff --git a/java/com/google/gerrit/server/patch/PatchListEntry.java b/java/com/google/gerrit/server/patch/PatchListEntry.java index 625f56c..f37b925 100644 --- a/java/com/google/gerrit/server/patch/PatchListEntry.java +++ b/java/com/google/gerrit/server/patch/PatchListEntry.java
@@ -37,7 +37,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; @@ -214,9 +213,10 @@ return sizeDelta; } - public List<String> getHeaderLines() { + public ImmutableList<String> getHeaderLines() { final IntList m = RawParseUtils.lineMap(header, 0, header.length); - final List<String> headerLines = new ArrayList<>(m.size() - 1); + final ImmutableList.Builder<String> headerLines = + ImmutableList.builderWithExpectedSize(m.size() - 1); for (int i = 1; i < m.size() - 1; i++) { final int b = m.get(i); int e = m.get(i + 1); @@ -225,7 +225,7 @@ } headerLines.add(RawParseUtils.decode(UTF_8, header, b, e)); } - return headerLines; + return headerLines.build(); } Patch toPatch(PatchSet.Id setId) {
diff --git a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java index f3c8eab..92df794 100644 --- a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java +++ b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -15,34 +15,32 @@ package com.google.gerrit.server.patch; import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Comparator.comparing; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.gerrit.common.data.CommentDetail; import com.google.gerrit.common.data.PatchScript; import com.google.gerrit.common.data.PatchScript.DisplayMethod; import com.google.gerrit.entities.Change; -import com.google.gerrit.entities.Comment; +import com.google.gerrit.entities.FixReplacement; import com.google.gerrit.entities.Patch; -import com.google.gerrit.entities.Project; +import com.google.gerrit.entities.Patch.ChangeType; +import com.google.gerrit.entities.Patch.PatchType; import com.google.gerrit.extensions.client.DiffPreferencesInfo; -import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace; -import com.google.gerrit.prettify.common.EditList; -import com.google.gerrit.prettify.common.SparseFileContent; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.server.fixes.FixCalculator; import com.google.gerrit.server.mime.FileTypeRegistry; +import com.google.gerrit.server.patch.DiffContentCalculator.DiffCalculatorResult; +import com.google.gerrit.server.patch.DiffContentCalculator.TextSource; import com.google.inject.Inject; import eu.medsea.mimeutil.MimeType; import eu.medsea.mimeutil.MimeUtil2; import java.io.IOException; -import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.Set; import org.eclipse.jgit.diff.Edit; -import org.eclipse.jgit.errors.CorruptObjectException; -import org.eclipse.jgit.errors.IncorrectObjectTypeException; -import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; @@ -53,39 +51,15 @@ import org.eclipse.jgit.treewalk.TreeWalk; class PatchScriptBuilder { - static final int MAX_CONTEXT = 5000000; - static final int BIG_FILE = 9000; - private static final Comparator<Edit> EDIT_SORT = comparing(Edit::getBeginA); - - private Repository db; - private Project.NameKey projectKey; - private ObjectReader reader; private Change change; private DiffPreferencesInfo diffPrefs; - private ComparisonType comparisonType; - private ObjectId aId; - private ObjectId bId; - - private final Side a; - private final Side b; - - private List<Edit> edits; private final FileTypeRegistry registry; - private final PatchListCache patchListCache; - private int context; + private IntraLineDiffCalculator intralineDiffCalculator; @Inject - PatchScriptBuilder(FileTypeRegistry ftr, PatchListCache plc) { - a = new Side(); - b = new Side(); + PatchScriptBuilder(FileTypeRegistry ftr) { registry = ftr; - patchListCache = plc; - } - - void setRepository(Repository r, Project.NameKey projectKey) { - this.db = r; - this.projectKey = projectKey; } void setChange(Change c) { @@ -94,107 +68,113 @@ void setDiffPrefs(DiffPreferencesInfo dp) { diffPrefs = dp; + } - context = diffPrefs.context; - if (context == DiffPreferencesInfo.WHOLE_FILE_CONTEXT) { - context = MAX_CONTEXT; - } else if (context > MAX_CONTEXT) { - context = MAX_CONTEXT; + void setIntraLineDiffCalculator(IntraLineDiffCalculator calculator) { + intralineDiffCalculator = calculator; + } + + PatchScript toPatchScript( + Repository git, + PatchList list, + PatchListEntry content, + CommentDetail comments, + ImmutableList<Patch> history) + throws IOException { + + PatchFileChange change = + new PatchFileChange( + content.getEdits(), + content.getEditsDueToRebase(), + content.getHeaderLines(), + content.getOldName(), + content.getNewName(), + content.getChangeType(), + content.getPatchType()); + SidesResolver sidesResolver = new SidesResolver(git, list.getComparisonType()); + ResolvedSides sides = + resolveSides( + git, sidesResolver, oldName(change), newName(change), list.getOldId(), list.getNewId()); + return build(sides.a, sides.b, change, comments, history); + } + + private ResolvedSides resolveSides( + Repository git, + SidesResolver sidesResolver, + String oldName, + String newName, + ObjectId aId, + ObjectId bId) + throws IOException { + try (ObjectReader reader = git.newObjectReader()) { + PatchSide a = sidesResolver.resolve(registry, reader, oldName, null, aId, true); + PatchSide b = + sidesResolver.resolve(registry, reader, newName, a, bId, Objects.equals(aId, bId)); + return new ResolvedSides(a, b); } } - void setTrees(ComparisonType ct, ObjectId a, ObjectId b) { - comparisonType = ct; - aId = a; - bId = b; + PatchScript toPatchScript( + Repository git, ObjectId baseId, String fileName, List<FixReplacement> fixReplacements) + throws IOException, ResourceConflictException { + SidesResolver sidesResolver = new SidesResolver(git, ComparisonType.againstOtherPatchSet()); + PatchSide a = resolveSideA(git, sidesResolver, fileName, baseId); + FixCalculator.FixResult fixResult = FixCalculator.calculateFix(a.src, fixReplacements); + PatchSide b = + new PatchSide( + null, + fileName, + ObjectId.zeroId(), + a.mode, + fixResult.text.getContent(), + fixResult.text, + a.mimeType, + a.displayMethod, + a.fileMode); + + PatchFileChange change = + new PatchFileChange( + fixResult.edits, + ImmutableSet.of(), + ImmutableList.of(), + fileName, + fileName, + ChangeType.MODIFIED, + PatchType.UNIFIED); + + return build(a, b, change, null, null); } - PatchScript toPatchScript(PatchListEntry content, CommentDetail comments, List<Patch> history) + private PatchSide resolveSideA( + Repository git, SidesResolver sidesResolver, String path, ObjectId baseId) throws IOException { - reader = db.newObjectReader(); - try { - return build(content, comments, history); - } finally { - reader.close(); + try (ObjectReader reader = git.newObjectReader()) { + return sidesResolver.resolve(registry, reader, path, null, baseId, true); } } - private PatchScript build(PatchListEntry content, CommentDetail comments, List<Patch> history) - throws IOException { - boolean intralineFailure = false; - boolean intralineTimeout = false; + private PatchScript build( + PatchSide a, + PatchSide b, + PatchFileChange content, + CommentDetail comments, + ImmutableList<Patch> history) { - a.path = oldName(content); - b.path = newName(content); - - a.resolve(null, aId); - b.resolve(a, bId); - - edits = new ArrayList<>(content.getEdits()); + ImmutableList<Edit> contentEdits = content.getEdits(); ImmutableSet<Edit> editsDueToRebase = content.getEditsDueToRebase(); - if (isModify(content) && diffPrefs.intralineDifference) { - IntraLineDiff d = - patchListCache.getIntraLineDiff( - IntraLineDiffKey.create(a.id, b.id, diffPrefs.ignoreWhitespace), - IntraLineDiffArgs.create( - a.src, b.src, edits, editsDueToRebase, projectKey, bId, b.path)); - if (d != null) { - switch (d.getStatus()) { - case EDIT_LIST: - edits = new ArrayList<>(d.getEdits()); - break; + IntraLineDiffCalculatorResult intralineResult = IntraLineDiffCalculatorResult.NO_RESULT; - case DISABLED: - break; - - case ERROR: - intralineFailure = true; - break; - - case TIMEOUT: - intralineTimeout = true; - break; - } - } else { - intralineFailure = true; - } + if (isModify(content) && intralineDiffCalculator != null && isIntralineModeAllowed(b)) { + intralineResult = + intralineDiffCalculator.calculateIntraLineDiff( + contentEdits, editsDueToRebase, a.id, b.id, a.src, b.src, b.treeId, b.path); } - - correctForDifferencesInNewlineAtEnd(); - - if (comments != null) { - ensureCommentsVisible(comments); - } - - boolean hugeFile = false; - if (a.src == b.src && a.size() <= context && content.getEdits().isEmpty()) { - // Odd special case; the files are identical (100% rename or copy) - // and the user has asked for context that is larger than the file. - // Send them the entire file, with an empty edit after the last line. - // - for (int i = 0; i < a.size(); i++) { - a.addLine(i); - } - edits = new ArrayList<>(1); - edits.add(new Edit(a.size(), a.size())); - - } else { - if (BIG_FILE < Math.max(a.size(), b.size())) { - // IF the file is really large, we disable things to avoid choking - // the browser client. - // - hugeFile = true; - } - - // In order to expand the skipped common lines or syntax highlight the - // file properly we need to give the client the complete file contents. - // So force our context temporarily to the complete file size. - // - context = MAX_CONTEXT; - - packContent(diffPrefs.ignoreWhitespace != Whitespace.IGNORE_NONE); - } + ImmutableList<Edit> finalEdits = intralineResult.edits.orElse(contentEdits); + DiffContentCalculator calculator = new DiffContentCalculator(diffPrefs); + DiffCalculatorResult diffCalculatorResult = + calculator.calculateDiffContent( + new TextSource(a.src), new TextSource(b.src), finalEdits, comments); return new PatchScript( change.getKey(), @@ -205,25 +185,23 @@ b.fileMode, content.getHeaderLines(), diffPrefs, - a.dst, - b.dst, - edits, + diffCalculatorResult.diffContent.a, + diffCalculatorResult.diffContent.b, + diffCalculatorResult.edits, editsDueToRebase, a.displayMethod, b.displayMethod, - a.mimeType.toString(), - b.mimeType.toString(), - comments, + a.mimeType, + b.mimeType, history, - hugeFile, - intralineFailure, - intralineTimeout, + intralineResult.failure, + intralineResult.timeout, content.getPatchType() == Patch.PatchType.BINARY, - aId == null ? null : aId.getName(), - bId == null ? null : bId.getName()); + a.treeId == null ? null : a.treeId.getName(), + b.treeId == null ? null : b.treeId.getName()); } - private static boolean isModify(PatchListEntry content) { + private static boolean isModify(PatchFileChange content) { switch (content.getChangeType()) { case MODIFIED: case COPIED: @@ -238,7 +216,7 @@ } } - private static String oldName(PatchListEntry entry) { + private static String oldName(PatchFileChange entry) { switch (entry.getChangeType()) { case ADDED: return null; @@ -253,7 +231,7 @@ } } - private static String newName(PatchListEntry entry) { + private static String newName(PatchFileChange entry) { switch (entry.getChangeType()) { case DELETED: return null; @@ -267,333 +245,194 @@ } } - private void correctForDifferencesInNewlineAtEnd() { - // a.src.size() is the size ignoring a newline at the end whereas a.size() considers it. - int aSize = a.src.size(); - int bSize = b.src.size(); + private static boolean isIntralineModeAllowed(PatchSide side) { + // The intraline diff cache keys are the same for these cases. It's better to not show + // intraline results than showing completely wrong diffs or to run into a server error. + return !Patch.isMagic(side.path) && !isSubmoduleCommit(side.mode); + } - if (edits.isEmpty() && (aSize == 0 || bSize == 0)) { - // The diff was requested for a file which was either added or deleted but which JGit doesn't - // consider a file addition/deletion (e.g. requesting a diff for the old file name of a - // renamed file looks like a deletion). - return; - } + private static boolean isSubmoduleCommit(FileMode mode) { + return mode.getObjectType() == Constants.OBJ_COMMIT; + } - Optional<Edit> lastEdit = getLast(edits); - if (isNewlineAtEndDeleted()) { - Optional<Edit> lastLineEdit = lastEdit.filter(edit -> edit.getEndA() == aSize); - if (lastLineEdit.isPresent()) { - lastLineEdit.get().extendA(); - } else { - Edit newlineEdit = new Edit(aSize, aSize + 1, bSize, bSize); - edits.add(newlineEdit); - } - } else if (isNewlineAtEndAdded()) { - Optional<Edit> lastLineEdit = lastEdit.filter(edit -> edit.getEndB() == bSize); - if (lastLineEdit.isPresent()) { - lastLineEdit.get().extendB(); - } else { - Edit newlineEdit = new Edit(aSize, aSize, bSize, bSize + 1); - edits.add(newlineEdit); - } + private static class PatchSide { + final ObjectId treeId; + final String path; + final ObjectId id; + final FileMode mode; + final byte[] srcContent; + final Text src; + final String mimeType; + final DisplayMethod displayMethod; + final PatchScript.FileMode fileMode; + + private PatchSide( + ObjectId treeId, + String path, + ObjectId id, + FileMode mode, + byte[] srcContent, + Text src, + String mimeType, + DisplayMethod displayMethod, + PatchScript.FileMode fileMode) { + this.treeId = treeId; + this.path = path; + this.id = id; + this.mode = mode; + this.srcContent = srcContent; + this.src = src; + this.mimeType = mimeType; + this.displayMethod = displayMethod; + this.fileMode = fileMode; } } - private static <T> Optional<T> getLast(List<T> list) { - return list.isEmpty() ? Optional.empty() : Optional.ofNullable(list.get(list.size() - 1)); - } + private static class ResolvedSides { + // Not an @AutoValue because PatchSide can't be AutoValue + public final PatchSide a; + public final PatchSide b; - private boolean isNewlineAtEndDeleted() { - return !a.src.isMissingNewlineAtEnd() && b.src.isMissingNewlineAtEnd(); - } - - private boolean isNewlineAtEndAdded() { - return a.src.isMissingNewlineAtEnd() && !b.src.isMissingNewlineAtEnd(); - } - - private void ensureCommentsVisible(CommentDetail comments) { - if (comments.getCommentsA().isEmpty() && comments.getCommentsB().isEmpty()) { - // No comments, no additional dummy edits are required. - // - return; - } - - // Construct empty Edit blocks around each location where a comment is. - // This will force the later packContent method to include the regions - // containing comments, potentially combining those regions together if - // they have overlapping contexts. UI renders will also be able to make - // correct hunks from this, but because the Edit is empty they will not - // style it specially. - // - final List<Edit> empty = new ArrayList<>(); - int lastLine; - - lastLine = -1; - for (Comment c : comments.getCommentsA()) { - final int a = c.lineNbr; - if (lastLine != a) { - final int b = mapA2B(a - 1); - if (0 <= b) { - safeAdd(empty, new Edit(a - 1, b)); - } - lastLine = a; - } - } - - lastLine = -1; - for (Comment c : comments.getCommentsB()) { - int b = c.lineNbr; - if (lastLine != b) { - final int a = mapB2A(b - 1); - if (0 <= a) { - safeAdd(empty, new Edit(a, b - 1)); - } - lastLine = b; - } - } - - // Sort the final list by the index in A, so packContent can combine - // them correctly later. - // - edits.addAll(empty); - edits.sort(EDIT_SORT); - } - - private void safeAdd(List<Edit> empty, Edit toAdd) { - final int a = toAdd.getBeginA(); - final int b = toAdd.getBeginB(); - for (Edit e : edits) { - if (e.getBeginA() <= a && a <= e.getEndA()) { - return; - } - if (e.getBeginB() <= b && b <= e.getEndB()) { - return; - } - } - empty.add(toAdd); - } - - private int mapA2B(int a) { - if (edits.isEmpty()) { - // Magic special case of an unmodified file. - // - return a; - } - - for (int i = 0; i < edits.size(); i++) { - final Edit e = edits.get(i); - if (a < e.getBeginA()) { - if (i == 0) { - // Special case of context at start of file. - // - return a; - } - return e.getBeginB() - (e.getBeginA() - a); - } - if (e.getBeginA() <= a && a <= e.getEndA()) { - return -1; - } - } - - final Edit last = edits.get(edits.size() - 1); - return last.getEndB() + (a - last.getEndA()); - } - - private int mapB2A(int b) { - if (edits.isEmpty()) { - // Magic special case of an unmodified file. - // - return b; - } - - for (int i = 0; i < edits.size(); i++) { - final Edit e = edits.get(i); - if (b < e.getBeginB()) { - if (i == 0) { - // Special case of context at start of file. - // - return b; - } - return e.getBeginA() - (e.getBeginB() - b); - } - if (e.getBeginB() <= b && b <= e.getEndB()) { - return -1; - } - } - - final Edit last = edits.get(edits.size() - 1); - return last.getEndA() + (b - last.getEndB()); - } - - private void packContent(boolean ignoredWhitespace) { - EditList list = new EditList(edits, context, a.size(), b.size()); - for (EditList.Hunk hunk : list.getHunks()) { - while (hunk.next()) { - if (hunk.isContextLine()) { - String lineA = a.getSourceLine(hunk.getCurA()); - a.dst.addLine(hunk.getCurA(), lineA); - - if (ignoredWhitespace) { - // If we ignored whitespace in some form, also get the line - // from b when it does not exactly match the line from a. - // - String lineB = b.getSourceLine(hunk.getCurB()); - if (!lineA.equals(lineB)) { - b.dst.addLine(hunk.getCurB(), lineB); - } - } - hunk.incBoth(); - continue; - } - - if (hunk.isDeletedA()) { - a.addLine(hunk.getCurA()); - hunk.incA(); - } - - if (hunk.isInsertedB()) { - b.addLine(hunk.getCurB()); - hunk.incB(); - } - } + ResolvedSides(PatchSide a, PatchSide b) { + this.a = a; + this.b = b; } } - private class Side { - String path; - ObjectId id; - FileMode mode; - byte[] srcContent; - Text src; - MimeType mimeType = MimeUtil2.UNKNOWN_MIME_TYPE; - DisplayMethod displayMethod = DisplayMethod.DIFF; - PatchScript.FileMode fileMode = PatchScript.FileMode.FILE; - final SparseFileContent dst = new SparseFileContent(); + static class SidesResolver { - int size() { - if (src == null) { - return 0; - } - if (src.isMissingNewlineAtEnd()) { - return src.size(); - } - return src.size() + 1; + private final Repository db; + private final ComparisonType comparisonType; + + SidesResolver(Repository db, ComparisonType comparisonType) { + this.db = db; + this.comparisonType = comparisonType; } - void addLine(int lineNumber) { - String lineContent = getSourceLine(lineNumber); - dst.addLine(lineNumber, lineContent); - } - - String getSourceLine(int lineNumber) { - return lineNumber >= src.size() ? "" : src.getString(lineNumber); - } - - void resolve(Side other, ObjectId within) throws IOException { + PatchSide resolve( + final FileTypeRegistry registry, + final ObjectReader reader, + final String path, + final PatchSide other, + final ObjectId within, + final boolean isWithinEqualsA) + throws IOException { try { - final boolean reuse; - if (Patch.COMMIT_MSG.equals(path)) { - if (comparisonType.isAgainstParentOrAutoMerge() && Objects.equals(aId, within)) { - id = ObjectId.zeroId(); - src = Text.EMPTY; - srcContent = Text.NO_BYTES; + boolean isCommitMsg = Patch.COMMIT_MSG.equals(path); + boolean isMergeList = Patch.MERGE_LIST.equals(path); + if (isCommitMsg || isMergeList) { + if (comparisonType.isAgainstParentOrAutoMerge() && isWithinEqualsA) { + return createSide( + within, + path, + ObjectId.zeroId(), + FileMode.MISSING, + Text.NO_BYTES, + Text.EMPTY, + MimeUtil2.UNKNOWN_MIME_TYPE.toString(), + DisplayMethod.NONE, + false); + } + Text src = + isCommitMsg + ? Text.forCommit(reader, within) + : Text.forMergeList(comparisonType, reader, within); + byte[] srcContent = src.getContent(); + DisplayMethod displayMethod; + FileMode mode; + if (src == Text.EMPTY) { mode = FileMode.MISSING; displayMethod = DisplayMethod.NONE; } else { - id = within; - src = Text.forCommit(reader, within); - srcContent = src.getContent(); - if (src == Text.EMPTY) { - mode = FileMode.MISSING; - displayMethod = DisplayMethod.NONE; - } else { - mode = FileMode.REGULAR_FILE; - } + mode = FileMode.REGULAR_FILE; + displayMethod = DisplayMethod.DIFF; } - reuse = false; - } else if (Patch.MERGE_LIST.equals(path)) { - if (comparisonType.isAgainstParentOrAutoMerge() && Objects.equals(aId, within)) { - id = ObjectId.zeroId(); - src = Text.EMPTY; - srcContent = Text.NO_BYTES; - mode = FileMode.MISSING; - displayMethod = DisplayMethod.NONE; - } else { - id = within; - src = Text.forMergeList(comparisonType, reader, within); - srcContent = src.getContent(); - if (src == Text.EMPTY) { - mode = FileMode.MISSING; - displayMethod = DisplayMethod.NONE; - } else { - mode = FileMode.REGULAR_FILE; - } - } - reuse = false; + return createSide( + within, + path, + within, + mode, + srcContent, + src, + MimeUtil2.UNKNOWN_MIME_TYPE.toString(), + displayMethod, + false); + } + final TreeWalk tw = find(reader, path, within); + ObjectId id = tw != null ? tw.getObjectId(0) : ObjectId.zeroId(); + FileMode mode = tw != null ? tw.getFileMode(0) : FileMode.MISSING; + boolean reuse = + other != null + && other.id.equals(id) + && (other.mode == mode || isBothFile(other.mode, mode)); + Text src = null; + byte[] srcContent; + if (reuse) { + srcContent = other.srcContent; + + } else if (mode.getObjectType() == Constants.OBJ_BLOB) { + srcContent = Text.asByteArray(db.open(id, Constants.OBJ_BLOB)); + + } else if (mode.getObjectType() == Constants.OBJ_COMMIT) { + String strContent = "Subproject commit " + ObjectId.toString(id); + srcContent = strContent.getBytes(UTF_8); + } else { - final TreeWalk tw = find(within); + srcContent = Text.NO_BYTES; + } + String mimeType = MimeUtil2.UNKNOWN_MIME_TYPE.toString(); + DisplayMethod displayMethod = DisplayMethod.DIFF; + if (reuse) { + mimeType = other.mimeType; + displayMethod = other.displayMethod; + src = other.src; - id = tw != null ? tw.getObjectId(0) : ObjectId.zeroId(); - mode = tw != null ? tw.getFileMode(0) : FileMode.MISSING; - reuse = - other != null - && other.id.equals(id) - && (other.mode == mode || isBothFile(other.mode, mode)); - - if (reuse) { - srcContent = other.srcContent; - - } else if (mode.getObjectType() == Constants.OBJ_BLOB) { - srcContent = Text.asByteArray(db.open(id, Constants.OBJ_BLOB)); - - } else if (mode.getObjectType() == Constants.OBJ_COMMIT) { - String strContent = "Subproject commit " + ObjectId.toString(id); - srcContent = strContent.getBytes(UTF_8); - - } else { - srcContent = Text.NO_BYTES; + } else if (srcContent.length > 0 && FileMode.SYMLINK != mode) { + MimeType registryMimeType = registry.getMimeType(path, srcContent); + if ("image".equals(registryMimeType.getMediaType()) + && registry.isSafeInline(registryMimeType)) { + displayMethod = DisplayMethod.IMG; } - - if (reuse) { - mimeType = other.mimeType; - displayMethod = other.displayMethod; - src = other.src; - - } else if (srcContent.length > 0 && FileMode.SYMLINK != mode) { - mimeType = registry.getMimeType(path, srcContent); - if ("image".equals(mimeType.getMediaType()) && registry.isSafeInline(mimeType)) { - displayMethod = DisplayMethod.IMG; - } - } + mimeType = registryMimeType.toString(); } + return createSide(within, path, id, mode, srcContent, src, mimeType, displayMethod, reuse); - if (mode == FileMode.MISSING) { - displayMethod = DisplayMethod.NONE; - } - - if (!reuse) { - if (srcContent == Text.NO_BYTES) { - src = Text.EMPTY; - } else { - src = new Text(srcContent); - } - } - - dst.setSize(size()); - - if (mode == FileMode.SYMLINK) { - fileMode = PatchScript.FileMode.SYMLINK; - } else if (mode == FileMode.GITLINK) { - fileMode = PatchScript.FileMode.GITLINK; - } } catch (IOException err) { throw new IOException("Cannot read " + within.name() + ":" + path, err); } } - private TreeWalk find(ObjectId within) - throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, - IOException { + private PatchSide createSide( + ObjectId treeId, + String path, + ObjectId id, + FileMode mode, + byte[] srcContent, + Text src, + String mimeType, + DisplayMethod displayMethod, + boolean reuse) { + if (!reuse) { + if (srcContent == Text.NO_BYTES) { + src = Text.EMPTY; + } else { + src = new Text(srcContent); + } + } + if (mode == FileMode.MISSING) { + displayMethod = DisplayMethod.NONE; + } + PatchScript.FileMode fileMode = PatchScript.FileMode.FILE; + if (mode == FileMode.SYMLINK) { + fileMode = PatchScript.FileMode.SYMLINK; + } else if (mode == FileMode.GITLINK) { + fileMode = PatchScript.FileMode.GITLINK; + } + return new PatchSide( + treeId, path, id, mode, srcContent, src, mimeType, displayMethod, fileMode); + } + + private TreeWalk find(ObjectReader reader, String path, ObjectId within) throws IOException { if (path == null || within == null) { return null; } @@ -608,4 +447,97 @@ return (a.getBits() & FileMode.TYPE_FILE) == FileMode.TYPE_FILE && (b.getBits() & FileMode.TYPE_FILE) == FileMode.TYPE_FILE; } + + static class IntraLineDiffCalculatorResult { + // Not an @AutoValue because Edit is mutable + final boolean failure; + final boolean timeout; + private final Optional<ImmutableList<Edit>> edits; + + private IntraLineDiffCalculatorResult( + Optional<ImmutableList<Edit>> edits, boolean failure, boolean timeout) { + this.failure = failure; + this.timeout = timeout; + this.edits = edits; + } + + static final IntraLineDiffCalculatorResult NO_RESULT = + new IntraLineDiffCalculatorResult(Optional.empty(), false, false); + static final IntraLineDiffCalculatorResult FAILURE = + new IntraLineDiffCalculatorResult(Optional.empty(), true, false); + static final IntraLineDiffCalculatorResult TIMEOUT = + new IntraLineDiffCalculatorResult(Optional.empty(), false, true); + + static IntraLineDiffCalculatorResult success(ImmutableList<Edit> edits) { + return new IntraLineDiffCalculatorResult(Optional.of(edits), false, false); + } + } + + interface IntraLineDiffCalculator { + + IntraLineDiffCalculatorResult calculateIntraLineDiff( + ImmutableList<Edit> edits, + Set<Edit> editsDueToRebase, + ObjectId aId, + ObjectId bId, + Text aSrc, + Text bSrc, + ObjectId bTreeId, + String bPath); + } + + static class PatchFileChange { + private final ImmutableList<Edit> edits; + private final ImmutableSet<Edit> editsDueToRebase; + private final ImmutableList<String> headerLines; + private final String oldName; + private final String newName; + private final ChangeType changeType; + private final Patch.PatchType patchType; + + public PatchFileChange( + ImmutableList<Edit> edits, + ImmutableSet<Edit> editsDueToRebase, + ImmutableList<String> headerLines, + String oldName, + String newName, + ChangeType changeType, + Patch.PatchType patchType) { + this.edits = edits; + this.editsDueToRebase = editsDueToRebase; + this.headerLines = headerLines; + this.oldName = oldName; + this.newName = newName; + this.changeType = changeType; + this.patchType = patchType; + } + + ImmutableList<Edit> getEdits() { + return edits; + } + + ImmutableSet<Edit> getEditsDueToRebase() { + return editsDueToRebase; + } + + ImmutableList<String> getHeaderLines() { + return headerLines; + } + + String getNewName() { + return newName; + } + + String getOldName() { + return oldName; + } + + ChangeType getChangeType() { + return changeType; + } + + Patch.PatchType getPatchType() { + return patchType; + } + } }
diff --git a/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/java/com/google/gerrit/server/patch/PatchScriptFactory.java index ffeda3d..afc1ab1 100644 --- a/java/com/google/gerrit/server/patch/PatchScriptFactory.java +++ b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -17,6 +17,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; +import com.google.common.collect.ImmutableList; import com.google.common.flogger.FluentLogger; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.CommentDetail; @@ -27,6 +28,7 @@ import com.google.gerrit.entities.Patch; import com.google.gerrit.entities.Patch.ChangeType; import com.google.gerrit.entities.PatchSet; +import com.google.gerrit.entities.Project; import com.google.gerrit.extensions.client.DiffPreferencesInfo; import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace; import com.google.gerrit.extensions.restapi.AuthException; @@ -38,6 +40,7 @@ import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.LargeObjectException; import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.patch.PatchScriptBuilder.IntraLineDiffCalculatorResult; import com.google.gerrit.server.permissions.ChangePermission; import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.permissions.PermissionBackendException; @@ -48,20 +51,22 @@ import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; import java.io.IOException; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.Callable; +import org.eclipse.jgit.diff.Edit; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; public class PatchScriptFactory implements Callable<PatchScript> { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); public interface Factory { + PatchScriptFactory create( ChangeNotes notes, String fileName, @@ -92,17 +97,12 @@ private final Provider<CurrentUser> userProvider; private final PermissionBackend permissionBackend; private final ProjectCache projectCache; - private Optional<ChangeEdit> edit; private final Change.Id changeId; private boolean loadHistory = true; private boolean loadComments = true; private ChangeNotes notes; - private ObjectId aId; - private ObjectId bId; - private List<Patch> history; - private CommentDetail comments; @AssistedInject PatchScriptFactory( @@ -189,23 +189,6 @@ public PatchScript call() throws LargeObjectException, AuthException, InvalidChangeOperationException, IOException, PermissionBackendException { - validatePatchSetId(psa); - validatePatchSetId(psb); - - if (psa != null) { - checkState(parentNum < 0, "expected no parentNum when psa is present"); - checkArgument(psa.get() != 0, "edit not supported for left side"); - aId = getCommitId(psa); - } else { - aId = null; - } - - if (psb.get() != 0) { - bId = getCommitId(psb); - } else { - // Change edit: create synthetic PatchSet corresponding to the edit. - bId = getEditRev(); - } try { permissionBackend.currentUser().change(notes).check(ChangePermission.READ); @@ -219,13 +202,31 @@ try (Repository git = repoManager.openRepository(notes.getProjectName())) { try { - final PatchList list = listFor(keyFor(diffPrefs.ignoreWhitespace)); - final PatchScriptBuilder b = newBuilder(list, git); + validatePatchSetId(psa); + validatePatchSetId(psb); + + ObjectId aId = getAId().orElse(null); + ObjectId bId = getBId().orElse(null); + boolean changeEdit = false; + if (bId == null) { + // Change edit: create synthetic PatchSet corresponding to the edit. + Optional<ChangeEdit> edit = editReader.byChange(notes); + if (!edit.isPresent()) { + throw new NoSuchChangeException(notes.getChangeId()); + } + bId = edit.get().getEditCommit(); + changeEdit = true; + } + + final PatchList list = listFor(keyFor(aId, bId, diffPrefs.ignoreWhitespace)); + final PatchScriptBuilder b = newBuilder(); final PatchListEntry content = list.get(fileName); - loadCommentsAndHistory(content.getChangeType(), content.getOldName(), content.getNewName()); + Optional<ImmutableList<Patch>> history = loadHistory(content, changeEdit); + Optional<CommentDetail> comments = + loadComments(content, changeEdit, history.orElse(ImmutableList.of())); - return b.toPatchScript(content, comments, history); + return b.toPatchScript(git, list, content, comments.orElse(null), history.orElse(null)); } catch (PatchListNotAvailableException e) { throw new NoSuchChangeException(changeId, e); } catch (IOException e) { @@ -243,7 +244,46 @@ } } - private PatchListKey keyFor(Whitespace whitespace) { + private Optional<CommentDetail> loadComments( + PatchListEntry content, boolean changeEdit, ImmutableList<Patch> history) { + if (!loadComments) { + return Optional.empty(); + } + return new CommentsLoader(psa, psb, userProvider, notes, commentsUtil) + .load( + changeEdit, + content.getChangeType(), + content.getOldName(), + content.getNewName(), + history); + } + + private Optional<ImmutableList<Patch>> loadHistory(PatchListEntry content, boolean changeEdit) { + if (!loadHistory) { + return Optional.empty(); + } + HistoryLoader loader = new HistoryLoader(psa, psb, psUtil, notes, fileName); + return Optional.of(loader.load(changeEdit, content.getChangeType(), content.getOldName())); + } + + private Optional<ObjectId> getAId() { + if (psa == null) { + return Optional.empty(); + } + checkState(parentNum < 0, "expected no parentNum when psa is present"); + checkArgument(psa.get() != 0, "edit not supported for left side"); + return Optional.of(getCommitId(psa)); + } + + private Optional<ObjectId> getBId() { + if (psb.get() == 0) { + // Change edit + return Optional.empty(); + } + return Optional.of(getCommitId(psb)); + } + + private PatchListKey keyFor(ObjectId aId, ObjectId bId, Whitespace whitespace) { if (parentNum < 0) { return PatchListKey.againstCommit(aId, bId, whitespace); } @@ -254,12 +294,14 @@ return patchListCache.get(key, notes.getProjectName()); } - private PatchScriptBuilder newBuilder(PatchList list, Repository git) { + private PatchScriptBuilder newBuilder() { final PatchScriptBuilder b = builderFactory.get(); - b.setRepository(git, notes.getProjectName()); b.setChange(notes.getChange()); b.setDiffPrefs(diffPrefs); - b.setTrees(list.getComparisonType(), list.getOldId(), list.getNewId()); + if (diffPrefs.intralineDifference) { + b.setIntraLineDiffCalculator( + new IntraLineDiffCalculator(patchListCache, notes.getProjectName(), diffPrefs)); + } return b; } @@ -271,14 +313,6 @@ return ps.commitId(); } - private ObjectId getEditRev() throws AuthException, IOException { - edit = editReader.byChange(notes); - if (edit.isPresent()) { - return edit.get().getEditCommit(); - } - throw new NoSuchChangeException(notes.getChangeId()); - } - private void validatePatchSetId(PatchSet.Id psId) throws NoSuchChangeException { if (psId == null) { // OK, means use base; } else if (changeId.equals(psId.changeId())) { // OK, same change; @@ -287,16 +321,29 @@ } } - private void loadCommentsAndHistory(ChangeType changeType, String oldName, String newName) { - Map<Patch.Key, Patch> byKey = new HashMap<>(); + private static class HistoryLoader { + private final PatchSet.Id psa; + private final PatchSet.Id psb; + private final PatchSetUtil psUtil; + private final ChangeNotes notes; + private final String fileName; - if (loadHistory) { + HistoryLoader( + PatchSet.Id psa, PatchSet.Id psb, PatchSetUtil psUtil, ChangeNotes notes, String fileName) { + this.psa = psa; + this.psb = psb; + this.psUtil = psUtil; + this.notes = notes; + this.fileName = fileName; + } + + private ImmutableList<Patch> load(boolean changeEdit, ChangeType changeType, String oldName) { // This seems like a cheap trick. It doesn't properly account for a // file that gets renamed between patch set 1 and patch set 2. We // will wind up packing the wrong Patch object because we didn't do // proper rename detection between the patch sets. // - history = new ArrayList<>(); + ImmutableList.Builder<Patch> historyBuilder = ImmutableList.builder(); for (PatchSet ps : psUtil.byChange(notes)) { String name = fileName; if (psa != null) { @@ -317,17 +364,51 @@ } Patch p = new Patch(Patch.key(ps.id(), name)); - history.add(p); - byKey.put(p.getKey(), p); + historyBuilder.add(p); } - if (edit != null && edit.isPresent()) { + if (changeEdit) { Patch p = new Patch(Patch.key(PatchSet.id(psb.changeId(), 0), fileName)); - history.add(p); - byKey.put(p.getKey(), p); + historyBuilder.add(p); } + return historyBuilder.build(); + } + } + + private static class CommentsLoader { + private final PatchSet.Id psa; + private final PatchSet.Id psb; + private final Provider<CurrentUser> userProvider; + private final ChangeNotes notes; + private final CommentsUtil commentsUtil; + private CommentDetail comments; + + CommentsLoader( + PatchSet.Id psa, + PatchSet.Id psb, + Provider<CurrentUser> userProvider, + ChangeNotes notes, + CommentsUtil commentsUtil) { + this.psa = psa; + this.psb = psb; + this.userProvider = userProvider; + this.notes = notes; + this.commentsUtil = commentsUtil; } - if (loadComments && edit == null) { + private Optional<CommentDetail> load( + boolean changeEdit, + ChangeType changeType, + String oldName, + String newName, + ImmutableList<Patch> history) { + // TODO: Implement this method with CommentDetailBuilder (this class doesn't exists yet). + // This is a legacy code which create final object and populate it and then returns it. + if (changeEdit) { + return Optional.empty(); + } + Map<Patch.Key, Patch> byKey = new HashMap<>(); + history.forEach(p -> byKey.put(p.getKey(), p)); + comments = new CommentDetail(psa, psb); switch (changeType) { case ADDED: @@ -376,29 +457,79 @@ break; } } + return Optional.of(comments); } - } - private void loadPublished(Map<Patch.Key, Patch> byKey, String file) { - for (Comment c : commentsUtil.publishedByChangeFile(notes, file)) { - comments.include(notes.getChangeId(), c); - PatchSet.Id psId = PatchSet.id(notes.getChangeId(), c.key.patchSetId); - Patch.Key pKey = Patch.key(psId, c.key.filename); - Patch p = byKey.get(pKey); - if (p != null) { - p.setCommentCount(p.getCommentCount() + 1); + private void loadPublished(Map<Patch.Key, Patch> byKey, String file) { + for (Comment c : commentsUtil.publishedByChangeFile(notes, file)) { + comments.include(notes.getChangeId(), c); + PatchSet.Id psId = PatchSet.id(notes.getChangeId(), c.key.patchSetId); + Patch.Key pKey = Patch.key(psId, c.key.filename); + Patch p = byKey.get(pKey); + if (p != null) { + p.setCommentCount(p.getCommentCount() + 1); + } + } + } + + private void loadDrafts(Map<Patch.Key, Patch> byKey, Account.Id me, String file) { + for (Comment c : commentsUtil.draftByChangeFileAuthor(notes, file, me)) { + comments.include(notes.getChangeId(), c); + PatchSet.Id psId = PatchSet.id(notes.getChangeId(), c.key.patchSetId); + Patch.Key pKey = Patch.key(psId, c.key.filename); + Patch p = byKey.get(pKey); + if (p != null) { + p.setDraftCount(p.getDraftCount() + 1); + } } } } - private void loadDrafts(Map<Patch.Key, Patch> byKey, Account.Id me, String file) { - for (Comment c : commentsUtil.draftByChangeFileAuthor(notes, file, me)) { - comments.include(notes.getChangeId(), c); - PatchSet.Id psId = PatchSet.id(notes.getChangeId(), c.key.patchSetId); - Patch.Key pKey = Patch.key(psId, c.key.filename); - Patch p = byKey.get(pKey); - if (p != null) { - p.setDraftCount(p.getDraftCount() + 1); + private static class IntraLineDiffCalculator + implements PatchScriptBuilder.IntraLineDiffCalculator { + + private final PatchListCache patchListCache; + private final Project.NameKey projectKey; + private final DiffPreferencesInfo diffPrefs; + + IntraLineDiffCalculator( + PatchListCache patchListCache, Project.NameKey projectKey, DiffPreferencesInfo diffPrefs) { + this.patchListCache = patchListCache; + this.projectKey = projectKey; + this.diffPrefs = diffPrefs; + } + + @Override + public IntraLineDiffCalculatorResult calculateIntraLineDiff( + ImmutableList<Edit> edits, + Set<Edit> editsDueToRebase, + ObjectId aId, + ObjectId bId, + Text aSrc, + Text bSrc, + ObjectId bTreeId, + String bPath) { + IntraLineDiff d = + patchListCache.getIntraLineDiff( + IntraLineDiffKey.create(aId, bId, diffPrefs.ignoreWhitespace), + IntraLineDiffArgs.create( + aSrc, bSrc, edits, editsDueToRebase, projectKey, bTreeId, bPath)); + if (d == null) { + return IntraLineDiffCalculatorResult.FAILURE; + } + switch (d.getStatus()) { + case EDIT_LIST: + return IntraLineDiffCalculatorResult.success(d.getEdits()); + + case ERROR: + return IntraLineDiffCalculatorResult.FAILURE; + + case TIMEOUT: + return IntraLineDiffCalculatorResult.TIMEOUT; + + case DISABLED: + default: + return IntraLineDiffCalculatorResult.NO_RESULT; } } }
diff --git a/java/com/google/gerrit/server/patch/PatchScriptFactoryForAutoFix.java b/java/com/google/gerrit/server/patch/PatchScriptFactoryForAutoFix.java new file mode 100644 index 0000000..cf07190 --- /dev/null +++ b/java/com/google/gerrit/server/patch/PatchScriptFactoryForAutoFix.java
@@ -0,0 +1,134 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.patch; + +import static com.google.common.base.Preconditions.checkState; + +import com.google.common.collect.ImmutableList; +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.common.data.PatchScript; +import com.google.gerrit.entities.Change; +import com.google.gerrit.entities.FixReplacement; +import com.google.gerrit.entities.PatchSet; +import com.google.gerrit.extensions.client.DiffPreferencesInfo; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.server.git.LargeObjectException; +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.InvalidChangeOperationException; +import com.google.gerrit.server.project.NoSuchChangeException; +import com.google.gerrit.server.project.ProjectCache; +import com.google.inject.Provider; +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; +import java.io.IOException; +import java.util.concurrent.Callable; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; + +public class PatchScriptFactoryForAutoFix implements Callable<PatchScript> { + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + public interface Factory { + + PatchScriptFactoryForAutoFix create( + Repository git, + ChangeNotes notes, + String fileName, + PatchSet patchSet, + ImmutableList<FixReplacement> fixReplacements, + DiffPreferencesInfo diffPrefs); + } + + private final PermissionBackend permissionBackend; + private final ProjectCache projectCache; + private final Change.Id changeId; + private final ChangeNotes notes; + private final Provider<PatchScriptBuilder> builderFactory; + private final Repository git; + private final PatchSet patchSet; + private final String fileName; + private final DiffPreferencesInfo diffPrefs; + private final ImmutableList<FixReplacement> fixReplacements; + + @AssistedInject + PatchScriptFactoryForAutoFix( + Provider<PatchScriptBuilder> builderFactory, + PermissionBackend permissionBackend, + ProjectCache projectCache, + @Assisted Repository git, + @Assisted ChangeNotes notes, + @Assisted String fileName, + @Assisted PatchSet patchSet, + @Assisted ImmutableList<FixReplacement> fixReplacements, + @Assisted DiffPreferencesInfo diffPrefs) { + this.notes = notes; + this.permissionBackend = permissionBackend; + this.projectCache = projectCache; + this.changeId = patchSet.id().changeId(); + this.git = git; + this.patchSet = patchSet; + this.fileName = fileName; + this.fixReplacements = fixReplacements; + this.builderFactory = builderFactory; + this.diffPrefs = diffPrefs; + } + + @Override + public PatchScript call() + throws LargeObjectException, AuthException, InvalidChangeOperationException, IOException, + PermissionBackendException { + + try { + permissionBackend.currentUser().change(notes).check(ChangePermission.READ); + } catch (AuthException e) { + throw new NoSuchChangeException(changeId); + } + + if (!projectCache.checkedGet(notes.getProjectName()).statePermitsRead()) { + throw new NoSuchChangeException(changeId); + } + + return createPatchScript(); + } + + private PatchScript createPatchScript() throws LargeObjectException { + checkState(patchSet.id().get() != 0, "edit not supported for left side"); + PatchScriptBuilder b = newBuilder(); + try { + ObjectId baseId = patchSet.commitId(); + return b.toPatchScript(git, baseId, fileName, fixReplacements); + } catch (ResourceConflictException e) { + logger.atSevere().withCause(e).log("AutoFix replacements is not valid"); + throw new IllegalStateException("AutoFix replacements is not valid", e); + } catch (IOException e) { + logger.atSevere().withCause(e).log("File content unavailable"); + throw new NoSuchChangeException(notes.getChangeId(), e); + } catch (org.eclipse.jgit.errors.LargeObjectException err) { + throw new LargeObjectException("File content is too large", err); + } + } + + private PatchScriptBuilder newBuilder() { + PatchScriptBuilder b = builderFactory.get(); + b.setChange(notes.getChange()); + b.setDiffPrefs(diffPrefs); + return b; + } +}
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java index 07cb50d..253f50c 100644 --- a/java/com/google/gerrit/server/permissions/ChangeControl.java +++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -89,7 +89,7 @@ } /** Can this user see this change? */ - private boolean isVisible(@Nullable ChangeData cd) { + private boolean isVisible(ChangeData cd) { if (getChange().isPrivate() && !isPrivateVisible(cd)) { return false; } @@ -144,7 +144,7 @@ /** Is this user assigned to this change? */ private boolean isAssignee() { - Account.Id currentAssignee = notes.getChange().getAssignee(); + Account.Id currentAssignee = getChange().getAssignee(); if (currentAssignee != null && getUser().isIdentifiedUser()) { Account.Id id = getUser().getAccountId(); return id.equals(currentAssignee); @@ -153,9 +153,8 @@ } /** Is this user a reviewer for the change? */ - private boolean isReviewer(@Nullable ChangeData cd) { + private boolean isReviewer(ChangeData cd) { if (getUser().isIdentifiedUser()) { - cd = cd != null ? cd : changeDataFactory.create(notes); Collection<Account.Id> results = cd.reviewers().all(); return results.contains(getUser().getAccountId()); }
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java index 16bbdaf..47a48b7 100644 --- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java +++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -20,11 +20,10 @@ import static com.google.gerrit.entities.RefNames.REFS_CONFIG; import static com.google.gerrit.entities.RefNames.REFS_USERS_SELF; import static java.util.Objects.requireNonNull; -import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toCollection; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; @@ -60,6 +59,7 @@ import com.google.inject.assistedinject.Assisted; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -129,7 +129,7 @@ } /** Filters given refs and tags by visibility. */ - Map<String, Ref> filter(Map<String, Ref> refs, Repository repo, RefFilterOptions opts) + Collection<Ref> filter(Collection<Ref> refs, Repository repo, RefFilterOptions opts) throws PermissionBackendException { logger.atFinest().log( "Filter refs for repository %s by visibility (options = %s, refs = %s)", @@ -145,10 +145,10 @@ // See if we can get away with a single, cheap ref evaluation. if (refs.size() == 1) { - String refName = Iterables.getOnlyElement(refs.values()).getName(); + String refName = Iterables.getOnlyElement(refs).getName(); if (opts.filterMeta() && isMetadata(refName)) { logger.atFinest().log("Filter out metadata ref %s", refName); - return ImmutableMap.of(); + return ImmutableList.of(); } if (RefNames.isRefsChanges(refName)) { boolean isChangeRefVisisble = canSeeSingleChangeRef(refName); @@ -157,18 +157,18 @@ return refs; } logger.atFinest().log("Filter out non-visible change ref %s", refName); - return ImmutableMap.of(); + return ImmutableList.of(); } } // Perform an initial ref filtering with all the refs the caller asked for. If we find tags that // we have to investigate separately (deferred tags) then perform a reachability check starting // from all visible branches (refs/heads/*). - Result initialRefFilter = filterRefs(refs, repo, opts); - Map<String, Ref> visibleRefs = initialRefFilter.visibleRefs(); + Result initialRefFilter = filterRefs(new ArrayList<>(refs), repo, opts); + List<Ref> visibleRefs = initialRefFilter.visibleRefs(); if (!initialRefFilter.deferredTags().isEmpty()) { try (TraceTimer traceTimer = TraceContext.newTimer("Check visibility of deferred tags")) { - Result allVisibleBranches = filterRefs(getTaggableRefsMap(repo), repo, opts); + Result allVisibleBranches = filterRefs(getTaggableRefs(repo), repo, opts); checkState( allVisibleBranches.deferredTags().isEmpty(), "unexpected tags found when filtering refs/heads/* " @@ -177,12 +177,12 @@ TagMatcher tags = tagCache .get(projectState.getNameKey()) - .matcher(tagCache, repo, allVisibleBranches.visibleRefs().values()); + .matcher(tagCache, repo, allVisibleBranches.visibleRefs()); for (Ref tag : initialRefFilter.deferredTags()) { try { if (tags.isReachable(tag)) { logger.atFinest().log("Include reachable tag %s", tag.getName()); - visibleRefs.put(tag.getName(), tag); + visibleRefs.add(tag); } else { logger.atFinest().log("Filter out non-reachable tag %s", tag.getName()); } @@ -202,7 +202,7 @@ * separately for later rev-walk-based visibility computation. Tags where visibility is trivial to * compute will be returned as part of {@link Result#visibleRefs()}. */ - Result filterRefs(Map<String, Ref> refs, Repository repo, RefFilterOptions opts) + Result filterRefs(List<Ref> refs, Repository repo, RefFilterOptions opts) throws PermissionBackendException { logger.atFinest().log("Filter refs (refs = %s)", refs); @@ -252,9 +252,9 @@ identifiedUser = null; } - Map<String, Ref> resultRefs = new HashMap<>(); + List<Ref> resultRefs = new ArrayList<>(refs.size()); List<Ref> deferredTags = new ArrayList<>(); - for (Ref ref : refs.values()) { + for (Ref ref : refs) { String name = ref.getName(); Change.Id changeId; Account.Id accountId; @@ -268,7 +268,7 @@ // Edits are visible only to the owning user, if change is visible. if (viewMetadata || visibleEdit(repo, name)) { logger.atFinest().log("Include edit ref %s", name); - resultRefs.put(name, ref); + resultRefs.add(ref); } else { logger.atFinest().log("Filter out edit ref %s", name); } @@ -276,7 +276,7 @@ // Change ref is visible only if the change is visible. if (viewMetadata || visible(repo, changeId)) { logger.atFinest().log("Include change ref %s", name); - resultRefs.put(name, ref); + resultRefs.add(ref); } else { logger.atFinest().log("Filter out change ref %s", name); } @@ -284,7 +284,7 @@ // Account ref is visible only to the corresponding account. if (viewMetadata || (accountId.equals(userId) && canReadRef(name))) { logger.atFinest().log("Include user ref %s", name); - resultRefs.put(name, ref); + resultRefs.add(ref); } else { logger.atFinest().log("Filter out user ref %s", name); } @@ -296,7 +296,7 @@ && isGroupOwner(group, identifiedUser, isAdmin) && canReadRef(name))) { logger.atFinest().log("Include group ref %s", name); - resultRefs.put(name, ref); + resultRefs.add(ref); } else { logger.atFinest().log("Filter out group ref %s", name); } @@ -312,7 +312,7 @@ // the regular Git tree that users interact with, not on any of the Gerrit trees, so this // is a negligible risk. logger.atFinest().log("Include tag ref %s because user has read on refs/*", name); - resultRefs.put(name, ref); + resultRefs.add(ref); } else { // If its a tag, consider it later. if (ref.getObjectId() != null) { @@ -326,7 +326,7 @@ // Sequences are internal database implementation details. if (viewMetadata) { logger.atFinest().log("Include sequence ref %s", name); - resultRefs.put(name, ref); + resultRefs.add(ref); } else { logger.atFinest().log("Filter out sequence ref %s", name); } @@ -336,7 +336,7 @@ // users. if (viewMetadata) { logger.atFinest().log("Include external IDs branch %s", name); - resultRefs.put(name, ref); + resultRefs.add(ref); } else { logger.atFinest().log("Filter out external IDs branch %s", name); } @@ -346,13 +346,13 @@ // not symbolic then getLeaf() is a no-op returning ref itself. logger.atFinest().log( "Include ref %s because its leaf %s is readable", name, ref.getLeaf().getName()); - resultRefs.put(name, ref); + resultRefs.add(ref); } else if (isRefsUsersSelf(ref)) { // viewMetadata allows to see all account refs, hence refs/users/self should be included as // well if (viewMetadata) { logger.atFinest().log("Include ref %s", REFS_USERS_SELF); - resultRefs.put(name, ref); + resultRefs.add(ref); } } else { logger.atFinest().log("Filter out ref %s", name); @@ -370,38 +370,39 @@ * <p>We exclude symbolic refs because their target will be included and this will suffice for * computing reachability. */ - private static Map<String, Ref> getTaggableRefsMap(Repository repo) - throws PermissionBackendException { + private static List<Ref> getTaggableRefs(Repository repo) throws PermissionBackendException { try { - return repo.getRefDatabase().getRefs().stream() + List<Ref> allRefs = repo.getRefDatabase().getRefs(); + return allRefs.stream() .filter( r -> !RefNames.isGerritRef(r.getName()) && !r.getName().startsWith(RefNames.REFS_TAGS) && !r.isSymbolic()) - .collect(toMap(Ref::getName, r -> r)); + // Don't use the default Java Collections.toList() as that is not size-aware and would + // expand an array list as new elements are added. Instead, provide a list that has the + // right size. This spares incremental list expansion which is quadratic in complexity. + .collect(toCollection(() -> new ArrayList<>(allRefs.size()))); } catch (IOException e) { throw new PermissionBackendException(e); } } - private Map<String, Ref> fastHideRefsMetaConfig(Map<String, Ref> refs) - throws PermissionBackendException { - if (refs.containsKey(REFS_CONFIG) && !canReadRef(REFS_CONFIG)) { - Map<String, Ref> r = new HashMap<>(refs); - r.remove(REFS_CONFIG); - return r; + private List<Ref> fastHideRefsMetaConfig(List<Ref> refs) throws PermissionBackendException { + if (!canReadRef(REFS_CONFIG)) { + return refs.stream() + .filter(r -> !r.getName().equals(REFS_CONFIG)) + // Don't use the default Java Collections.toList() as that is not size-aware and would + // expand an array list as new elements are added. Instead, provide a list that has the + // right size. This spares incremental list expansion which is quadratic in complexity. + .collect(toCollection(() -> new ArrayList<>(refs.size()))); } return refs; } - private Map<String, Ref> addUsersSelfSymref(Repository repo, Map<String, Ref> refs) + private List<Ref> addUsersSelfSymref(Repository repo, List<Ref> refs) throws PermissionBackendException { if (user.isIdentifiedUser()) { - // User self symref is already there - if (refs.containsKey(REFS_USERS_SELF)) { - return refs; - } String refName = RefNames.refsUsers(user.getAccountId()); try { Ref r = repo.exactRef(refName); @@ -411,8 +412,8 @@ } SymbolicRef s = new SymbolicRef(REFS_USERS_SELF, r); - refs = new HashMap<>(refs); - refs.put(s.getName(), s); + refs = new ArrayList<>(refs); + refs.add(s); logger.atFinest().log("Added %s as alias for user ref %s", REFS_USERS_SELF, refName); } catch (IOException e) { throw new PermissionBackendException(e); @@ -614,7 +615,7 @@ @AutoValue abstract static class Result { /** Subset of the refs passed into the computation that is visible to the user. */ - abstract Map<String, Ref> visibleRefs(); + abstract List<Ref> visibleRefs(); /** * List of tags where we couldn't figure out visibility in the first pass and need to do an
diff --git a/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java b/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java index 0800d6b..2344781 100644 --- a/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java +++ b/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
@@ -26,7 +26,6 @@ import com.google.gerrit.server.permissions.PermissionBackend.WithUser; import com.google.gerrit.server.query.change.ChangeData; import java.util.Collection; -import java.util.Map; import java.util.Set; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; @@ -142,7 +141,7 @@ } @Override - public Map<String, Ref> filter(Map<String, Ref> refs, Repository repo, RefFilterOptions opts) + public Collection<Ref> filter(Collection<Ref> refs, Repository repo, RefFilterOptions opts) throws PermissionBackendException { throw new PermissionBackendException(message, cause); }
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackend.java b/java/com/google/gerrit/server/permissions/PermissionBackend.java index 9149a1d..d831ab6 100644 --- a/java/com/google/gerrit/server/permissions/PermissionBackend.java +++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -15,7 +15,6 @@ package com.google.gerrit.server.permissions; import static java.util.Objects.requireNonNull; -import static java.util.stream.Collectors.toMap; import static java.util.stream.Collectors.toSet; import com.google.auto.value.AutoValue; @@ -41,7 +40,6 @@ import java.util.EnumSet; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Set; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.Ref; @@ -329,33 +327,18 @@ public abstract BooleanCondition testCond(CoreOrPluginProjectPermission perm); /** - * Filter a map of references by visibility. - * - * @param refs a map of references to filter. - * @param repo an open {@link Repository} handle for this instance's project - * @param opts further options for filtering. - * @return a partition of the provided refs that are visible to the user that this instance is - * scoped to. - * @throws PermissionBackendException if failure consulting backend configuration. - */ - public abstract Map<String, Ref> filter( - Map<String, Ref> refs, Repository repo, RefFilterOptions opts) - throws PermissionBackendException; - - /** * Filter a list of references by visibility. * - * @param refs a list of references to filter. + * @param refs a collection of references to filter. * @param repo an open {@link Repository} handle for this instance's project * @param opts further options for filtering. * @return a partition of the provided refs that are visible to the user that this instance is * scoped to. * @throws PermissionBackendException if failure consulting backend configuration. */ - public Map<String, Ref> filter(List<Ref> refs, Repository repo, RefFilterOptions opts) - throws PermissionBackendException { - return filter(refs.stream().collect(toMap(Ref::getName, r -> r, (a, b) -> b)), repo, opts); - } + public abstract Collection<Ref> filter( + Collection<Ref> refs, Repository repo, RefFilterOptions opts) + throws PermissionBackendException; } /** Options for filtering refs using {@link ForProject}. */
diff --git a/java/com/google/gerrit/server/permissions/ProjectControl.java b/java/com/google/gerrit/server/permissions/ProjectControl.java index cc3b666..145e0b6 100644 --- a/java/com/google/gerrit/server/permissions/ProjectControl.java +++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -404,7 +404,7 @@ } @Override - public Map<String, Ref> filter(Map<String, Ref> refs, Repository repo, RefFilterOptions opts) + public Collection<Ref> filter(Collection<Ref> refs, Repository repo, RefFilterOptions opts) throws PermissionBackendException { if (refFilter == null) { refFilter = refFilterFactory.create(ProjectControl.this);
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java index 06fe471..378a512 100644 --- a/java/com/google/gerrit/server/permissions/RefControl.java +++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -440,8 +440,7 @@ @Override public ForChange change(ChangeData cd) { try { - // TODO(hiesel) Force callers to call database() and use db instead of cd.db() - return getProjectControl().controlFor(cd.change()).asForChange(cd); + return getProjectControl().controlFor(cd.notes()).asForChange(cd); } catch (StorageException e) { return FailedPermissionBackend.change("unavailable", e); }
diff --git a/java/com/google/gerrit/server/project/DefaultProjectNameLockManager.java b/java/com/google/gerrit/server/project/DefaultProjectNameLockManager.java index 000fb09..ab347e5 100644 --- a/java/com/google/gerrit/server/project/DefaultProjectNameLockManager.java +++ b/java/com/google/gerrit/server/project/DefaultProjectNameLockManager.java
@@ -21,6 +21,7 @@ import com.google.inject.Singleton; import java.util.concurrent.locks.Lock; +/** In-memory lock for project names. */ @Singleton public class DefaultProjectNameLockManager implements ProjectNameLockManager {
diff --git a/java/com/google/gerrit/server/project/FileResource.java b/java/com/google/gerrit/server/project/FileResource.java index 6e5375a..e8926dc 100644 --- a/java/com/google/gerrit/server/project/FileResource.java +++ b/java/com/google/gerrit/server/project/FileResource.java
@@ -27,6 +27,11 @@ import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; +/** + * A file. + * + * <p>This is in the project package because it is accessed through the project/branch/file path. + */ public class FileResource implements RestResource { public static final TypeLiteral<RestView<FileResource>> FILE_KIND = new TypeLiteral<RestView<FileResource>>() {};
diff --git a/java/com/google/gerrit/server/project/GroupList.java b/java/com/google/gerrit/server/project/GroupList.java index fe59012..ba7dc95 100644 --- a/java/com/google/gerrit/server/project/GroupList.java +++ b/java/com/google/gerrit/server/project/GroupList.java
@@ -28,6 +28,12 @@ import java.util.Map; import java.util.Set; +/** + * File format for group name aliases. + * + * <p>Project configuration must use aliases for groups used in the permission section. The + * aliases/group mapping is stored in a file "groups", (de)serialized with this class. + */ public class GroupList extends TabFile { private static final FluentLogger logger = FluentLogger.forEnclosingClass();
diff --git a/java/com/google/gerrit/server/project/LabelDefinitionJson.java b/java/com/google/gerrit/server/project/LabelDefinitionJson.java new file mode 100644 index 0000000..2ecd8c2 --- /dev/null +++ b/java/com/google/gerrit/server/project/LabelDefinitionJson.java
@@ -0,0 +1,53 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.project; + +import static java.util.stream.Collectors.toMap; + +import com.google.gerrit.common.data.LabelType; +import com.google.gerrit.common.data.LabelValue; +import com.google.gerrit.entities.Project; +import com.google.gerrit.extensions.common.LabelDefinitionInfo; + +public class LabelDefinitionJson { + public static LabelDefinitionInfo format(Project.NameKey projectName, LabelType labelType) { + LabelDefinitionInfo label = new LabelDefinitionInfo(); + label.name = labelType.getName(); + label.projectName = projectName.get(); + label.function = labelType.getFunction().getFunctionName(); + label.values = + labelType.getValues().stream().collect(toMap(LabelValue::formatValue, LabelValue::getText)); + label.defaultValue = labelType.getDefaultValue(); + label.branches = labelType.getRefPatterns() != null ? labelType.getRefPatterns() : null; + label.canOverride = toBoolean(labelType.canOverride()); + label.copyAnyScore = toBoolean(labelType.isCopyAnyScore()); + label.copyMinScore = toBoolean(labelType.isCopyMinScore()); + label.copyMaxScore = toBoolean(labelType.isCopyMaxScore()); + label.copyAllScoresIfNoChange = toBoolean(labelType.isCopyAllScoresIfNoChange()); + label.copyAllScoresIfNoCodeChange = toBoolean(labelType.isCopyAllScoresIfNoCodeChange()); + label.copyAllScoresOnTrivialRebase = toBoolean(labelType.isCopyAllScoresOnTrivialRebase()); + label.copyAllScoresOnMergeFirstParentUpdate = + toBoolean(labelType.isCopyAllScoresOnMergeFirstParentUpdate()); + label.allowPostSubmit = toBoolean(labelType.allowPostSubmit()); + label.ignoreSelfApproval = toBoolean(labelType.ignoreSelfApproval()); + return label; + } + + private static Boolean toBoolean(boolean v) { + return v ? v : null; + } + + private LabelDefinitionJson() {} +}
diff --git a/java/com/google/gerrit/server/project/LabelResource.java b/java/com/google/gerrit/server/project/LabelResource.java new file mode 100644 index 0000000..a7a2f07 --- /dev/null +++ b/java/com/google/gerrit/server/project/LabelResource.java
@@ -0,0 +1,41 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.project; + +import com.google.gerrit.common.data.LabelType; +import com.google.gerrit.extensions.restapi.RestResource; +import com.google.gerrit.extensions.restapi.RestView; +import com.google.inject.TypeLiteral; + +public class LabelResource implements RestResource { + public static final TypeLiteral<RestView<LabelResource>> LABEL_KIND = + new TypeLiteral<RestView<LabelResource>>() {}; + + private final ProjectResource project; + private final LabelType labelType; + + public LabelResource(ProjectResource project, LabelType labelType) { + this.project = project; + this.labelType = labelType; + } + + public ProjectResource getProject() { + return project; + } + + public LabelType getLabelType() { + return labelType; + } +}
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java index 44d9d98..fa877af 100644 --- a/java/com/google/gerrit/server/project/ProjectConfig.java +++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -19,6 +19,7 @@ import static com.google.gerrit.common.data.Permission.isPermission; import static com.google.gerrit.entities.Project.DEFAULT_SUBMIT_TYPE; import static com.google.gerrit.server.permissions.PluginPermissionsUtil.isValidPluginPermission; +import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toList; import com.google.common.base.CharMatcher; @@ -112,10 +113,10 @@ public static final String KEY_CAN_OVERRIDE = "canOverride"; public static final String KEY_BRANCH = "branch"; - private static final String KEY_MATCH = "match"; + public static final String KEY_MATCH = "match"; private static final String KEY_HTML = "html"; - private static final String KEY_LINK = "link"; - private static final String KEY_ENABLED = "enabled"; + public static final String KEY_LINK = "link"; + public static final String KEY_ENABLED = "enabled"; public static final String PROJECT_CONFIG = "project.config"; @@ -291,6 +292,11 @@ commentLinkSections.put(commentLink.name, commentLink); } + public void removeCommentLinkSection(String name) { + requireNonNull(name); + requireNonNull(commentLinkSections.remove(name)); + } + private ProjectConfig(Project.NameKey projectName, @Nullable StoredConfig baseConfig) { this.projectName = projectName; this.baseConfig = baseConfig; @@ -917,9 +923,18 @@ lowerNames.put(lower, name); List<LabelValue> values = new ArrayList<>(); + Set<Short> allValues = new HashSet<>(); for (String value : rc.getStringList(LABEL, name, KEY_VALUE)) { try { - values.add(parseLabelValue(value)); + LabelValue labelValue = parseLabelValue(value); + if (allValues.add(labelValue.getValue())) { + values.add(labelValue); + } else { + error( + new ValidationError( + PROJECT_CONFIG, + String.format("Duplicate %s \"%s\" for label \"%s\"", KEY_VALUE, value, name))); + } } catch (IllegalArgumentException notValue) { error( new ValidationError( @@ -1488,6 +1503,8 @@ List<String> refPatterns = label.getRefPatterns(); if (refPatterns != null && !refPatterns.isEmpty()) { rc.setStringList(LABEL, name, KEY_BRANCH, refPatterns); + } else { + rc.unset(LABEL, name, KEY_BRANCH); } }
diff --git a/java/com/google/gerrit/server/project/ProjectCreator.java b/java/com/google/gerrit/server/project/ProjectCreator.java index 2d746d8..ba44142 100644 --- a/java/com/google/gerrit/server/project/ProjectCreator.java +++ b/java/com/google/gerrit/server/project/ProjectCreator.java
@@ -28,6 +28,7 @@ import com.google.gerrit.extensions.events.NewProjectCreatedListener; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.git.LockFailureException; import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.GroupBackend; @@ -54,6 +55,12 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.ReceiveCommand; +/** + * Business logic for creating projects. + * + * <p>This creates the repository, the underlying configuration in {@code refs/meta/config} and + * initializes a first commit if necessary. + */ public class ProjectCreator { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @@ -200,10 +207,11 @@ referenceUpdated.fire( project, ru, ReceiveCommand.Type.CREATE, identifiedUser.get().state()); break; + case LOCK_FAILURE: + throw new LockFailureException(String.format("Failed to create ref \"%s\"", ref), ru); case FAST_FORWARD: case FORCED: case IO_FAILURE: - case LOCK_FAILURE: case NOT_ATTEMPTED: case NO_CHANGE: case REJECTED:
diff --git a/java/com/google/gerrit/server/project/ProjectJson.java b/java/com/google/gerrit/server/project/ProjectJson.java index cd67fbd..f2254d6 100644 --- a/java/com/google/gerrit/server/project/ProjectJson.java +++ b/java/com/google/gerrit/server/project/ProjectJson.java
@@ -31,6 +31,7 @@ import com.google.inject.Singleton; import java.util.HashMap; +/** Collection of routines to populate {@link ProjectInfo}. */ @Singleton public class ProjectJson {
diff --git a/java/com/google/gerrit/server/project/ProjectNameLockManager.java b/java/com/google/gerrit/server/project/ProjectNameLockManager.java index 72036a7..f67dd04 100644 --- a/java/com/google/gerrit/server/project/ProjectNameLockManager.java +++ b/java/com/google/gerrit/server/project/ProjectNameLockManager.java
@@ -17,6 +17,14 @@ import com.google.gerrit.entities.Project; import java.util.concurrent.locks.Lock; +/** + * A per-repo lock mechanism. + * + * <p>This ensures that project creation (repo creation, config creation, first commit) is atomic, + * and can be used to separate creation and deletion in the delete-project plugin. + * + * <p>This is an interface because distributed setup may need something beyond an in-memory lock. + */ public interface ProjectNameLockManager { public Lock getLock(Project.NameKey name); }
diff --git a/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java index f6aba34..110beaf 100644 --- a/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java +++ b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
@@ -48,13 +48,10 @@ import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.ChangeIdPredicate; import com.google.gerrit.server.query.change.CommitPredicate; -import com.google.gerrit.server.query.change.InternalChangeQuery; import com.google.gerrit.server.query.change.ProjectPredicate; import com.google.gerrit.server.query.change.RefPredicate; import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryHelper.ActionType; import com.google.inject.Inject; -import com.google.inject.Provider; import com.google.inject.Singleton; import java.io.IOException; import java.util.ArrayList; @@ -76,7 +73,6 @@ private final GitRepositoryManager repoManager; private final RetryHelper retryHelper; - private final Provider<InternalChangeQuery> changeQueryProvider; private final ChangeJson.Factory changeJsonFactory; private final IndexConfig indexConfig; @@ -84,12 +80,10 @@ ProjectsConsistencyChecker( GitRepositoryManager repoManager, RetryHelper retryHelper, - Provider<InternalChangeQuery> changeQueryProvider, ChangeJson.Factory changeJsonFactory, IndexConfig indexConfig) { this.repoManager = repoManager; this.retryHelper = retryHelper; - this.changeQueryProvider = changeQueryProvider; this.changeJsonFactory = changeJsonFactory; this.indexConfig = indexConfig; } @@ -264,16 +258,13 @@ try { List<ChangeData> queryResult = - retryHelper.execute( - ActionType.INDEX_QUERY, - () -> { - // Execute the query. - return changeQueryProvider - .get() - .setRequestedFields(ChangeField.CHANGE, ChangeField.PATCH_SET) - .query(and(basePredicate, or(predicates))); - }, - StorageException.class::isInstance); + retryHelper + .changeIndexQuery( + "projectsConsistencyCheckerQueryChanges", + q -> + q.setRequestedFields(ChangeField.CHANGE, ChangeField.PATCH_SET) + .query(and(basePredicate, or(predicates)))) + .call(); // Result for this query that we want to return to the client. List<ChangeInfo> autoCloseableChangesByBranch = new ArrayList<>(); @@ -282,32 +273,34 @@ // Skip changes that we have already processed, either by this query or by // earlier queries. if (seenChanges.add(autoCloseableChange.getId())) { - retryHelper.execute( - ActionType.CHANGE_UPDATE, - () -> { - // Auto-close by change - if (changeIdToMergedSha1.containsKey(autoCloseableChange.change().getKey())) { - autoCloseableChangesByBranch.add( - changeJson( - fix, changeIdToMergedSha1.get(autoCloseableChange.change().getKey())) - .format(autoCloseableChange)); - return null; - } + retryHelper + .changeUpdate( + "projectsConsistencyCheckerAutoCloseChanges", + () -> { + // Auto-close by change + if (changeIdToMergedSha1.containsKey(autoCloseableChange.change().getKey())) { + autoCloseableChangesByBranch.add( + changeJson( + fix, + changeIdToMergedSha1.get(autoCloseableChange.change().getKey())) + .format(autoCloseableChange)); + return null; + } - // Auto-close by commit - for (ObjectId patchSetSha1 : - autoCloseableChange.patchSets().stream() - .map(PatchSet::commitId) - .collect(toSet())) { - if (mergedSha1s.contains(patchSetSha1)) { - autoCloseableChangesByBranch.add( - changeJson(fix, patchSetSha1).format(autoCloseableChange)); - break; - } - } - return null; - }, - StorageException.class::isInstance); + // Auto-close by commit + for (ObjectId patchSetSha1 : + autoCloseableChange.patchSets().stream() + .map(PatchSet::commitId) + .collect(toSet())) { + if (mergedSha1s.contains(patchSetSha1)) { + autoCloseableChangesByBranch.add( + changeJson(fix, patchSetSha1).format(autoCloseableChange)); + break; + } + } + return null; + }) + .call(); } }
diff --git a/java/com/google/gerrit/server/project/Reachable.java b/java/com/google/gerrit/server/project/Reachable.java index 4ea5d11..57e9a7e 100644 --- a/java/com/google/gerrit/server/project/Reachable.java +++ b/java/com/google/gerrit/server/project/Reachable.java
@@ -17,14 +17,17 @@ import com.google.common.flogger.FluentLogger; import com.google.gerrit.entities.Project; import com.google.gerrit.server.change.IncludedInResolver; +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.permissions.PermissionBackend; import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions; import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.inject.Inject; import com.google.inject.Singleton; import java.io.IOException; +import java.util.Collection; import java.util.List; -import java.util.Map; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; @@ -53,12 +56,20 @@ public boolean fromRefs( Project.NameKey project, Repository repo, RevCommit commit, List<Ref> refs) { try (RevWalk rw = new RevWalk(repo)) { - Map<String, Ref> filtered = + Collection<Ref> filtered = permissionBackend .currentUser() .project(project) .filter(refs, repo, RefFilterOptions.defaults()); - return IncludedInResolver.includedInAny(repo, rw, commit, filtered.values()); + + // The filtering above already produces a voluminous trace. To separate the permission check + // from the reachability check, do the trace here: + try (TraceTimer timer = + TraceContext.newTimer( + "IncludedInResolver.includedInAny", + Metadata.builder().projectName(project.get()).resourceCount(refs.size()).build())) { + return IncludedInResolver.includedInAny(repo, rw, commit, filtered); + } } catch (IOException | PermissionBackendException e) { logger.atSevere().withCause(e).log( "Cannot verify permissions to commit object %s in repository %s", commit.name(), project);
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java index c6beac4..78ca0fc 100644 --- a/java/com/google/gerrit/server/query/change/ChangeData.java +++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -598,7 +598,11 @@ committer = c.getCommitterIdent(); parentCount = c.getParentCount(); } catch (IOException e) { - throw new StorageException(e); + throw new StorageException( + String.format( + "Loading commit %s for ps %d of change %d failed.", + ps.commitId(), ps.id().get(), ps.id().changeId().get()), + e); } return true; }
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java index d2fc77d..5f076b1 100644 --- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java +++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -25,6 +25,7 @@ import com.google.common.base.Enums; import com.google.common.base.Splitter; import com.google.common.collect.Lists; +import com.google.common.flogger.FluentLogger; import com.google.common.primitives.Ints; import com.google.gerrit.common.data.GroupDescription; import com.google.gerrit.common.data.GroupReference; @@ -63,6 +64,8 @@ import com.google.gerrit.server.change.ChangeTriplet; import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.config.AllUsersName; +import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.config.OperatorAliasConfig; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.index.change.ChangeField; import com.google.gerrit.server.index.change.ChangeIndex; @@ -93,10 +96,13 @@ import java.util.regex.Pattern; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Repository; /** Parses a query string meant to be applied to change objects. */ public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuilder> { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + public interface ChangeOperatorFactory extends OperatorFactory<ChangeData, ChangeQueryBuilder> {} /** @@ -219,6 +225,8 @@ final SubmitDryRun submitDryRun; final GroupMembers groupMembers; final Provider<AnonymousUser> anonymousUserProvider; + final OperatorAliasConfig operatorAliasConfig; + final boolean indexMergeable; private final Provider<CurrentUser> self; @@ -250,7 +258,9 @@ StarredChangesUtil starredChangesUtil, AccountCache accountCache, GroupMembers groupMembers, - Provider<AnonymousUser> anonymousUserProvider) { + Provider<AnonymousUser> anonymousUserProvider, + OperatorAliasConfig operatorAliasConfig, + @GerritServerConfig Config gerritConfig) { this( queryProvider, rewriter, @@ -277,7 +287,9 @@ starredChangesUtil, accountCache, groupMembers, - anonymousUserProvider); + anonymousUserProvider, + operatorAliasConfig, + gerritConfig.getBoolean("index", "change", "indexMergeable", true)); } private Arguments( @@ -306,7 +318,9 @@ StarredChangesUtil starredChangesUtil, AccountCache accountCache, GroupMembers groupMembers, - Provider<AnonymousUser> anonymousUserProvider) { + Provider<AnonymousUser> anonymousUserProvider, + OperatorAliasConfig operatorAliasConfig, + boolean indexMergeable) { this.queryProvider = queryProvider; this.rewriter = rewriter; this.opFactories = opFactories; @@ -333,6 +347,8 @@ this.hasOperands = hasOperands; this.groupMembers = groupMembers; this.anonymousUserProvider = anonymousUserProvider; + this.operatorAliasConfig = operatorAliasConfig; + this.indexMergeable = indexMergeable; } Arguments asUser(CurrentUser otherUser) { @@ -362,7 +378,9 @@ starredChangesUtil, accountCache, groupMembers, - anonymousUserProvider); + anonymousUserProvider, + operatorAliasConfig, + indexMergeable); } Arguments asUser(Account.Id otherId) { @@ -407,6 +425,7 @@ @Inject ChangeQueryBuilder(Arguments args) { this(mydef, args); + setupAliases(); } @VisibleForTesting @@ -415,6 +434,10 @@ this.args = args; } + private void setupAliases() { + setOperatorAliases(args.operatorAliasConfig.getChangeQueryOperatorAliases()); + } + public Arguments getArgs() { return args; } @@ -558,6 +581,9 @@ } if ("mergeable".equalsIgnoreCase(value)) { + if (!args.indexMergeable) { + throw new QueryParseException("'is:mergeable' operator is not supported by server"); + } return new BooleanPredicate(ChangeField.MERGEABLE); } @@ -1233,6 +1259,15 @@ throw new QueryParseException("'revertof' operator is not supported by change index version"); } + @Operator + public Predicate<ChangeData> submissionId(String value) throws QueryParseException { + if (args.getSchema().hasField(ChangeField.SUBMISSIONID)) { + return new SubmissionIdPredicate(value); + } + throw new QueryParseException( + "'submissionid' operator is not supported by change index version"); + } + @Override protected Predicate<ChangeData> defaultField(String query) throws QueryParseException { if (query.startsWith("refs/")) { @@ -1362,7 +1397,11 @@ private List<Change> parseChange(String value) throws QueryParseException { if (PAT_LEGACY_ID.matcher(value).matches()) { - return asChanges(args.queryProvider.get().byLegacyChangeId(Change.Id.parse(value))); + Optional<Change.Id> id = Change.Id.tryParse(value); + if (!id.isPresent()) { + throw error("Invalid change id " + value); + } + return asChanges(args.queryProvider.get().byLegacyChangeId(id.get())); } else if (PAT_CHANGE_ID.matcher(value).matches()) { List<Change> changes = asChanges(args.queryProvider.get().byKeyPrefix(parseChangeId(value))); if (changes.isEmpty()) { @@ -1405,8 +1444,14 @@ accounts.stream() .map(id -> ReviewerPredicate.forState(id, state)) .collect(toList())); + } else { + logger.atFine().log( + "Skipping reviewer predicate for %s in default field query" + + " because the number of matched accounts (%d) exceeds the limit of %d", + who, accounts.size(), MAX_ACCOUNTS_PER_DEFAULT_FIELD); } } catch (QueryParseException e) { + logger.atFine().log("Parsing %s as account failed: %s", who, e.getMessage()); // Propagate this exception only if we can't use 'who' to query by email if (reviewerByEmailPredicate == null) { throw e;
diff --git a/java/com/google/gerrit/server/restapi/BUILD b/java/com/google/gerrit/server/restapi/BUILD index fd341e9..720e7d2 100644 --- a/java/com/google/gerrit/server/restapi/BUILD +++ b/java/com/google/gerrit/server/restapi/BUILD
@@ -34,7 +34,6 @@ "//lib:servlet-api", "//lib/auto:auto-value", "//lib/auto:auto-value-annotations", - "//lib/commons:codec", "//lib/commons:compress", "//lib/commons:lang", "//lib/flogger:api",
diff --git a/java/com/google/gerrit/server/restapi/account/CreateAccount.java b/java/com/google/gerrit/server/restapi/account/CreateAccount.java index 8e24786..8f525ef 100644 --- a/java/com/google/gerrit/server/restapi/account/CreateAccount.java +++ b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
@@ -62,6 +62,13 @@ import java.util.Set; import org.eclipse.jgit.errors.ConfigInvalidException; +/** + * REST endpoint for creating a new account. + * + * <p>This REST endpoint handles {@code PUT /accounts/<account-identifier>} requests if the + * specified account doesn't exist yet. If it already exists, the request is handled by {@link + * PutAccount}. + */ @RequiresCapability(GlobalCapability.CREATE_ACCOUNT) @Singleton public class CreateAccount
diff --git a/java/com/google/gerrit/server/restapi/account/GetAgreements.java b/java/com/google/gerrit/server/restapi/account/GetAgreements.java index 5feca66..572b489 100644 --- a/java/com/google/gerrit/server/restapi/account/GetAgreements.java +++ b/java/com/google/gerrit/server/restapi/account/GetAgreements.java
@@ -29,6 +29,8 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountResource; import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.permissions.GlobalPermission; +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.restapi.config.AgreementJson; @@ -48,17 +50,20 @@ private final ProjectCache projectCache; private final AgreementJson agreementJson; private final boolean agreementsEnabled; + private final PermissionBackend permissionBackend; @Inject GetAgreements( Provider<CurrentUser> self, ProjectCache projectCache, AgreementJson agreementJson, + PermissionBackend permissionBackend, @GerritServerConfig Config config) { this.self = self; this.projectCache = projectCache; this.agreementJson = agreementJson; this.agreementsEnabled = config.getBoolean("auth", "contributorAgreements", false); + this.permissionBackend = permissionBackend; } @Override @@ -74,7 +79,11 @@ IdentifiedUser user = self.get().asIdentifiedUser(); if (user != resource.getUser()) { - throw new AuthException("not allowed to get contributor agreements"); + try { + permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER); + } catch (AuthException e) { + throw new AuthException("not allowed to get contributor agreements", e); + } } List<AgreementInfo> results = new ArrayList<>();
diff --git a/java/com/google/gerrit/server/restapi/account/PutAccount.java b/java/com/google/gerrit/server/restapi/account/PutAccount.java index 4c84c19..780002c 100644 --- a/java/com/google/gerrit/server/restapi/account/PutAccount.java +++ b/java/com/google/gerrit/server/restapi/account/PutAccount.java
@@ -22,6 +22,23 @@ import com.google.gerrit.server.account.AccountResource; import com.google.inject.Singleton; +/** + * REST endpoint for updating an existing account. + * + * <p>This REST endpoint handles {@code PUT /accounts/<account-identifier>} requests if the + * specified account already exists. If it doesn't exist yet, the request is handled by {@link + * CreateAccount}. + * + * <p>We do not support account updates via this path, hence this REST endpoint always throws a + * {@link ResourceConflictException} which results in a {@code 409 Conflict} response. Account + * properties can only be updated via the dedicated REST endpoints that serve {@code PUT} requests + * on {@code /accounts/<account-identifier>/<account-view>}. + * + * <p>This REST endpoint solely exists to avoid user confusion if they create a new account with + * {@code PUT /accounts/<account-identifier>} and then repeat the same request. Without this REST + * endpoint the second request would fail with {@code 404 Not Found}, which would be surprising to + * the user. + */ @Singleton public class PutAccount implements RestModifyView<AccountResource, AccountInput> { @Override
diff --git a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java index 7b89b9c..36a0c71 100644 --- a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java +++ b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
@@ -15,9 +15,11 @@ package com.google.gerrit.server.restapi.account; import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME; +import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.base.Strings; import com.google.common.flogger.FluentLogger; +import com.google.common.io.BaseEncoding; import com.google.gerrit.common.UsedAt; import com.google.gerrit.exceptions.EmailException; import com.google.gerrit.extensions.common.HttpPasswordInput; @@ -43,7 +45,6 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Optional; -import org.apache.commons.codec.binary.Base64; import org.eclipse.jgit.errors.ConfigInvalidException; public class PutHttpPassword implements RestModifyView<AccountResource, HttpPasswordInput> { @@ -142,7 +143,7 @@ byte[] rand = new byte[LEN]; rng.nextBytes(rand); - byte[] enc = Base64.encodeBase64(rand, false); + byte[] enc = BaseEncoding.base64().encode(rand).getBytes(UTF_8); StringBuilder r = new StringBuilder(enc.length); for (int i = 0; i < enc.length; i++) { if (enc[i] == '=') {
diff --git a/java/com/google/gerrit/server/restapi/account/PutPreferred.java b/java/com/google/gerrit/server/restapi/account/PutPreferred.java index 2ddea2f..c781246 100644 --- a/java/com/google/gerrit/server/restapi/account/PutPreferred.java +++ b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
@@ -145,6 +145,6 @@ if (exception.get().isPresent()) { throw exception.get().get(); } - return alreadyPreferred.get() ? Response.ok("") : Response.created(""); + return alreadyPreferred.get() ? Response.ok() : Response.created(); } }
diff --git a/java/com/google/gerrit/server/restapi/account/PutUsername.java b/java/com/google/gerrit/server/restapi/account/PutUsername.java index 3c73d88..dabfaa5 100644 --- a/java/com/google/gerrit/server/restapi/account/PutUsername.java +++ b/java/com/google/gerrit/server/restapi/account/PutUsername.java
@@ -46,6 +46,17 @@ import java.util.Optional; import org.eclipse.jgit.errors.ConfigInvalidException; +/** + * REST endpoint to set the username of an account. + * + * <p>This REST endpoint handles {@code PUT /accounts/<account-identifier>/username} requests. + * + * <p>Whether a username can be set depends on whether the used {@link Realm} supports this. + * + * <p>Once set a username cannot be changed or deleted. Changing usernames is disallowed because + * they can be used in ref names that represent user-specific sandbox branches which can exist in + * any repository and we have no way to find and rename those refs. + */ @Singleton public class PutUsername implements RestModifyView<AccountResource, UsernameInput> { private final Provider<CurrentUser> self;
diff --git a/java/com/google/gerrit/server/restapi/account/Stars.java b/java/com/google/gerrit/server/restapi/account/Stars.java index cdaa99d..c27bdd8 100644 --- a/java/com/google/gerrit/server/restapi/account/Stars.java +++ b/java/com/google/gerrit/server/restapi/account/Stars.java
@@ -98,7 +98,8 @@ @Override @SuppressWarnings("unchecked") - public Response<List<ChangeInfo>> apply(AccountResource rsrc) throws Exception { + public Response<List<ChangeInfo>> apply(AccountResource rsrc) + throws RestApiException, PermissionBackendException { if (!self.get().hasSameAccountId(rsrc.getUser())) { throw new AuthException("not allowed to list stars of another account"); }
diff --git a/java/com/google/gerrit/server/restapi/change/Abandon.java b/java/com/google/gerrit/server/restapi/change/Abandon.java index df3b58e..ae69ccd 100644 --- a/java/com/google/gerrit/server/restapi/change/Abandon.java +++ b/java/com/google/gerrit/server/restapi/change/Abandon.java
@@ -22,6 +22,7 @@ import com.google.gerrit.extensions.common.ChangeInfo; 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; @@ -34,8 +35,6 @@ import com.google.gerrit.server.permissions.ChangePermission; import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.update.BatchUpdate; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.time.TimeUtil; import com.google.inject.Inject; @@ -44,10 +43,11 @@ import org.eclipse.jgit.errors.ConfigInvalidException; @Singleton -public class Abandon extends RetryingRestModifyView<ChangeResource, AbandonInput, ChangeInfo> - implements UiAction<ChangeResource> { +public class Abandon + implements RestModifyView<ChangeResource, AbandonInput>, UiAction<ChangeResource> { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private final BatchUpdate.Factory updateFactory; private final ChangeJson.Factory json; private final AbandonOp.Factory abandonOpFactory; private final NotifyResolver notifyResolver; @@ -55,12 +55,12 @@ @Inject Abandon( + BatchUpdate.Factory updateFactory, ChangeJson.Factory json, - RetryHelper retryHelper, AbandonOp.Factory abandonOpFactory, NotifyResolver notifyResolver, PatchSetUtil patchSetUtil) { - super(retryHelper); + this.updateFactory = updateFactory; this.json = json; this.abandonOpFactory = abandonOpFactory; this.notifyResolver = notifyResolver; @@ -68,8 +68,7 @@ } @Override - protected Response<ChangeInfo> applyImpl( - BatchUpdate.Factory updateFactory, ChangeResource rsrc, AbandonInput input) + public Response<ChangeInfo> apply(ChangeResource rsrc, AbandonInput input) throws RestApiException, UpdateException, PermissionBackendException, IOException, ConfigInvalidException { // Not allowed to abandon if the current patch set is locked.
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyFix.java b/java/com/google/gerrit/server/restapi/change/ApplyFix.java index d31fd92..74c5bc2 100644 --- a/java/com/google/gerrit/server/restapi/change/ApplyFix.java +++ b/java/com/google/gerrit/server/restapi/change/ApplyFix.java
@@ -18,6 +18,7 @@ import com.google.gerrit.entities.Project; import com.google.gerrit.extensions.common.EditInfo; import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.Response; @@ -65,8 +66,8 @@ @Override public Response<EditInfo> apply(FixResource fixResource, Void nothing) - throws AuthException, ResourceConflictException, IOException, ResourceNotFoundException, - PermissionBackendException { + throws AuthException, BadRequestException, ResourceConflictException, IOException, + ResourceNotFoundException, PermissionBackendException { RevisionResource revisionResource = fixResource.getRevisionResource(); Project.NameKey project = revisionResource.getProject(); ProjectState projectState = projectCache.checkedGet(project);
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java index 6955d8b..e220f35 100644 --- a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java +++ b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
@@ -19,6 +19,7 @@ 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.FileContentInput; import com.google.gerrit.extensions.common.DiffWebLinkInfo; import com.google.gerrit.extensions.common.EditInfo; import com.google.gerrit.extensions.common.Input; @@ -107,8 +108,9 @@ * PUT request with a path was called but change edit wasn't created yet. Change edit is created * and PUT handler is called. */ + @Singleton public static class Create - implements RestCollectionCreateView<ChangeResource, ChangeEditResource, Put.Input> { + implements RestCollectionCreateView<ChangeResource, ChangeEditResource, FileContentInput> { private final Put putEdit; @Inject @@ -117,13 +119,14 @@ } @Override - public Response<?> apply(ChangeResource resource, IdString id, Put.Input input) + public Response<?> apply(ChangeResource resource, IdString id, FileContentInput input) throws AuthException, ResourceConflictException, IOException, PermissionBackendException { - putEdit.apply(resource, id.get(), input.content); + putEdit.apply(resource, id.get(), input); return Response.none(); } } + @Singleton public static class DeleteFile implements RestCollectionDeleteMissingView<ChangeResource, ChangeEditResource, Input> { private final DeleteContent deleteContent; @@ -265,11 +268,7 @@ /** Put handler that is activated when PUT request is called on collection element. */ @Singleton - public static class Put implements RestModifyView<ChangeEditResource, Put.Input> { - public static class Input { - @DefaultInput public RawInput content; - } - + public static class Put implements RestModifyView<ChangeEditResource, FileContentInput> { private final ChangeEditModifier editModifier; private final GitRepositoryManager repositoryManager; @@ -280,17 +279,18 @@ } @Override - public Response<?> apply(ChangeEditResource rsrc, Input input) + public Response<?> apply(ChangeEditResource rsrc, FileContentInput input) throws AuthException, ResourceConflictException, IOException, PermissionBackendException { - return apply(rsrc.getChangeResource(), rsrc.getPath(), input.content); + return apply(rsrc.getChangeResource(), rsrc.getPath(), input); } - public Response<?> apply(ChangeResource rsrc, String path, RawInput newContent) + public Response<?> apply(ChangeResource rsrc, String path, FileContentInput input) throws ResourceConflictException, AuthException, IOException, PermissionBackendException { if (Strings.isNullOrEmpty(path) || path.charAt(0) == '/') { throw new ResourceConflictException("Invalid path: " + path); } + RawInput newContent = input.content; try (Repository repository = repositoryManager.openRepository(rsrc.getProject())) { editModifier.modifyFile(repository, rsrc.getNotes(), path, newContent); } catch (InvalidChangeOperationException e) {
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPick.java b/java/com/google/gerrit/server/restapi/change/CherryPick.java index 1a89935..725defc 100644 --- a/java/com/google/gerrit/server/restapi/change/CherryPick.java +++ b/java/com/google/gerrit/server/restapi/change/CherryPick.java
@@ -25,6 +25,7 @@ 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.change.ChangeJson; import com.google.gerrit.server.change.RevisionResource; @@ -39,8 +40,6 @@ import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.submit.IntegrationException; import com.google.gerrit.server.update.BatchUpdate; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -49,11 +48,11 @@ @Singleton public class CherryPick - extends RetryingRestModifyView<RevisionResource, CherryPickInput, CherryPickChangeInfo> - implements UiAction<RevisionResource> { + implements RestModifyView<RevisionResource, CherryPickInput>, UiAction<RevisionResource> { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private final PermissionBackend permissionBackend; + private final BatchUpdate.Factory updateFactory; private final CherryPickChange cherryPickChange; private final ChangeJson.Factory json; private final ContributorAgreementsChecker contributorAgreements; @@ -62,13 +61,13 @@ @Inject CherryPick( PermissionBackend permissionBackend, - RetryHelper retryHelper, + BatchUpdate.Factory updateFactory, CherryPickChange cherryPickChange, ChangeJson.Factory json, ContributorAgreementsChecker contributorAgreements, ProjectCache projectCache) { - super(retryHelper); this.permissionBackend = permissionBackend; + this.updateFactory = updateFactory; this.cherryPickChange = cherryPickChange; this.json = json; this.contributorAgreements = contributorAgreements; @@ -76,8 +75,7 @@ } @Override - public Response<CherryPickChangeInfo> applyImpl( - BatchUpdate.Factory updateFactory, RevisionResource rsrc, CherryPickInput input) + public Response<CherryPickChangeInfo> apply(RevisionResource rsrc, CherryPickInput input) throws IOException, UpdateException, RestApiException, PermissionBackendException, ConfigInvalidException, NoSuchProjectException { input.parent = input.parent == null ? 1 : input.parent;
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java index 1973b00..a7a7998 100644 --- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java +++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -18,6 +18,7 @@ import com.google.auto.value.AutoValue; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.gerrit.common.FooterConstants; import com.google.gerrit.common.Nullable; @@ -168,6 +169,9 @@ patch.commitId(), input, dest, + false, + null, + null, null, null, null); @@ -204,7 +208,18 @@ throws IOException, InvalidChangeOperationException, IntegrationException, UpdateException, RestApiException, ConfigInvalidException, NoSuchProjectException { return cherryPick( - batchUpdateFactory, sourceChange, project, sourceCommit, input, dest, null, null, null); + batchUpdateFactory, + sourceChange, + project, + sourceCommit, + input, + dest, + false, + null, + null, + null, + null, + null); } /** @@ -219,16 +234,23 @@ * @param sourceCommit Id of the commit to be cherry picked. * @param input Input object for different configurations of cherry pick. * @param dest Destination branch for the cherry pick. + * @param ignoreIdenticalTree When false, we throw an error when trying to cherry-pick creates an + * empty commit. When true, we allow creation of an empty commit. * @param topic Topic name for the change created. * @param revertedChange The id of the change that is reverted. This is used for the "revertOf" * field to mark the created cherry pick change as "revertOf" the original change that was * reverted. - * @param changeIdForNewChange The Change-Id that the new change that of the cherry pick will - * have. + * @param changeIdForNewChange The Change-Id that the new change of the cherry pick will have. + * @param idForNewChange The ID that the new change of the cherry pick will have. If provided and + * the cherry-pick doesn't result in creating a new change, then + * InvalidChangeOperationException is thrown. + * @param groupName The name of the group for grouping related changes (used by GetRelated + * endpoint). * @return Result object that describes the cherry pick. * @throws IOException Unable to open repository or read from the database. * @throws InvalidChangeOperationException Parent or branch don't exist, or two changes with same - * key exist in the branch. + * key exist in the branch. Also thrown when idForNewChange is not null but cherry-pick only + * creates a new patchset rather than a new change. * @throws IntegrationException Merge conflict or trees are identical after cherry pick. * @throws UpdateException Problem updating the database using batchUpdateFactory. * @throws RestApiException Error such as invalid SHA1 @@ -242,9 +264,12 @@ ObjectId sourceCommit, CherryPickInput input, BranchNameKey dest, + boolean ignoreIdenticalTree, @Nullable String topic, @Nullable Change.Id revertedChange, - @Nullable ObjectId changeIdForNewChange) + @Nullable ObjectId changeIdForNewChange, + @Nullable Change.Id idForNewChange, + @Nullable String groupName) throws IOException, InvalidChangeOperationException, IntegrationException, UpdateException, RestApiException, ConfigInvalidException, NoSuchProjectException { @@ -308,7 +333,7 @@ commitMessage, revWalk, input.parent - 1, - false, + ignoreIdenticalTree, input.allowConflicts); Change.Key changeKey; @@ -336,7 +361,19 @@ Change.Id changeId; if (destChanges.size() == 1) { // The change key exists on the destination branch. The cherry pick - // will be added as a new patch set. + // will be added as a new patch set. If "idForNewChange" is not null we must fail, + // since we are not expecting an already existing change. + if (idForNewChange != null) { + throw new InvalidChangeOperationException( + String.format( + "Expected that cherry-pick of commit %s with Change-Id %s to branch %s" + + "in project %s creates a new change, but found existing change %d", + sourceCommit.getName(), + changeKey, + dest.branch(), + dest.project(), + destChanges.get(0).getId().get())); + } changeId = insertPatchSet(bu, git, destChanges.get(0).notes(), cherryPickCommit); } else { // Change key not found on destination branch. We can create a new @@ -356,7 +393,9 @@ sourceChange, sourceCommit, input, - revertedChange); + revertedChange, + idForNewChange, + groupName); } bu.execute(); return Result.create(changeId, cherryPickCommit.getFilesWithGitConflicts()); @@ -436,9 +475,11 @@ @Nullable Change sourceChange, @Nullable ObjectId sourceCommit, CherryPickInput input, - @Nullable Change.Id revertOf) + @Nullable Change.Id revertOf, + @Nullable Change.Id idForNewChange, + @Nullable String groupName) throws IOException { - Change.Id changeId = Change.id(seq.nextChangeId()); + Change.Id changeId = idForNewChange != null ? idForNewChange : Change.id(seq.nextChangeId()); ChangeInserter ins = changeInserterFactory.create(changeId, cherryPickCommit, refName); ins.setRevertOf(revertOf); BranchNameKey sourceBranch = sourceChange == null ? null : sourceChange.getDest(); @@ -462,6 +503,9 @@ Set<Account.Id> ccs = new HashSet<>(reviewerSet.byState(ReviewerStateInternal.CC)); ccs.remove(user.get().getAccountId()); ins.setReviewersAndCcs(reviewers, ccs); + if (groupName != null) { + ins.setGroups(ImmutableList.of(groupName)); + } } bu.insertChange(ins); return changeId;
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java b/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java index a3c8a97..729e32d 100644 --- a/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java +++ b/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
@@ -24,6 +24,7 @@ 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.server.CurrentUser; import com.google.gerrit.server.change.ChangeJson; import com.google.gerrit.server.permissions.PermissionBackend; @@ -35,8 +36,6 @@ import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.submit.IntegrationException; import com.google.gerrit.server.update.BatchUpdate; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -45,9 +44,9 @@ import org.eclipse.jgit.errors.ConfigInvalidException; @Singleton -public class CherryPickCommit - extends RetryingRestModifyView<CommitResource, CherryPickInput, CherryPickChangeInfo> { +public class CherryPickCommit implements RestModifyView<CommitResource, CherryPickInput> { private final PermissionBackend permissionBackend; + private final BatchUpdate.Factory updateFactory; private final Provider<CurrentUser> user; private final CherryPickChange cherryPickChange; private final ChangeJson.Factory json; @@ -55,14 +54,14 @@ @Inject CherryPickCommit( - RetryHelper retryHelper, + PermissionBackend permissionBackend, + BatchUpdate.Factory updateFactory, Provider<CurrentUser> user, CherryPickChange cherryPickChange, ChangeJson.Factory json, - PermissionBackend permissionBackend, ContributorAgreementsChecker contributorAgreements) { - super(retryHelper); this.permissionBackend = permissionBackend; + this.updateFactory = updateFactory; this.user = user; this.cherryPickChange = cherryPickChange; this.json = json; @@ -70,8 +69,7 @@ } @Override - public Response<CherryPickChangeInfo> applyImpl( - BatchUpdate.Factory updateFactory, CommitResource rsrc, CherryPickInput input) + public Response<CherryPickChangeInfo> apply(CommitResource rsrc, CherryPickInput input) throws IOException, UpdateException, RestApiException, PermissionBackendException, ConfigInvalidException, NoSuchProjectException { String destination = Strings.nullToEmpty(input.destination).trim();
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java index acc6465..310f71b 100644 --- a/java/com/google/gerrit/server/restapi/change/CreateChange.java +++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -27,6 +27,7 @@ import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.Project; import com.google.gerrit.entities.RefNames; +import com.google.gerrit.exceptions.InvalidMergeStrategyException; import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.client.SubmitType; @@ -40,6 +41,7 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestCollectionModifyView; import com.google.gerrit.extensions.restapi.TopLevelResource; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.server.CurrentUser; @@ -69,8 +71,6 @@ import com.google.gerrit.server.restapi.project.CommitsCollection; import com.google.gerrit.server.restapi.project.ProjectsCollection; import com.google.gerrit.server.update.BatchUpdate; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestCollectionModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.time.TimeUtil; import com.google.inject.Inject; @@ -103,8 +103,8 @@ @Singleton public class CreateChange - extends RetryingRestCollectionModifyView< - TopLevelResource, ChangeResource, ChangeInput, ChangeInfo> { + implements RestCollectionModifyView<TopLevelResource, ChangeResource, ChangeInput> { + private final BatchUpdate.Factory updateFactory; private final String anonymousCowardName; private final GitRepositoryManager gitManager; private final Sequences seq; @@ -126,6 +126,7 @@ @Inject CreateChange( + BatchUpdate.Factory updateFactory, @AnonymousCowardName String anonymousCowardName, GitRepositoryManager gitManager, Sequences seq, @@ -138,13 +139,12 @@ ChangeJson.Factory json, ChangeFinder changeFinder, Provider<InternalChangeQuery> queryProvider, - RetryHelper retryHelper, PatchSetUtil psUtil, @GerritServerConfig Config config, MergeUtil.Factory mergeUtilFactory, NotifyResolver notifyResolver, ContributorAgreementsChecker contributorAgreements) { - super(retryHelper); + this.updateFactory = updateFactory; this.anonymousCowardName = anonymousCowardName; this.gitManager = gitManager; this.seq = seq; @@ -166,8 +166,7 @@ } @Override - protected Response<ChangeInfo> applyImpl( - BatchUpdate.Factory updateFactory, TopLevelResource parent, ChangeInput input) + public Response<ChangeInfo> apply(TopLevelResource parent, ChangeInput input) throws IOException, InvalidChangeOperationException, RestApiException, UpdateException, PermissionBackendException, ConfigInvalidException { if (!user.get().isIdentifiedUser()) { @@ -341,8 +340,8 @@ bu.execute(); } return ins.getChange(); - } catch (IllegalArgumentException e) { - throw new BadRequestException(e.getMessage(), e); + } catch (InvalidMergeStrategyException e) { + throw new BadRequestException(e.getMessage()); } }
diff --git a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java index f434e31..5b7245d 100644 --- a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java +++ b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
@@ -25,6 +25,7 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 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.restapi.Url; import com.google.gerrit.server.CommentsUtil; @@ -36,8 +37,6 @@ import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.time.TimeUtil; import com.google.inject.Inject; @@ -46,8 +45,8 @@ import java.util.Collections; @Singleton -public class CreateDraftComment - extends RetryingRestModifyView<RevisionResource, DraftInput, CommentInfo> { +public class CreateDraftComment implements RestModifyView<RevisionResource, DraftInput> { + private final BatchUpdate.Factory updateFactory; private final Provider<CommentJson> commentJson; private final CommentsUtil commentsUtil; private final PatchSetUtil psUtil; @@ -55,12 +54,12 @@ @Inject CreateDraftComment( - RetryHelper retryHelper, + BatchUpdate.Factory updateFactory, Provider<CommentJson> commentJson, CommentsUtil commentsUtil, PatchSetUtil psUtil, PatchListCache patchListCache) { - super(retryHelper); + this.updateFactory = updateFactory; this.commentJson = commentJson; this.commentsUtil = commentsUtil; this.psUtil = psUtil; @@ -68,8 +67,7 @@ } @Override - protected Response<CommentInfo> applyImpl( - BatchUpdate.Factory updateFactory, RevisionResource rsrc, DraftInput in) + public Response<CommentInfo> apply(RevisionResource rsrc, DraftInput in) throws RestApiException, UpdateException, PermissionBackendException { if (Strings.isNullOrEmpty(in.path)) { throw new BadRequestException("path must be non-empty");
diff --git a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java index b84ac12..8d50b86 100644 --- a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java +++ b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
@@ -31,6 +31,7 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 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.server.ChangeUtil; import com.google.gerrit.server.CurrentUser; @@ -53,8 +54,6 @@ import com.google.gerrit.server.restapi.project.CommitsCollection; import com.google.gerrit.server.submit.MergeIdenticalTreeException; import com.google.gerrit.server.update.BatchUpdate; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.time.TimeUtil; import com.google.inject.Inject; @@ -75,8 +74,8 @@ import org.eclipse.jgit.util.ChangeIdUtil; @Singleton -public class CreateMergePatchSet - extends RetryingRestModifyView<ChangeResource, MergePatchSetInput, ChangeInfo> { +public class CreateMergePatchSet implements RestModifyView<ChangeResource, MergePatchSetInput> { + private final BatchUpdate.Factory updateFactory; private final GitRepositoryManager gitManager; private final CommitsCollection commits; private final TimeZone serverTimeZone; @@ -91,6 +90,7 @@ @Inject CreateMergePatchSet( + BatchUpdate.Factory updateFactory, GitRepositoryManager gitManager, CommitsCollection commits, @GerritPersonIdent PersonIdent myIdent, @@ -98,12 +98,11 @@ ChangeJson.Factory json, PatchSetUtil psUtil, MergeUtil.Factory mergeUtilFactory, - RetryHelper retryHelper, PatchSetInserter.Factory patchSetInserterFactory, ProjectCache projectCache, ChangeFinder changeFinder, PermissionBackend permissionBackend) { - super(retryHelper); + this.updateFactory = updateFactory; this.gitManager = gitManager; this.commits = commits; this.serverTimeZone = myIdent.getTimeZone(); @@ -118,8 +117,7 @@ } @Override - protected Response<ChangeInfo> applyImpl( - BatchUpdate.Factory updateFactory, ChangeResource rsrc, MergePatchSetInput in) + public Response<ChangeInfo> apply(ChangeResource rsrc, MergePatchSetInput in) throws IOException, RestApiException, UpdateException, PermissionBackendException { // Not allowed to create a new patch set if the current patch set is locked. psUtil.checkPatchSetNotLocked(rsrc.getNotes());
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java b/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java index 834782f..20fd675 100644 --- a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java +++ b/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
@@ -21,6 +21,7 @@ import com.google.gerrit.extensions.common.Input; 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.server.ChangeMessagesUtil; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountLoader; @@ -34,16 +35,14 @@ import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.server.update.Context; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; 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; @Singleton -public class DeleteAssignee extends RetryingRestModifyView<ChangeResource, Input, AccountInfo> { - +public class DeleteAssignee implements RestModifyView<ChangeResource, Input> { + private final BatchUpdate.Factory updateFactory; private final ChangeMessagesUtil cmUtil; private final AssigneeChanged assigneeChanged; private final IdentifiedUser.GenericFactory userFactory; @@ -51,12 +50,12 @@ @Inject DeleteAssignee( - RetryHelper retryHelper, + BatchUpdate.Factory updateFactory, ChangeMessagesUtil cmUtil, AssigneeChanged assigneeChanged, IdentifiedUser.GenericFactory userFactory, AccountLoader.Factory accountLoaderFactory) { - super(retryHelper); + this.updateFactory = updateFactory; this.cmUtil = cmUtil; this.assigneeChanged = assigneeChanged; this.userFactory = userFactory; @@ -64,8 +63,7 @@ } @Override - protected Response<AccountInfo> applyImpl( - BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input) + public Response<AccountInfo> apply(ChangeResource rsrc, Input input) throws RestApiException, UpdateException, PermissionBackendException { rsrc.permissions().check(ChangePermission.EDIT_ASSIGNEE);
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChange.java b/java/com/google/gerrit/server/restapi/change/DeleteChange.java index aa4dcf0..3ca5463 100644 --- a/java/com/google/gerrit/server/restapi/change/DeleteChange.java +++ b/java/com/google/gerrit/server/restapi/change/DeleteChange.java
@@ -21,6 +21,7 @@ import com.google.gerrit.extensions.restapi.MethodNotAllowedException; 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.change.ChangeResource; import com.google.gerrit.server.change.DeleteChangeOp; @@ -28,28 +29,25 @@ import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.update.BatchUpdate; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; 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; @Singleton -public class DeleteChange extends RetryingRestModifyView<ChangeResource, Input, Object> - implements UiAction<ChangeResource> { - +public class DeleteChange + implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> { + private final BatchUpdate.Factory updateFactory; private final DeleteChangeOp.Factory opFactory; @Inject - public DeleteChange(RetryHelper retryHelper, DeleteChangeOp.Factory opFactory) { - super(retryHelper); + public DeleteChange(BatchUpdate.Factory updateFactory, DeleteChangeOp.Factory opFactory) { + this.updateFactory = updateFactory; this.opFactory = opFactory; } @Override - protected Response<Object> applyImpl( - BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input) + public Response<Object> apply(ChangeResource rsrc, Input input) throws RestApiException, UpdateException, PermissionBackendException { if (!isChangeDeletable(rsrc)) { throw new MethodNotAllowedException("delete not permitted");
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java b/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java index 30cfad6..f79209d 100644 --- a/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java +++ b/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
@@ -28,6 +28,7 @@ import com.google.gerrit.extensions.common.Input; 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.server.ChangeMessagesUtil; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.account.AccountLoader; @@ -39,8 +40,6 @@ import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.time.TimeUtil; import com.google.inject.Inject; @@ -52,11 +51,11 @@ /** Deletes a change message by rewriting history. */ @Singleton public class DeleteChangeMessage - extends RetryingRestModifyView< - ChangeMessageResource, DeleteChangeMessageInput, ChangeMessageInfo> { + implements RestModifyView<ChangeMessageResource, DeleteChangeMessageInput> { private final Provider<CurrentUser> userProvider; private final PermissionBackend permissionBackend; + private final BatchUpdate.Factory updateFactory; private final ChangeMessagesUtil changeMessagesUtil; private final AccountLoader.Factory accountLoaderFactory; private final ChangeNotes.Factory notesFactory; @@ -65,23 +64,21 @@ public DeleteChangeMessage( Provider<CurrentUser> userProvider, PermissionBackend permissionBackend, + BatchUpdate.Factory updateFactory, ChangeMessagesUtil changeMessagesUtil, AccountLoader.Factory accountLoaderFactory, - ChangeNotes.Factory notesFactory, - RetryHelper retryHelper) { - super(retryHelper); + ChangeNotes.Factory notesFactory) { this.userProvider = userProvider; this.permissionBackend = permissionBackend; + this.updateFactory = updateFactory; this.changeMessagesUtil = changeMessagesUtil; this.accountLoaderFactory = accountLoaderFactory; this.notesFactory = notesFactory; } @Override - public Response<ChangeMessageInfo> applyImpl( - BatchUpdate.Factory updateFactory, - ChangeMessageResource resource, - DeleteChangeMessageInput input) + public Response<ChangeMessageInfo> apply( + ChangeMessageResource resource, DeleteChangeMessageInput input) throws RestApiException, PermissionBackendException, UpdateException, IOException { CurrentUser user = userProvider.get(); permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER); @@ -146,21 +143,18 @@ @Singleton public static class DefaultDeleteChangeMessage - extends RetryingRestModifyView<ChangeMessageResource, Input, ChangeMessageInfo> { + implements RestModifyView<ChangeMessageResource, Input> { private final DeleteChangeMessage deleteChangeMessage; @Inject - public DefaultDeleteChangeMessage( - DeleteChangeMessage deleteChangeMessage, RetryHelper retryHelper) { - super(retryHelper); + public DefaultDeleteChangeMessage(DeleteChangeMessage deleteChangeMessage) { this.deleteChangeMessage = deleteChangeMessage; } @Override - protected Response<ChangeMessageInfo> applyImpl( - BatchUpdate.Factory updateFactory, ChangeMessageResource resource, Input input) - throws Exception { - return deleteChangeMessage.applyImpl(updateFactory, resource, new DeleteChangeMessageInput()); + public Response<ChangeMessageInfo> apply(ChangeMessageResource resource, Input input) + throws RestApiException, PermissionBackendException, UpdateException, IOException { + return deleteChangeMessage.apply(resource, new DeleteChangeMessageInput()); } } }
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteComment.java b/java/com/google/gerrit/server/restapi/change/DeleteComment.java index 95479a6..f915728 100644 --- a/java/com/google/gerrit/server/restapi/change/DeleteComment.java +++ b/java/com/google/gerrit/server/restapi/change/DeleteComment.java
@@ -23,6 +23,7 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 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.server.CommentsUtil; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.change.CommentResource; @@ -33,8 +34,6 @@ import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.time.TimeUtil; import com.google.inject.Inject; @@ -46,11 +45,11 @@ import org.eclipse.jgit.errors.ConfigInvalidException; @Singleton -public class DeleteComment - extends RetryingRestModifyView<CommentResource, DeleteCommentInput, CommentInfo> { +public class DeleteComment implements RestModifyView<CommentResource, DeleteCommentInput> { private final Provider<CurrentUser> userProvider; private final PermissionBackend permissionBackend; + private final BatchUpdate.Factory updateFactory; private final CommentsUtil commentsUtil; private final Provider<CommentJson> commentJson; private final ChangeNotes.Factory notesFactory; @@ -59,21 +58,20 @@ public DeleteComment( Provider<CurrentUser> userProvider, PermissionBackend permissionBackend, - RetryHelper retryHelper, + BatchUpdate.Factory updateFactory, CommentsUtil commentsUtil, Provider<CommentJson> commentJson, ChangeNotes.Factory notesFactory) { - super(retryHelper); this.userProvider = userProvider; this.permissionBackend = permissionBackend; + this.updateFactory = updateFactory; this.commentsUtil = commentsUtil; this.commentJson = commentJson; this.notesFactory = notesFactory; } @Override - public Response<CommentInfo> applyImpl( - BatchUpdate.Factory batchUpdateFactory, CommentResource rsrc, DeleteCommentInput input) + public Response<CommentInfo> apply(CommentResource rsrc, DeleteCommentInput input) throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException, UpdateException { CurrentUser user = userProvider.get(); @@ -86,8 +84,7 @@ String newMessage = getCommentNewMessage(user.asIdentifiedUser().getName(), input.reason); DeleteCommentOp deleteCommentOp = new DeleteCommentOp(rsrc, newMessage); try (BatchUpdate batchUpdate = - batchUpdateFactory.create( - rsrc.getRevisionResource().getProject(), user, TimeUtil.nowTs())) { + updateFactory.create(rsrc.getRevisionResource().getProject(), user, TimeUtil.nowTs())) { batchUpdate.addOp(rsrc.getRevisionResource().getChange().getId(), deleteCommentOp).execute(); }
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java index 9296988..89fc3b7 100644 --- a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java +++ b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
@@ -23,6 +23,7 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 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.server.CommentsUtil; import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.change.DraftCommentResource; @@ -31,8 +32,6 @@ import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.time.TimeUtil; import com.google.inject.Inject; @@ -41,28 +40,26 @@ import java.util.Optional; @Singleton -public class DeleteDraftComment - extends RetryingRestModifyView<DraftCommentResource, Input, CommentInfo> { - +public class DeleteDraftComment implements RestModifyView<DraftCommentResource, Input> { + private final BatchUpdate.Factory updateFactory; private final CommentsUtil commentsUtil; private final PatchSetUtil psUtil; private final PatchListCache patchListCache; @Inject DeleteDraftComment( + BatchUpdate.Factory updateFactory, CommentsUtil commentsUtil, PatchSetUtil psUtil, - RetryHelper retryHelper, PatchListCache patchListCache) { - super(retryHelper); + this.updateFactory = updateFactory; this.commentsUtil = commentsUtil; this.psUtil = psUtil; this.patchListCache = patchListCache; } @Override - protected Response<CommentInfo> applyImpl( - BatchUpdate.Factory updateFactory, DraftCommentResource rsrc, Input input) + public Response<CommentInfo> apply(DraftCommentResource rsrc, Input input) throws RestApiException, UpdateException { try (BatchUpdate bu = updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
diff --git a/java/com/google/gerrit/server/restapi/change/DeletePrivate.java b/java/com/google/gerrit/server/restapi/change/DeletePrivate.java index de7a683..16b7136 100644 --- a/java/com/google/gerrit/server/restapi/change/DeletePrivate.java +++ b/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
@@ -17,42 +17,41 @@ import static com.google.gerrit.extensions.conditions.BooleanCondition.or; import com.google.gerrit.common.Nullable; +import com.google.gerrit.extensions.common.InputWithMessage; import com.google.gerrit.extensions.conditions.BooleanCondition; 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.server.change.ChangeResource; import com.google.gerrit.server.change.SetPrivateOp; import com.google.gerrit.server.permissions.GlobalPermission; import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.update.BatchUpdate; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; 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; @Singleton -public class DeletePrivate - extends RetryingRestModifyView<ChangeResource, SetPrivateOp.Input, String> { +public class DeletePrivate implements RestModifyView<ChangeResource, InputWithMessage> { private final PermissionBackend permissionBackend; + private final BatchUpdate.Factory updateFactory; private final SetPrivateOp.Factory setPrivateOpFactory; @Inject DeletePrivate( - RetryHelper retryHelper, PermissionBackend permissionBackend, + BatchUpdate.Factory updateFactory, SetPrivateOp.Factory setPrivateOpFactory) { - super(retryHelper); this.permissionBackend = permissionBackend; + this.updateFactory = updateFactory; this.setPrivateOpFactory = setPrivateOpFactory; } @Override - protected Response<String> applyImpl( - BatchUpdate.Factory updateFactory, ChangeResource rsrc, @Nullable SetPrivateOp.Input input) + public Response<String> apply(ChangeResource rsrc, @Nullable InputWithMessage input) throws RestApiException, UpdateException { if (!canDeletePrivate(rsrc).value()) { throw new AuthException("not allowed to unmark private");
diff --git a/java/com/google/gerrit/server/restapi/change/DeletePrivateByPost.java b/java/com/google/gerrit/server/restapi/change/DeletePrivateByPost.java index c86d0ca..10feb63 100644 --- a/java/com/google/gerrit/server/restapi/change/DeletePrivateByPost.java +++ b/java/com/google/gerrit/server/restapi/change/DeletePrivateByPost.java
@@ -20,7 +20,7 @@ import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.change.SetPrivateOp; import com.google.gerrit.server.permissions.PermissionBackend; -import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.BatchUpdate; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -28,10 +28,10 @@ public class DeletePrivateByPost extends DeletePrivate implements UiAction<ChangeResource> { @Inject DeletePrivateByPost( - RetryHelper retryHelper, PermissionBackend permissionBackend, + BatchUpdate.Factory updateFactory, SetPrivateOp.Factory setPrivateOpFactory) { - super(retryHelper, permissionBackend, setPrivateOpFactory); + super(permissionBackend, updateFactory, setPrivateOpFactory); } @Override
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java b/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java index b98bb3b..3e4a483 100644 --- a/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java +++ b/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
@@ -19,39 +19,36 @@ import com.google.gerrit.extensions.api.changes.NotifyHandling; 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.server.change.DeleteReviewerByEmailOp; import com.google.gerrit.server.change.DeleteReviewerOp; import com.google.gerrit.server.change.NotifyResolver; import com.google.gerrit.server.change.ReviewerResource; import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; 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; @Singleton -public class DeleteReviewer - extends RetryingRestModifyView<ReviewerResource, DeleteReviewerInput, Object> { - +public class DeleteReviewer implements RestModifyView<ReviewerResource, DeleteReviewerInput> { + private final BatchUpdate.Factory updateFactory; private final DeleteReviewerOp.Factory deleteReviewerOpFactory; private final DeleteReviewerByEmailOp.Factory deleteReviewerByEmailOpFactory; @Inject DeleteReviewer( - RetryHelper retryHelper, + BatchUpdate.Factory updateFactory, DeleteReviewerOp.Factory deleteReviewerOpFactory, DeleteReviewerByEmailOp.Factory deleteReviewerByEmailOpFactory) { - super(retryHelper); + this.updateFactory = updateFactory; this.deleteReviewerOpFactory = deleteReviewerOpFactory; this.deleteReviewerByEmailOpFactory = deleteReviewerByEmailOpFactory; } @Override - protected Response<Object> applyImpl( - BatchUpdate.Factory updateFactory, ReviewerResource rsrc, DeleteReviewerInput input) + public Response<Object> apply(ReviewerResource rsrc, DeleteReviewerInput input) throws RestApiException, UpdateException { if (input == null) { input = new DeleteReviewerInput();
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVote.java b/java/com/google/gerrit/server/restapi/change/DeleteVote.java index 1193ad6..bdbf3f7 100644 --- a/java/com/google/gerrit/server/restapi/change/DeleteVote.java +++ b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
@@ -32,6 +32,7 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 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.server.ApprovalsUtil; import com.google.gerrit.server.ChangeMessagesUtil; import com.google.gerrit.server.IdentifiedUser; @@ -51,8 +52,6 @@ import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.server.update.Context; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.LabelVote; import com.google.gerrit.server.util.time.TimeUtil; @@ -64,9 +63,10 @@ import org.eclipse.jgit.errors.ConfigInvalidException; @Singleton -public class DeleteVote extends RetryingRestModifyView<VoteResource, DeleteVoteInput, Object> { +public class DeleteVote implements RestModifyView<VoteResource, DeleteVoteInput> { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private final BatchUpdate.Factory updateFactory; private final ApprovalsUtil approvalsUtil; private final PatchSetUtil psUtil; private final ChangeMessagesUtil cmUtil; @@ -79,7 +79,7 @@ @Inject DeleteVote( - RetryHelper retryHelper, + BatchUpdate.Factory updateFactory, ApprovalsUtil approvalsUtil, PatchSetUtil psUtil, ChangeMessagesUtil cmUtil, @@ -89,7 +89,7 @@ NotifyResolver notifyResolver, RemoveReviewerControl removeReviewerControl, ProjectCache projectCache) { - super(retryHelper); + this.updateFactory = updateFactory; this.approvalsUtil = approvalsUtil; this.psUtil = psUtil; this.cmUtil = cmUtil; @@ -102,8 +102,7 @@ } @Override - protected Response<Object> applyImpl( - BatchUpdate.Factory updateFactory, VoteResource rsrc, DeleteVoteInput input) + public Response<Object> apply(VoteResource rsrc, DeleteVoteInput input) throws RestApiException, UpdateException, IOException, ConfigInvalidException { if (input == null) { input = new DeleteVoteInput();
diff --git a/java/com/google/gerrit/server/restapi/change/GetDiff.java b/java/com/google/gerrit/server/restapi/change/GetDiff.java index 857205a..82bdc34 100644 --- a/java/com/google/gerrit/server/restapi/change/GetDiff.java +++ b/java/com/google/gerrit/server/restapi/change/GetDiff.java
@@ -14,26 +14,18 @@ package com.google.gerrit.server.restapi.change; -import static com.google.common.base.Preconditions.checkState; import static com.google.gerrit.util.cli.Localizable.localizable; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.PatchScript; -import com.google.gerrit.common.data.PatchScript.DisplayMethod; -import com.google.gerrit.entities.Patch; import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.Project; import com.google.gerrit.extensions.client.DiffPreferencesInfo; import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace; -import com.google.gerrit.extensions.common.ChangeType; import com.google.gerrit.extensions.common.DiffInfo; -import com.google.gerrit.extensions.common.DiffInfo.ContentEntry; -import com.google.gerrit.extensions.common.DiffInfo.FileMeta; -import com.google.gerrit.extensions.common.DiffInfo.IntraLineStatus; import com.google.gerrit.extensions.common.DiffWebLinkInfo; import com.google.gerrit.extensions.common.WebLinkInfo; import com.google.gerrit.extensions.restapi.AuthException; @@ -43,12 +35,12 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestReadView; -import com.google.gerrit.jgit.diff.ReplaceEdit; -import com.google.gerrit.prettify.common.SparseFileContent; import com.google.gerrit.server.WebLinks; -import com.google.gerrit.server.change.FileContentUtil; import com.google.gerrit.server.change.FileResource; import com.google.gerrit.server.change.RevisionResource; +import com.google.gerrit.server.diff.DiffInfoCreator; +import com.google.gerrit.server.diff.DiffSide; +import com.google.gerrit.server.diff.DiffWebLinksProvider; import com.google.gerrit.server.git.LargeObjectException; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.patch.PatchScriptFactory; @@ -59,10 +51,7 @@ import com.google.gerrit.server.project.ProjectState; import com.google.inject.Inject; import java.io.IOException; -import java.util.List; -import java.util.Set; import java.util.concurrent.TimeUnit; -import org.eclipse.jgit.diff.Edit; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.NamedOptionDef; @@ -73,16 +62,7 @@ import org.kohsuke.args4j.spi.Setter; public class GetDiff implements RestReadView<FileResource> { - private static final ImmutableMap<Patch.ChangeType, ChangeType> CHANGE_TYPE = - Maps.immutableEnumMap( - new ImmutableMap.Builder<Patch.ChangeType, ChangeType>() - .put(Patch.ChangeType.ADDED, ChangeType.ADDED) - .put(Patch.ChangeType.MODIFIED, ChangeType.MODIFIED) - .put(Patch.ChangeType.DELETED, ChangeType.DELETED) - .put(Patch.ChangeType.RENAMED, ChangeType.RENAMED) - .put(Patch.ChangeType.COPIED, ChangeType.COPIED) - .put(Patch.ChangeType.REWRITE, ChangeType.REWRITE) - .build()); + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private final ProjectCache projectCache; private final PatchScriptFactory.Factory patchScriptFactoryFactory; @@ -134,11 +114,17 @@ } prefs.context = context; prefs.intralineDifference = intraline; + logger.atFine().log( + "diff preferences: ignoreWhitespace = %s, context = %s, intralineDifference = %s", + prefs.ignoreWhitespace, prefs.context, prefs.intralineDifference); PatchScriptFactory psf; PatchSet basePatchSet = null; PatchSet.Id pId = resource.getPatchKey().patchSetId(); String fileName = resource.getPatchKey().fileName(); + logger.atFine().log( + "patchSetId = %d, fileName = %s, base = %s, parentNum = %d", + pId.get(), fileName, base, parentNum); ChangeNotes notes = resource.getRevision().getNotes(); if (base != null) { RevisionResource baseResource = @@ -155,105 +141,18 @@ psf.setLoadHistory(false); psf.setLoadComments(context != DiffPreferencesInfo.WHOLE_FILE_CONTEXT); PatchScript ps = psf.call(); - Content content = new Content(ps); - Set<Edit> editsDueToRebase = ps.getEditsDueToRebase(); - for (Edit edit : ps.getEdits()) { - if (edit.getType() == Edit.Type.EMPTY) { - continue; - } - content.addCommon(edit.getBeginA()); - - checkState( - content.nextA == edit.getBeginA(), - "nextA = %s; want %s", - content.nextA, - edit.getBeginA()); - checkState( - content.nextB == edit.getBeginB(), - "nextB = %s; want %s", - content.nextB, - edit.getBeginB()); - switch (edit.getType()) { - case DELETE: - case INSERT: - case REPLACE: - List<Edit> internalEdit = - edit instanceof ReplaceEdit ? ((ReplaceEdit) edit).getInternalEdits() : null; - boolean dueToRebase = editsDueToRebase.contains(edit); - content.addDiff(edit.getEndA(), edit.getEndB(), internalEdit, dueToRebase); - break; - case EMPTY: - default: - throw new IllegalStateException(); - } - } - content.addCommon(ps.getA().size()); - - ProjectState state = projectCache.get(resource.getRevision().getChange().getProject()); - - DiffInfo result = new DiffInfo(); - String revA = basePatchSet != null ? basePatchSet.refName() : content.commitIdA; - String revB = - resource.getRevision().getEdit().isPresent() - ? resource.getRevision().getEdit().get().getRefName() - : resource.getRevision().getPatchSet().refName(); - - ImmutableList<DiffWebLinkInfo> links = - webLinks.getDiffLinks( - state.getName(), - resource.getPatchKey().patchSetId().changeId().get(), - basePatchSet != null ? basePatchSet.id().get() : null, - revA, + Project.NameKey projectName = resource.getRevision().getChange().getProject(); + ProjectState state = projectCache.get(projectName); + DiffSide sideA = + DiffSide.create( + ps.getFileInfoA(), MoreObjects.firstNonNull(ps.getOldName(), ps.getNewName()), - resource.getPatchKey().patchSetId().get(), - revB, - ps.getNewName()); - result.webLinks = links.isEmpty() ? null : links; - - if (ps.isBinary()) { - result.binary = true; - } - if (ps.getDisplayMethodA() != DisplayMethod.NONE) { - result.metaA = new FileMeta(); - result.metaA.name = MoreObjects.firstNonNull(ps.getOldName(), ps.getNewName()); - result.metaA.contentType = - FileContentUtil.resolveContentType( - state, result.metaA.name, ps.getFileModeA(), ps.getMimeTypeA()); - result.metaA.lines = ps.getA().size(); - result.metaA.webLinks = getFileWebLinks(state.getProject(), revA, result.metaA.name); - result.metaA.commitId = content.commitIdA; - } - - if (ps.getDisplayMethodB() != DisplayMethod.NONE) { - result.metaB = new FileMeta(); - result.metaB.name = ps.getNewName(); - result.metaB.contentType = - FileContentUtil.resolveContentType( - state, result.metaB.name, ps.getFileModeB(), ps.getMimeTypeB()); - result.metaB.lines = ps.getB().size(); - result.metaB.webLinks = getFileWebLinks(state.getProject(), revB, result.metaB.name); - result.metaB.commitId = content.commitIdB; - } - - if (intraline) { - if (ps.hasIntralineTimeout()) { - result.intralineStatus = IntraLineStatus.TIMEOUT; - } else if (ps.hasIntralineFailure()) { - result.intralineStatus = IntraLineStatus.FAILURE; - } else { - result.intralineStatus = IntraLineStatus.OK; - } - } - - result.changeType = CHANGE_TYPE.get(ps.getChangeType()); - if (result.changeType == null) { - throw new IllegalStateException("unknown change type: " + ps.getChangeType()); - } - - if (ps.getPatchHeader().size() > 0) { - result.diffHeader = ps.getPatchHeader(); - } - result.content = content.lines; + DiffSide.Type.SIDE_A); + DiffSide sideB = DiffSide.create(ps.getFileInfoB(), ps.getNewName(), DiffSide.Type.SIDE_B); + DiffWebLinksProvider webLinksProvider = + new DiffWebLinksProviderImpl(sideA, sideB, projectName, basePatchSet, webLinks, resource); + DiffInfoCreator diffInfoCreator = new DiffInfoCreator(state, webLinksProvider, intraline); + DiffInfo result = diffInfoCreator.create(ps, sideA, sideB); Response<DiffInfo> r = Response.ok(result); if (resource.isCacheable()) { @@ -267,9 +166,69 @@ } } - private List<WebLinkInfo> getFileWebLinks(Project project, String rev, String file) { - ImmutableList<WebLinkInfo> links = webLinks.getFileLinks(project.getName(), rev, file); - return links.isEmpty() ? null : links; + private static class DiffWebLinksProviderImpl implements DiffWebLinksProvider { + + private final WebLinks webLinks; + private final Project.NameKey projectName; + private final DiffSide sideA; + private final DiffSide sideB; + private final String revA; + private final String revB; + private final FileResource resource; + @Nullable private final PatchSet basePatchSet; + + DiffWebLinksProviderImpl( + DiffSide sideA, + DiffSide sideB, + Project.NameKey projectName, + @Nullable PatchSet basePatchSet, + WebLinks webLinks, + FileResource resource) { + this.projectName = projectName; + this.webLinks = webLinks; + this.basePatchSet = basePatchSet; + this.resource = resource; + this.sideA = sideA; + this.sideB = sideB; + + revA = basePatchSet != null ? basePatchSet.refName() : sideA.fileInfo().commitId; + + RevisionResource revision = resource.getRevision(); + revB = + revision + .getEdit() + .map(edit -> edit.getRefName()) + .orElseGet(() -> revision.getPatchSet().refName()); + + logger.atFine().log("revA = %s, revB = %s", revA, revB); + } + + @Override + public ImmutableList<DiffWebLinkInfo> getDiffLinks() { + return webLinks.getDiffLinks( + projectName.get(), + resource.getPatchKey().patchSetId().changeId().get(), + basePatchSet != null ? basePatchSet.id().get() : null, + revA, + sideA.fileName(), + resource.getPatchKey().patchSetId().get(), + revB, + sideB.fileName()); + } + + @Override + public ImmutableList<WebLinkInfo> getFileWebLinks(DiffSide.Type type) { + String rev; + DiffSide side; + if (type == DiffSide.Type.SIDE_A) { + rev = revA; + side = sideA; + } else { + rev = revB; + side = sideB; + } + return webLinks.getFileLinks(projectName.get(), rev, side.fileName()); + } } public GetDiff setBase(String base) { @@ -297,110 +256,6 @@ return this; } - private static class Content { - final List<ContentEntry> lines; - final SparseFileContent fileA; - final SparseFileContent fileB; - final boolean ignoreWS; - final String commitIdA; - final String commitIdB; - - int nextA; - int nextB; - - Content(PatchScript ps) { - lines = Lists.newArrayListWithExpectedSize(ps.getEdits().size() + 2); - fileA = ps.getA(); - fileB = ps.getB(); - ignoreWS = ps.isIgnoreWhitespace(); - commitIdA = ps.getCommitIdA(); - commitIdB = ps.getCommitIdB(); - } - - void addCommon(int end) { - end = Math.min(end, fileA.size()); - if (nextA >= end) { - return; - } - - while (nextA < end) { - if (!fileA.contains(nextA)) { - int endRegion = Math.min(end, nextA == 0 ? fileA.first() : fileA.next(nextA - 1)); - int len = endRegion - nextA; - entry().skip = len; - nextA = endRegion; - nextB += len; - continue; - } - - ContentEntry e = null; - for (int i = nextA; i == nextA && i < end; i = fileA.next(i), nextA++, nextB++) { - if (ignoreWS && fileB.contains(nextB)) { - if (e == null || e.common == null) { - e = entry(); - e.a = Lists.newArrayListWithCapacity(end - nextA); - e.b = Lists.newArrayListWithCapacity(end - nextA); - e.common = true; - } - e.a.add(fileA.get(nextA)); - e.b.add(fileB.get(nextB)); - } else { - if (e == null || e.common != null) { - e = entry(); - e.ab = Lists.newArrayListWithCapacity(end - nextA); - } - e.ab.add(fileA.get(nextA)); - } - } - } - } - - void addDiff(int endA, int endB, List<Edit> internalEdit, boolean dueToRebase) { - int lenA = endA - nextA; - int lenB = endB - nextB; - checkState(lenA > 0 || lenB > 0); - - ContentEntry e = entry(); - if (lenA > 0) { - e.a = Lists.newArrayListWithCapacity(lenA); - for (; nextA < endA; nextA++) { - e.a.add(fileA.get(nextA)); - } - } - if (lenB > 0) { - e.b = Lists.newArrayListWithCapacity(lenB); - for (; nextB < endB; nextB++) { - e.b.add(fileB.get(nextB)); - } - } - if (internalEdit != null && !internalEdit.isEmpty()) { - e.editA = Lists.newArrayListWithCapacity(internalEdit.size() * 2); - e.editB = Lists.newArrayListWithCapacity(internalEdit.size() * 2); - int lastA = 0; - int lastB = 0; - for (Edit edit : internalEdit) { - if (edit.getBeginA() != edit.getEndA()) { - e.editA.add( - ImmutableList.of(edit.getBeginA() - lastA, edit.getEndA() - edit.getBeginA())); - lastA = edit.getEndA(); - } - if (edit.getBeginB() != edit.getEndB()) { - e.editB.add( - ImmutableList.of(edit.getBeginB() - lastB, edit.getEndB() - edit.getBeginB())); - lastB = edit.getEndB(); - } - } - } - e.dueToRebase = dueToRebase ? true : null; - } - - private ContentEntry entry() { - ContentEntry e = new ContentEntry(); - lines.add(e); - return e; - } - } - @Deprecated enum IgnoreWhitespace { NONE(DiffPreferencesInfo.Whitespace.IGNORE_NONE), @@ -416,6 +271,7 @@ } public static class ContextOptionHandler extends OptionHandler<Short> { + public ContextOptionHandler(CmdLineParser parser, OptionDef option, Setter<Short> setter) { super(parser, option, setter); }
diff --git a/java/com/google/gerrit/server/restapi/change/GetFixPreview.java b/java/com/google/gerrit/server/restapi/change/GetFixPreview.java new file mode 100644 index 0000000..0666756 --- /dev/null +++ b/java/com/google/gerrit/server/restapi/change/GetFixPreview.java
@@ -0,0 +1,142 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.restapi.change; + +import static java.util.stream.Collectors.groupingBy; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import com.google.gerrit.common.data.PatchScript; +import com.google.gerrit.entities.Change; +import com.google.gerrit.entities.FixReplacement; +import com.google.gerrit.entities.PatchSet; +import com.google.gerrit.extensions.client.DiffPreferencesInfo; +import com.google.gerrit.extensions.common.DiffInfo; +import com.google.gerrit.extensions.common.DiffWebLinkInfo; +import com.google.gerrit.extensions.common.WebLinkInfo; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestReadView; +import com.google.gerrit.server.change.FixResource; +import com.google.gerrit.server.diff.DiffInfoCreator; +import com.google.gerrit.server.diff.DiffSide; +import com.google.gerrit.server.diff.DiffSide.Type; +import com.google.gerrit.server.diff.DiffWebLinksProvider; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.LargeObjectException; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.patch.PatchScriptFactoryForAutoFix; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.project.InvalidChangeOperationException; +import com.google.gerrit.server.project.NoSuchChangeException; +import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.server.project.ProjectState; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.eclipse.jgit.lib.Repository; + +@Singleton +public class GetFixPreview implements RestReadView<FixResource> { + + private final ProjectCache projectCache; + private final GitRepositoryManager repoManager; + private final PatchScriptFactoryForAutoFix.Factory patchScriptFactoryFactory; + + @Inject + GetFixPreview( + ProjectCache projectCache, + GitRepositoryManager repoManager, + PatchScriptFactoryForAutoFix.Factory patchScriptFactoryFactory) { + this.projectCache = projectCache; + this.repoManager = repoManager; + this.patchScriptFactoryFactory = patchScriptFactoryFactory; + } + + @Override + public Response<Map<String, DiffInfo>> apply(FixResource resource) + throws PermissionBackendException, ResourceNotFoundException, ResourceConflictException, + AuthException, IOException, InvalidChangeOperationException { + Map<String, DiffInfo> result = new HashMap<>(); + PatchSet patchSet = resource.getRevisionResource().getPatchSet(); + ChangeNotes notes = resource.getRevisionResource().getNotes(); + Change change = notes.getChange(); + ProjectState state = projectCache.get(change.getProject()); + Map<String, List<FixReplacement>> fixReplacementsPerFilePath = + resource.getFixReplacements().stream() + .collect(groupingBy(fixReplacement -> fixReplacement.path)); + try { + try (Repository git = repoManager.openRepository(notes.getProjectName())) { + for (Map.Entry<String, List<FixReplacement>> entry : + fixReplacementsPerFilePath.entrySet()) { + String fileName = entry.getKey(); + DiffInfo diffInfo = + getFixPreviewForSingleFile( + git, patchSet, state, notes, fileName, ImmutableList.copyOf(entry.getValue())); + result.put(fileName, diffInfo); + } + } + } catch (NoSuchChangeException e) { + throw new ResourceNotFoundException(e.getMessage(), e); + } catch (LargeObjectException e) { + throw new ResourceConflictException(e.getMessage(), e); + } + return Response.ok(result); + } + + private DiffInfo getFixPreviewForSingleFile( + Repository git, + PatchSet patchSet, + ProjectState state, + ChangeNotes notes, + String fileName, + ImmutableList<FixReplacement> fixReplacements) + throws PermissionBackendException, AuthException, LargeObjectException, + InvalidChangeOperationException, IOException { + PatchScriptFactoryForAutoFix psf = + patchScriptFactoryFactory.create( + git, notes, fileName, patchSet, fixReplacements, DiffPreferencesInfo.defaults()); + PatchScript ps = psf.call(); + + DiffSide sideA = + DiffSide.create( + ps.getFileInfoA(), + MoreObjects.firstNonNull(ps.getOldName(), ps.getNewName()), + Type.SIDE_A); + DiffSide sideB = DiffSide.create(ps.getFileInfoB(), ps.getNewName(), DiffSide.Type.SIDE_B); + + DiffInfoCreator diffInfoCreator = + new DiffInfoCreator(state, new DiffWebLinksProviderImpl(), true); + return diffInfoCreator.create(ps, sideA, sideB); + } + + private static class DiffWebLinksProviderImpl implements DiffWebLinksProvider { + + @Override + public ImmutableList<DiffWebLinkInfo> getDiffLinks() { + return ImmutableList.of(); + } + + @Override + public ImmutableList<WebLinkInfo> getFileWebLinks(Type fileInfoType) { + return ImmutableList.of(); + } + } +}
diff --git a/java/com/google/gerrit/server/restapi/change/GetRelated.java b/java/com/google/gerrit/server/restapi/change/GetRelated.java index a846d50..5f075f6 100644 --- a/java/com/google/gerrit/server/restapi/change/GetRelated.java +++ b/java/com/google/gerrit/server/restapi/change/GetRelated.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.restapi.change; +import static com.google.common.base.Preconditions.checkArgument; import static java.util.stream.Collectors.toSet; import com.google.common.annotations.VisibleForTesting; @@ -55,17 +56,20 @@ private final PatchSetUtil psUtil; private final RelatedChangesSorter sorter; private final IndexConfig indexConfig; + private final ChangeData.Factory changeDataFactory; @Inject GetRelated( Provider<InternalChangeQuery> queryProvider, PatchSetUtil psUtil, RelatedChangesSorter sorter, - IndexConfig indexConfig) { + IndexConfig indexConfig, + ChangeData.Factory changeDataFactory) { this.queryProvider = queryProvider; this.psUtil = psUtil; this.sorter = sorter; this.indexConfig = indexConfig; + this.changeDataFactory = changeDataFactory; } @Override @@ -98,7 +102,7 @@ boolean isEdit = rsrc.getEdit().isPresent(); PatchSet basePs = isEdit ? rsrc.getEdit().get().getBasePatchSet() : rsrc.getPatchSet(); - reloadChangeIfStale(cds, basePs); + cds = reloadChangeIfStale(cds, rsrc.getChange(), basePs); for (RelatedChangesSorter.PatchSetData d : sorter.sort(cds, basePs)) { PatchSet ps = d.patchSet(); @@ -127,14 +131,30 @@ return psUtil.byChange(notes).stream().flatMap(ps -> ps.groups().stream()).collect(toSet()); } - private void reloadChangeIfStale(List<ChangeData> cds, PatchSet wantedPs) { - for (ChangeData cd : cds) { - if (cd.getId().equals(wantedPs.id().changeId())) { - if (cd.patchSet(wantedPs.id()) == null) { - cd.reloadChange(); - } - } + private List<ChangeData> reloadChangeIfStale( + List<ChangeData> changeDatasFromIndex, Change wantedChange, PatchSet wantedPs) { + checkArgument( + wantedChange.getId().equals(wantedPs.id().changeId()), + "change of wantedPs (%s) doesn't match wantedChange (%s)", + wantedPs.id().changeId(), + wantedChange.getId()); + + List<ChangeData> changeDatas = new ArrayList<>(changeDatasFromIndex.size() + 1); + changeDatas.addAll(changeDatasFromIndex); + + // Reload the change in case the patch set is absent. + changeDatas.stream() + .filter( + cd -> cd.getId().equals(wantedPs.id().changeId()) && cd.patchSet(wantedPs.id()) == null) + .forEach(ChangeData::reloadChange); + + if (changeDatas.stream().noneMatch(cd -> cd.getId().equals(wantedPs.id().changeId()))) { + // The change of the wanted patch set is missing in the result from the index. + // Load it from NoteDb and add it to the result. + changeDatas.add(changeDataFactory.create(wantedChange)); } + + return changeDatas; } static RelatedChangeAndCommitInfo newChangeAndCommit(
diff --git a/java/com/google/gerrit/server/restapi/change/Ignore.java b/java/com/google/gerrit/server/restapi/change/Ignore.java index 25cf311..a049e54 100644 --- a/java/com/google/gerrit/server/restapi/change/Ignore.java +++ b/java/com/google/gerrit/server/restapi/change/Ignore.java
@@ -60,7 +60,7 @@ if (!isIgnored(rsrc)) { stars.ignore(rsrc); } - return Response.ok(""); + return Response.ok(); } catch (MutuallyExclusiveLabelsException e) { throw new ResourceConflictException(e.getMessage()); }
diff --git a/java/com/google/gerrit/server/restapi/change/Index.java b/java/com/google/gerrit/server/restapi/change/Index.java index 5a17c07..5e17ae8 100644 --- a/java/com/google/gerrit/server/restapi/change/Index.java +++ b/java/com/google/gerrit/server/restapi/change/Index.java
@@ -17,33 +17,29 @@ import com.google.gerrit.extensions.common.Input; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.index.change.ChangeIndexer; import com.google.gerrit.server.permissions.GlobalPermission; import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.permissions.PermissionBackendException; -import com.google.gerrit.server.update.BatchUpdate; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.inject.Inject; import com.google.inject.Singleton; import java.io.IOException; @Singleton -public class Index extends RetryingRestModifyView<ChangeResource, Input, Object> { +public class Index implements RestModifyView<ChangeResource, Input> { private final PermissionBackend permissionBackend; private final ChangeIndexer indexer; @Inject - Index(RetryHelper retryHelper, PermissionBackend permissionBackend, ChangeIndexer indexer) { - super(retryHelper); + Index(PermissionBackend permissionBackend, ChangeIndexer indexer) { this.permissionBackend = permissionBackend; this.indexer = indexer; } @Override - protected Response<Object> applyImpl( - BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input) + public Response<Object> apply(ChangeResource rsrc, Input input) throws IOException, AuthException, PermissionBackendException { permissionBackend.currentUser().check(GlobalPermission.MAINTAIN_SERVER); indexer.index(rsrc.getChange());
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java b/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java index bfc9f12..099d0a6 100644 --- a/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java +++ b/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java
@@ -32,18 +32,19 @@ @Singleton public class ListChangeMessages implements RestReadView<ChangeResource> { private final ChangeMessagesUtil changeMessagesUtil; - private final AccountLoader accountLoader; + private final AccountLoader.Factory accountLoaderFactory; @Inject public ListChangeMessages( ChangeMessagesUtil changeMessagesUtil, AccountLoader.Factory accountLoaderFactory) { this.changeMessagesUtil = changeMessagesUtil; - this.accountLoader = accountLoaderFactory.create(true); + this.accountLoaderFactory = accountLoaderFactory; } @Override public Response<List<ChangeMessageInfo>> apply(ChangeResource resource) throws PermissionBackendException { + AccountLoader accountLoader = accountLoaderFactory.create(true); List<ChangeMessage> messages = changeMessagesUtil.byChange(resource.getNotes()); List<ChangeMessageInfo> messageInfos = messages.stream()
diff --git a/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java b/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java index 4c942d2..fa4555b 100644 --- a/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java +++ b/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java
@@ -54,7 +54,7 @@ public Response<String> apply(ChangeResource rsrc, Input input) throws RestApiException, IllegalLabelException { stars.markAsReviewed(rsrc); - return Response.ok(""); + return Response.ok(); } private boolean isReviewed(ChangeResource rsrc) {
diff --git a/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java b/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java index 5945b14..601fc4a 100644 --- a/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java +++ b/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java
@@ -52,7 +52,7 @@ @Override public Response<String> apply(ChangeResource rsrc, Input input) throws IllegalLabelException { stars.markAsUnreviewed(rsrc); - return Response.ok(""); + return Response.ok(); } private boolean isReviewed(ChangeResource rsrc) {
diff --git a/java/com/google/gerrit/server/restapi/change/Mergeable.java b/java/com/google/gerrit/server/restapi/change/Mergeable.java index 9b17ed6..cce8923 100644 --- a/java/com/google/gerrit/server/restapi/change/Mergeable.java +++ b/java/com/google/gerrit/server/restapi/change/Mergeable.java
@@ -17,7 +17,6 @@ import com.google.gerrit.common.data.SubmitTypeRecord; import com.google.gerrit.entities.Change; import com.google.gerrit.entities.PatchSet; -import com.google.gerrit.exceptions.StorageException; import com.google.gerrit.extensions.client.SubmitType; import com.google.gerrit.extensions.common.MergeableInfo; import com.google.gerrit.extensions.restapi.AuthException; @@ -133,10 +132,10 @@ return Response.ok(result); } - private SubmitType getSubmitType(ChangeData cd) { + private SubmitType getSubmitType(ChangeData cd) throws ResourceConflictException { SubmitTypeRecord rec = submitRuleEvaluator.getSubmitType(cd); if (rec.status != SubmitTypeRecord.Status.OK) { - throw new StorageException("Submit type rule failed: " + rec); + throw new ResourceConflictException("submit type rule error: " + rec.errorMessage); } return rec.type; }
diff --git a/java/com/google/gerrit/server/restapi/change/Module.java b/java/com/google/gerrit/server/restapi/change/Module.java index a57bd64..4409c6a 100644 --- a/java/com/google/gerrit/server/restapi/change/Module.java +++ b/java/com/google/gerrit/server/restapi/change/Module.java
@@ -96,6 +96,7 @@ post(CHANGE_KIND, "hashtags").to(PostHashtags.class); post(CHANGE_KIND, "restore").to(Restore.class); post(CHANGE_KIND, "revert").to(Revert.class); + post(CHANGE_KIND, "revert_submission").to(RevertSubmission.class); post(CHANGE_KIND, "submit").to(Submit.CurrentRevision.class); get(CHANGE_KIND, "submitted_together").to(SubmittedTogether.class); post(CHANGE_KIND, "rebase").to(Rebase.CurrentRevision.class); @@ -159,6 +160,7 @@ get(ROBOT_COMMENT_KIND).to(GetRobotComment.class); child(REVISION_KIND, "fixes").to(Fixes.class); post(FIX_KIND, "apply").to(ApplyFix.class); + get(FIX_KIND, "preview").to(GetFixPreview.class); child(REVISION_KIND, "files").to(Files.class); put(FILE_KIND, "reviewed").to(PutReviewed.class);
diff --git a/java/com/google/gerrit/server/restapi/change/Move.java b/java/com/google/gerrit/server/restapi/change/Move.java index 7d4c4d1..51c512f 100644 --- a/java/com/google/gerrit/server/restapi/change/Move.java +++ b/java/com/google/gerrit/server/restapi/change/Move.java
@@ -40,6 +40,7 @@ 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.ApprovalsUtil; import com.google.gerrit.server.ChangeMessagesUtil; @@ -59,8 +60,6 @@ import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.time.TimeUtil; import com.google.inject.Inject; @@ -76,11 +75,11 @@ import org.eclipse.jgit.revwalk.RevWalk; @Singleton -public class Move extends RetryingRestModifyView<ChangeResource, MoveInput, ChangeInfo> - implements UiAction<ChangeResource> { +public class Move implements RestModifyView<ChangeResource, MoveInput>, UiAction<ChangeResource> { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private final PermissionBackend permissionBackend; + private final BatchUpdate.Factory updateFactory; private final ChangeJson.Factory json; private final GitRepositoryManager repoManager; private final Provider<InternalChangeQuery> queryProvider; @@ -93,17 +92,17 @@ @Inject Move( PermissionBackend permissionBackend, + BatchUpdate.Factory updateFactory, ChangeJson.Factory json, GitRepositoryManager repoManager, Provider<InternalChangeQuery> queryProvider, ChangeMessagesUtil cmUtil, - RetryHelper retryHelper, PatchSetUtil psUtil, ApprovalsUtil approvalsUtil, ProjectCache projectCache, @GerritServerConfig Config gerritConfig) { - super(retryHelper); this.permissionBackend = permissionBackend; + this.updateFactory = updateFactory; this.json = json; this.repoManager = repoManager; this.queryProvider = queryProvider; @@ -115,8 +114,7 @@ } @Override - protected Response<ChangeInfo> applyImpl( - BatchUpdate.Factory updateFactory, ChangeResource rsrc, MoveInput input) + public Response<ChangeInfo> apply(ChangeResource rsrc, MoveInput input) throws RestApiException, UpdateException, PermissionBackendException, IOException { if (!moveEnabled) { // This will be removed with the above config once we reach consensus for the move change
diff --git a/java/com/google/gerrit/server/restapi/change/PostHashtags.java b/java/com/google/gerrit/server/restapi/change/PostHashtags.java index 516dead..c1a6a13 100644 --- a/java/com/google/gerrit/server/restapi/change/PostHashtags.java +++ b/java/com/google/gerrit/server/restapi/change/PostHashtags.java
@@ -18,14 +18,13 @@ import com.google.gerrit.extensions.api.changes.HashtagsInput; 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.change.ChangeResource; import com.google.gerrit.server.change.SetHashtagsOp; import com.google.gerrit.server.permissions.ChangePermission; import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.update.BatchUpdate; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.time.TimeUtil; import com.google.inject.Inject; @@ -33,19 +32,18 @@ @Singleton public class PostHashtags - extends RetryingRestModifyView<ChangeResource, HashtagsInput, ImmutableSortedSet<String>> - implements UiAction<ChangeResource> { + implements RestModifyView<ChangeResource, HashtagsInput>, UiAction<ChangeResource> { + private final BatchUpdate.Factory updateFactory; private final SetHashtagsOp.Factory hashtagsFactory; @Inject - PostHashtags(RetryHelper retryHelper, SetHashtagsOp.Factory hashtagsFactory) { - super(retryHelper); + PostHashtags(BatchUpdate.Factory updateFactory, SetHashtagsOp.Factory hashtagsFactory) { + this.updateFactory = updateFactory; this.hashtagsFactory = hashtagsFactory; } @Override - protected Response<ImmutableSortedSet<String>> applyImpl( - BatchUpdate.Factory updateFactory, ChangeResource req, HashtagsInput input) + public Response<ImmutableSortedSet<String>> apply(ChangeResource req, HashtagsInput input) throws RestApiException, UpdateException, PermissionBackendException { req.permissions().check(ChangePermission.EDIT_HASHTAGS);
diff --git a/java/com/google/gerrit/server/restapi/change/PostPrivate.java b/java/com/google/gerrit/server/restapi/change/PostPrivate.java index f008df3..f774457 100644 --- a/java/com/google/gerrit/server/restapi/change/PostPrivate.java +++ b/java/com/google/gerrit/server/restapi/change/PostPrivate.java
@@ -18,11 +18,13 @@ import static com.google.gerrit.extensions.conditions.BooleanCondition.or; import com.google.gerrit.entities.Change; +import com.google.gerrit.extensions.common.InputWithMessage; import com.google.gerrit.extensions.conditions.BooleanCondition; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.MethodNotAllowedException; 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.change.ChangeResource; import com.google.gerrit.server.change.SetPrivateOp; @@ -30,8 +32,6 @@ import com.google.gerrit.server.permissions.GlobalPermission; import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.update.BatchUpdate; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.time.TimeUtil; import com.google.inject.Inject; @@ -39,27 +39,27 @@ import org.eclipse.jgit.lib.Config; @Singleton -public class PostPrivate extends RetryingRestModifyView<ChangeResource, SetPrivateOp.Input, String> - implements UiAction<ChangeResource> { +public class PostPrivate + implements RestModifyView<ChangeResource, InputWithMessage>, UiAction<ChangeResource> { private final PermissionBackend permissionBackend; + private final BatchUpdate.Factory updateFactory; private final SetPrivateOp.Factory setPrivateOpFactory; private final boolean disablePrivateChanges; @Inject PostPrivate( - RetryHelper retryHelper, PermissionBackend permissionBackend, + BatchUpdate.Factory updateFactory, SetPrivateOp.Factory setPrivateOpFactory, @GerritServerConfig Config config) { - super(retryHelper); this.permissionBackend = permissionBackend; + this.updateFactory = updateFactory; this.setPrivateOpFactory = setPrivateOpFactory; this.disablePrivateChanges = config.getBoolean("change", null, "disablePrivateChanges", false); } @Override - public Response<String> applyImpl( - BatchUpdate.Factory updateFactory, ChangeResource rsrc, SetPrivateOp.Input input) + public Response<String> apply(ChangeResource rsrc, InputWithMessage input) throws RestApiException, UpdateException { if (disablePrivateChanges) { throw new MethodNotAllowedException("private changes are disabled"); @@ -70,7 +70,7 @@ } if (rsrc.getChange().isPrivate()) { - return Response.ok(""); + return Response.ok(); } SetPrivateOp op = setPrivateOpFactory.create(true, input); @@ -79,7 +79,7 @@ u.addOp(rsrc.getId(), op).execute(); } - return Response.created(""); + return Response.created(); } @Override
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java index 974a72c..6f10839 100644 --- a/java/com/google/gerrit/server/restapi/change/PostReview.java +++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -73,6 +73,7 @@ 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.restapi.Url; import com.google.gerrit.extensions.validators.CommentForValidation; @@ -120,8 +121,6 @@ import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.server.update.CommentsRejectedException; import com.google.gerrit.server.update.Context; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.LabelVote; import com.google.gerrit.server.util.time.TimeUtil; @@ -150,8 +149,7 @@ import org.eclipse.jgit.lib.ObjectId; @Singleton -public class PostReview - extends RetryingRestModifyView<RevisionResource, ReviewInput, ReviewResult> { +public class PostReview implements RestModifyView<RevisionResource, ReviewInput> { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private static final String ERROR_ADDING_REVIEWER = "error adding reviewer"; @@ -163,6 +161,7 @@ private static final Gson GSON = OutputFormat.JSON_COMPACT.newGson(); private static final int DEFAULT_ROBOT_COMMENT_SIZE_LIMIT_IN_BYTES = 1024 * 1024; + private final BatchUpdate.Factory updateFactory; private final ChangeResource.Factory changeResourceFactory; private final ChangeData.Factory changeDataFactory; private final ApprovalsUtil approvalsUtil; @@ -186,7 +185,7 @@ @Inject PostReview( - RetryHelper retryHelper, + BatchUpdate.Factory updateFactory, ChangeResource.Factory changeResourceFactory, ChangeData.Factory changeDataFactory, ApprovalsUtil approvalsUtil, @@ -206,7 +205,7 @@ ProjectCache projectCache, PermissionBackend permissionBackend, PluginSetContext<CommentValidator> commentValidators) { - super(retryHelper); + this.updateFactory = updateFactory; this.changeResourceFactory = changeResourceFactory; this.changeDataFactory = changeDataFactory; this.commentsUtil = commentsUtil; @@ -230,15 +229,13 @@ } @Override - protected Response<ReviewResult> applyImpl( - BatchUpdate.Factory updateFactory, RevisionResource revision, ReviewInput input) + public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input) throws RestApiException, UpdateException, IOException, PermissionBackendException, ConfigInvalidException, PatchListNotAvailableException { - return apply(updateFactory, revision, input, TimeUtil.nowTs()); + return apply(revision, input, TimeUtil.nowTs()); } - public Response<ReviewResult> apply( - BatchUpdate.Factory updateFactory, RevisionResource revision, ReviewInput input, Timestamp ts) + public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input, Timestamp ts) throws RestApiException, UpdateException, IOException, PermissionBackendException, ConfigInvalidException, PatchListNotAvailableException { // Respect timestamp, but truncate at change created-on time. @@ -249,6 +246,8 @@ ProjectState projectState = projectCache.checkedGet(revision.getProject()); LabelTypes labelTypes = projectState.getLabelTypes(revision.getNotes()); + logger.atFine().log("strict label checking is %s", (strictLabels ? "enabled" : "disabled")); + input.drafts = firstNonNull(input.drafts, DraftHandling.KEEP); logger.atFine().log("draft handling = %s", input.drafts); @@ -270,6 +269,7 @@ if (input.notify == null) { input.notify = defaultNotify(revision.getChange(), input); } + logger.atFine().log("notify handling = %s", input.notify); Map<String, AddReviewerResult> reviewerJsonResults = null; List<ReviewerAddition> reviewerResults = Lists.newArrayList(); @@ -282,13 +282,18 @@ reviewerAdder.prepare(revision.getNotes(), revision.getUser(), reviewerInput, true); reviewerJsonResults.put(reviewerInput.reviewer, result.result); if (result.result.error != null) { + logger.atFine().log( + "Adding %s as reviewer failed: %s", reviewerInput.reviewer, result.result.error); hasError = true; continue; } if (result.result.confirm != null) { + logger.atFine().log( + "Adding %s as reviewer requires confirmation", reviewerInput.reviewer); confirm = true; continue; } + logger.atFine().log("Adding %s as reviewer was prepared", reviewerInput.reviewer); reviewerResults.add(result); } } @@ -307,6 +312,9 @@ boolean ccOrReviewer = false; if (input.labels != null && !input.labels.isEmpty()) { ccOrReviewer = input.labels.values().stream().anyMatch(v -> v != 0); + if (ccOrReviewer) { + logger.atFine().log("calling user is cc/reviewer on the change due to voting on a label"); + } } if (!ccOrReviewer) { @@ -314,17 +322,22 @@ ReviewerSet currentReviewers = approvalsUtil.getReviewers(revision.getChangeResource().getNotes()); ccOrReviewer = currentReviewers.all().contains(id); + if (ccOrReviewer) { + logger.atFine().log("calling user is already cc/reviewer on the change"); + } } // Apply reviewer changes first. Revision emails should be sent to the // updated set of reviewers. Also keep track of whether the user added // themselves as a reviewer or to the CC list. + logger.atFine().log("adding reviewer additions"); for (ReviewerAddition reviewerResult : reviewerResults) { reviewerResult.op.suppressEmail(); // Send a single batch email below. bu.addOp(revision.getChange().getId(), reviewerResult.op); if (!ccOrReviewer && reviewerResult.result.reviewers != null) { for (ReviewerInfo reviewerInfo : reviewerResult.result.reviewers) { if (Objects.equals(id.get(), reviewerInfo._accountId)) { + logger.atFine().log("calling user is explicitly added as reviewer"); ccOrReviewer = true; break; } @@ -333,6 +346,7 @@ if (!ccOrReviewer && reviewerResult.result.ccs != null) { for (AccountInfo accountInfo : reviewerResult.result.ccs) { if (Objects.equals(id.get(), accountInfo._accountId)) { + logger.atFine().log("calling user is explicitly added as cc"); ccOrReviewer = true; break; } @@ -344,6 +358,7 @@ // User posting this review isn't currently in the reviewer or CC list, // isn't being explicitly added, and isn't voting on any label. // Automatically CC them on this change so they receive replies. + logger.atFine().log("CCing calling user"); ReviewerAddition selfAddition = reviewerAdder.ccCurrentUser(revision.getUser(), revision); selfAddition.op.suppressEmail(); bu.addOp(revision.getChange().getId(), selfAddition.op); @@ -365,6 +380,7 @@ output.ready = true; } + logger.atFine().log("setting work-in-progress to %s", input.workInProgress); WorkInProgressOp wipOp = workInProgressOpFactory.create(input.workInProgress, new WorkInProgressOp.Input()); wipOp.suppressEmail(); @@ -372,6 +388,7 @@ } // Add the review op. + logger.atFine().log("posting review"); bu.addOp( revision.getChange().getId(), new Op(projectState, revision.getPatchSet().id(), input)); @@ -455,6 +472,8 @@ private RevisionResource onBehalfOf(RevisionResource rev, LabelTypes labelTypes, ReviewInput in) throws BadRequestException, AuthException, UnprocessableEntityException, PermissionBackendException, IOException, ConfigInvalidException { + logger.atFine().log("request is executed on behalf of %s", in.onBehalfOf); + if (in.labels == null || in.labels.isEmpty()) { throw new AuthException( String.format("label required to post review on behalf of \"%s\"", in.onBehalfOf)); @@ -463,6 +482,8 @@ throw new AuthException("not allowed to modify other user's drafts"); } + logger.atFine().log("label input: %s", in.labels); + CurrentUser caller = rev.getUser(); PermissionBackend.ForChange perm = rev.permissions(); Iterator<Map.Entry<String, Short>> itr = in.labels.entrySet().iterator(); @@ -470,15 +491,22 @@ Map.Entry<String, Short> ent = itr.next(); LabelType type = labelTypes.byLabel(ent.getKey()); if (type == null) { + logger.atFine().log("label %s not found", ent.getKey()); if (strictLabels) { throw new BadRequestException( String.format("label \"%s\" is not a configured label", ent.getKey())); } + logger.atFine().log("ignoring input for unknown label %s", ent.getKey()); itr.remove(); continue; } - if (!caller.isInternalUser()) { + if (caller.isInternalUser()) { + logger.atFine().log( + "skipping on behalf of permission check for label %s" + + " because caller is an internal user", + type.getName()); + } else { try { perm.check(new LabelPermission.WithValue(ON_BEHALF_OF, type, ent.getValue())); } catch (AuthException e) { @@ -490,11 +518,13 @@ } } if (in.labels.isEmpty()) { + logger.atFine().log("labels are empty after unknown labels have been removed"); throw new AuthException( String.format("label required to post review on behalf of \"%s\"", in.onBehalfOf)); } IdentifiedUser reviewer = accountResolver.resolve(in.onBehalfOf).asUniqueUserOnBehalfOf(caller); + logger.atFine().log("on behalf of user was resolved to %s", reviewer.getLoggableName()); try { permissionBackend.user(reviewer).change(rev.getNotes()).check(ChangePermission.READ); } catch (AuthException e) { @@ -508,16 +538,20 @@ private void checkLabels(RevisionResource rsrc, LabelTypes labelTypes, Map<String, Short> labels) throws BadRequestException, AuthException, PermissionBackendException { + logger.atFine().log("checking label input: %s", labels); + PermissionBackend.ForChange perm = rsrc.permissions(); Iterator<Map.Entry<String, Short>> itr = labels.entrySet().iterator(); while (itr.hasNext()) { Map.Entry<String, Short> ent = itr.next(); LabelType lt = labelTypes.byLabel(ent.getKey()); if (lt == null) { + logger.atFine().log("label %s not found", ent.getKey()); if (strictLabels) { throw new BadRequestException( String.format("label \"%s\" is not a configured label", ent.getKey())); } + logger.atFine().log("ignoring input for unknown label %s", ent.getKey()); itr.remove(); continue; } @@ -529,10 +563,13 @@ } if (lt.getValue(ent.getValue()) == null) { + logger.atFine().log("label value %s not found", ent.getValue()); if (strictLabels) { throw new BadRequestException( String.format("label \"%s\": %d is not a valid value", ent.getKey(), ent.getValue())); } + logger.atFine().log( + "ignoring input for label %s because label value is unknown", ent.getKey()); itr.remove(); continue; } @@ -576,6 +613,7 @@ private <T extends CommentInput> void checkComments( RevisionResource revision, Map<String, List<T>> commentsPerPath) throws BadRequestException, PatchListNotAvailableException { + logger.atFine().log("checking comments"); Set<String> revisionFilePaths = getAffectedFilePaths(revision); for (Map.Entry<String, List<T>> entry : commentsPerPath.entrySet()) { String path = entry.getKey(); @@ -628,6 +666,7 @@ private void checkRobotComments( RevisionResource revision, Map<String, List<RobotCommentInput>> in) throws BadRequestException, PatchListNotAvailableException { + logger.atFine().log("checking robot comments"); for (Map.Entry<String, List<RobotCommentInput>> e : in.entrySet()) { String commentPath = e.getKey(); for (RobotCommentInput c : e.getValue()) {
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewers.java b/java/com/google/gerrit/server/restapi/change/PostReviewers.java index f74643c..e6a87e9 100644 --- a/java/com/google/gerrit/server/restapi/change/PostReviewers.java +++ b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
@@ -21,6 +21,7 @@ import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestCollectionModifyView; import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.change.NotifyResolver; import com.google.gerrit.server.change.ReviewerAdder; @@ -29,8 +30,6 @@ import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.update.BatchUpdate; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestCollectionModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.time.TimeUtil; import com.google.inject.Inject; @@ -40,28 +39,26 @@ @Singleton public class PostReviewers - extends RetryingRestCollectionModifyView< - ChangeResource, ReviewerResource, AddReviewerInput, AddReviewerResult> { - + implements RestCollectionModifyView<ChangeResource, ReviewerResource, AddReviewerInput> { + private final BatchUpdate.Factory updateFactory; private final ChangeData.Factory changeDataFactory; private final NotifyResolver notifyResolver; private final ReviewerAdder reviewerAdder; @Inject PostReviewers( + BatchUpdate.Factory updateFactory, ChangeData.Factory changeDataFactory, - RetryHelper retryHelper, NotifyResolver notifyResolver, ReviewerAdder reviewerAdder) { - super(retryHelper); + this.updateFactory = updateFactory; this.changeDataFactory = changeDataFactory; this.notifyResolver = notifyResolver; this.reviewerAdder = reviewerAdder; } @Override - protected Response<AddReviewerResult> applyImpl( - BatchUpdate.Factory updateFactory, ChangeResource rsrc, AddReviewerInput input) + public Response<AddReviewerResult> apply(ChangeResource rsrc, AddReviewerInput input) throws IOException, RestApiException, UpdateException, PermissionBackendException, ConfigInvalidException { if (input.reviewer == null) {
diff --git a/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java b/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java index 44f35a0..d76e53a 100644 --- a/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java +++ b/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java
@@ -21,6 +21,7 @@ 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.server.change.ChangeResource; import com.google.gerrit.server.change.NotifyResolver; import com.google.gerrit.server.edit.ChangeEdit; @@ -28,8 +29,6 @@ import com.google.gerrit.server.project.ContributorAgreementsChecker; import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.update.BatchUpdate; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -38,27 +37,26 @@ import org.eclipse.jgit.errors.ConfigInvalidException; @Singleton -public class PublishChangeEdit - extends RetryingRestModifyView<ChangeResource, PublishChangeEditInput, Object> { +public class PublishChangeEdit implements RestModifyView<ChangeResource, PublishChangeEditInput> { + private final BatchUpdate.Factory updateFactory; private final ChangeEditUtil editUtil; private final NotifyResolver notifyResolver; private final ContributorAgreementsChecker contributorAgreementsChecker; @Inject PublishChangeEdit( - RetryHelper retryHelper, + BatchUpdate.Factory updateFactory, ChangeEditUtil editUtil, NotifyResolver notifyResolver, ContributorAgreementsChecker contributorAgreementsChecker) { - super(retryHelper); + this.updateFactory = updateFactory; this.editUtil = editUtil; this.notifyResolver = notifyResolver; this.contributorAgreementsChecker = contributorAgreementsChecker; } @Override - protected Response<Object> applyImpl( - BatchUpdate.Factory updateFactory, ChangeResource rsrc, PublishChangeEditInput in) + public Response<Object> apply(ChangeResource rsrc, PublishChangeEditInput in) throws IOException, RestApiException, UpdateException, ConfigInvalidException, NoSuchProjectException { contributorAgreementsChecker.check(rsrc.getProject(), rsrc.getUser());
diff --git a/java/com/google/gerrit/server/restapi/change/PutAssignee.java b/java/com/google/gerrit/server/restapi/change/PutAssignee.java index dd84624..dc1adfa 100644 --- a/java/com/google/gerrit/server/restapi/change/PutAssignee.java +++ b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
@@ -24,6 +24,7 @@ import com.google.gerrit.extensions.restapi.BadRequestException; 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.ApprovalsUtil; import com.google.gerrit.server.IdentifiedUser; @@ -38,8 +39,6 @@ import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.update.BatchUpdate; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.time.TimeUtil; import com.google.inject.Inject; @@ -48,9 +47,10 @@ import org.eclipse.jgit.errors.ConfigInvalidException; @Singleton -public class PutAssignee extends RetryingRestModifyView<ChangeResource, AssigneeInput, AccountInfo> - implements UiAction<ChangeResource> { +public class PutAssignee + implements RestModifyView<ChangeResource, AssigneeInput>, UiAction<ChangeResource> { + private final BatchUpdate.Factory updateFactory; private final AccountResolver accountResolver; private final SetAssigneeOp.Factory assigneeFactory; private final ReviewerAdder reviewerAdder; @@ -60,14 +60,14 @@ @Inject PutAssignee( + BatchUpdate.Factory updateFactory, AccountResolver accountResolver, SetAssigneeOp.Factory assigneeFactory, - RetryHelper retryHelper, ReviewerAdder reviewerAdder, AccountLoader.Factory accountLoaderFactory, PermissionBackend permissionBackend, ApprovalsUtil approvalsUtil) { - super(retryHelper); + this.updateFactory = updateFactory; this.accountResolver = accountResolver; this.assigneeFactory = assigneeFactory; this.reviewerAdder = reviewerAdder; @@ -77,8 +77,7 @@ } @Override - protected Response<AccountInfo> applyImpl( - BatchUpdate.Factory updateFactory, ChangeResource rsrc, AssigneeInput input) + public Response<AccountInfo> apply(ChangeResource rsrc, AssigneeInput input) throws RestApiException, UpdateException, IOException, PermissionBackendException, ConfigInvalidException { rsrc.permissions().check(ChangePermission.EDIT_ASSIGNEE);
diff --git a/java/com/google/gerrit/server/restapi/change/PutDescription.java b/java/com/google/gerrit/server/restapi/change/PutDescription.java index 451d010..d84ab3e 100644 --- a/java/com/google/gerrit/server/restapi/change/PutDescription.java +++ b/java/com/google/gerrit/server/restapi/change/PutDescription.java
@@ -20,6 +20,7 @@ import com.google.gerrit.extensions.common.DescriptionInput; 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.ChangeMessagesUtil; import com.google.gerrit.server.PatchSetUtil; @@ -30,8 +31,6 @@ import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.time.TimeUtil; import com.google.inject.Inject; @@ -39,21 +38,21 @@ @Singleton public class PutDescription - extends RetryingRestModifyView<RevisionResource, DescriptionInput, String> - implements UiAction<RevisionResource> { + implements RestModifyView<RevisionResource, DescriptionInput>, UiAction<RevisionResource> { + private final BatchUpdate.Factory updateFactory; private final ChangeMessagesUtil cmUtil; private final PatchSetUtil psUtil; @Inject - PutDescription(ChangeMessagesUtil cmUtil, RetryHelper retryHelper, PatchSetUtil psUtil) { - super(retryHelper); + PutDescription( + BatchUpdate.Factory updateFactory, ChangeMessagesUtil cmUtil, PatchSetUtil psUtil) { + this.updateFactory = updateFactory; this.cmUtil = cmUtil; this.psUtil = psUtil; } @Override - protected Response<String> applyImpl( - BatchUpdate.Factory updateFactory, RevisionResource rsrc, DescriptionInput input) + public Response<String> apply(RevisionResource rsrc, DescriptionInput input) throws UpdateException, RestApiException, PermissionBackendException { rsrc.permissions().check(ChangePermission.EDIT_DESCRIPTION);
diff --git a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java index 5696fcb..63cd7a3 100644 --- a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java +++ b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
@@ -24,6 +24,7 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 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.Url; import com.google.gerrit.server.CommentsUtil; import com.google.gerrit.server.PatchSetUtil; @@ -35,8 +36,6 @@ import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.time.TimeUtil; import com.google.inject.Inject; @@ -47,9 +46,8 @@ import java.util.Optional; @Singleton -public class PutDraftComment - extends RetryingRestModifyView<DraftCommentResource, DraftInput, CommentInfo> { - +public class PutDraftComment implements RestModifyView<DraftCommentResource, DraftInput> { + private final BatchUpdate.Factory updateFactory; private final DeleteDraftComment delete; private final CommentsUtil commentsUtil; private final PatchSetUtil psUtil; @@ -58,13 +56,13 @@ @Inject PutDraftComment( + BatchUpdate.Factory updateFactory, DeleteDraftComment delete, CommentsUtil commentsUtil, PatchSetUtil psUtil, - RetryHelper retryHelper, Provider<CommentJson> commentJson, PatchListCache patchListCache) { - super(retryHelper); + this.updateFactory = updateFactory; this.delete = delete; this.commentsUtil = commentsUtil; this.psUtil = psUtil; @@ -73,11 +71,10 @@ } @Override - protected Response<CommentInfo> applyImpl( - BatchUpdate.Factory updateFactory, DraftCommentResource rsrc, DraftInput in) + public Response<CommentInfo> apply(DraftCommentResource rsrc, DraftInput in) throws RestApiException, UpdateException, PermissionBackendException { if (in == null || in.message == null || in.message.trim().isEmpty()) { - return delete.applyImpl(updateFactory, rsrc, null); + return delete.apply(rsrc, null); } else if (in.id != null && !rsrc.getId().equals(in.id)) { throw new BadRequestException("id must match URL"); } else if (in.line != null && in.line < 0) {
diff --git a/java/com/google/gerrit/server/restapi/change/PutMessage.java b/java/com/google/gerrit/server/restapi/change/PutMessage.java index acda547..4761d0c 100644 --- a/java/com/google/gerrit/server/restapi/change/PutMessage.java +++ b/java/com/google/gerrit/server/restapi/change/PutMessage.java
@@ -24,6 +24,7 @@ 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.server.ChangeUtil; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.GerritPersonIdent; @@ -38,8 +39,6 @@ import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.update.BatchUpdate; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.CommitMessageUtil; import com.google.gerrit.server.util.time.TimeUtil; @@ -61,8 +60,9 @@ import org.eclipse.jgit.revwalk.RevWalk; @Singleton -public class PutMessage extends RetryingRestModifyView<ChangeResource, CommitMessageInput, String> { +public class PutMessage implements RestModifyView<ChangeResource, CommitMessageInput> { + private final BatchUpdate.Factory updateFactory; private final GitRepositoryManager repositoryManager; private final Provider<CurrentUser> userProvider; private final TimeZone tz; @@ -74,7 +74,7 @@ @Inject PutMessage( - RetryHelper retryHelper, + BatchUpdate.Factory updateFactory, GitRepositoryManager repositoryManager, Provider<CurrentUser> userProvider, PatchSetInserter.Factory psInserterFactory, @@ -83,7 +83,7 @@ PatchSetUtil psUtil, NotifyResolver notifyResolver, ProjectCache projectCache) { - super(retryHelper); + this.updateFactory = updateFactory; this.repositoryManager = repositoryManager; this.userProvider = userProvider; this.psInserterFactory = psInserterFactory; @@ -95,8 +95,7 @@ } @Override - protected Response<String> applyImpl( - BatchUpdate.Factory updateFactory, ChangeResource resource, CommitMessageInput input) + public Response<String> apply(ChangeResource resource, CommitMessageInput input) throws IOException, RestApiException, UpdateException, PermissionBackendException, ConfigInvalidException { PatchSet ps = psUtil.current(resource.getNotes());
diff --git a/java/com/google/gerrit/server/restapi/change/PutTopic.java b/java/com/google/gerrit/server/restapi/change/PutTopic.java index cfeb884..f673bfc 100644 --- a/java/com/google/gerrit/server/restapi/change/PutTopic.java +++ b/java/com/google/gerrit/server/restapi/change/PutTopic.java
@@ -21,6 +21,7 @@ import com.google.gerrit.extensions.restapi.BadRequestException; 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.ChangeMessagesUtil; import com.google.gerrit.server.ChangeUtil; @@ -33,29 +34,27 @@ import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.server.update.Context; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; 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; @Singleton -public class PutTopic extends RetryingRestModifyView<ChangeResource, TopicInput, String> - implements UiAction<ChangeResource> { +public class PutTopic + implements RestModifyView<ChangeResource, TopicInput>, UiAction<ChangeResource> { + private final BatchUpdate.Factory updateFactory; private final ChangeMessagesUtil cmUtil; private final TopicEdited topicEdited; @Inject - PutTopic(ChangeMessagesUtil cmUtil, RetryHelper retryHelper, TopicEdited topicEdited) { - super(retryHelper); + PutTopic(BatchUpdate.Factory updateFactory, ChangeMessagesUtil cmUtil, TopicEdited topicEdited) { + this.updateFactory = updateFactory; this.cmUtil = cmUtil; this.topicEdited = topicEdited; } @Override - protected Response<String> applyImpl( - BatchUpdate.Factory updateFactory, ChangeResource req, TopicInput input) + public Response<String> apply(ChangeResource req, TopicInput input) throws UpdateException, RestApiException, PermissionBackendException { req.permissions().check(ChangePermission.EDIT_TOPIC_NAME);
diff --git a/java/com/google/gerrit/server/restapi/change/QueryChanges.java b/java/com/google/gerrit/server/restapi/change/QueryChanges.java index 50e1e42..544177f 100644 --- a/java/com/google/gerrit/server/restapi/change/QueryChanges.java +++ b/java/com/google/gerrit/server/restapi/change/QueryChanges.java
@@ -34,9 +34,11 @@ import com.google.gerrit.server.query.change.ChangeQueryBuilder; import com.google.gerrit.server.query.change.ChangeQueryProcessor; import com.google.inject.Inject; +import com.google.inject.Provider; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; +import java.util.HashMap; import java.util.List; import org.kohsuke.args4j.Option; @@ -45,8 +47,12 @@ private final ChangeJson.Factory json; private final ChangeQueryBuilder qb; - private final ChangeQueryProcessor imp; + private final Provider<ChangeQueryProcessor> queryProcessorProvider; + private final HashMap<String, DynamicOptions.DynamicBean> dynamicBeans = new HashMap<>(); private EnumSet<ListChangesOption> options; + private Integer limit; + private Integer start; + private Boolean noLimit; @Option( name = "--query", @@ -61,7 +67,7 @@ metaVar = "CNT", usage = "Maximum number of results to return") public void setLimit(int limit) { - imp.setUserProvidedLimit(limit); + this.limit = limit; } @Option(name = "-o", usage = "Output options per change") @@ -80,24 +86,27 @@ metaVar = "CNT", usage = "Number of changes to skip") public void setStart(int start) { - imp.setStart(start); + this.start = start; } @Option(name = "--no-limit", usage = "Return all results, overriding the default limit") public void setNoLimit(boolean on) { - imp.setNoLimit(on); + this.noLimit = on; } @Override public void setDynamicBean(String plugin, DynamicOptions.DynamicBean dynamicBean) { - imp.setDynamicBean(plugin, dynamicBean); + dynamicBeans.put(plugin, dynamicBean); } @Inject - QueryChanges(ChangeJson.Factory json, ChangeQueryBuilder qb, ChangeQueryProcessor qp) { + QueryChanges( + ChangeJson.Factory json, + ChangeQueryBuilder qb, + Provider<ChangeQueryProcessor> queryProcessorProvider) { this.json = json; this.qb = qb; - this.imp = qp; + this.queryProcessorProvider = queryProcessorProvider; options = EnumSet.noneOf(ListChangesOption.class); } @@ -129,9 +138,22 @@ } private List<List<ChangeInfo>> query() throws QueryParseException, PermissionBackendException { - if (imp.isDisabled()) { + ChangeQueryProcessor queryProcessor = queryProcessorProvider.get(); + if (queryProcessor.isDisabled()) { throw new QueryParseException("query disabled"); } + + if (limit != null) { + queryProcessor.setUserProvidedLimit(limit); + } + if (start != null) { + queryProcessor.setStart(start); + } + if (noLimit != null) { + queryProcessor.setNoLimit(noLimit); + } + dynamicBeans.forEach((p, b) -> queryProcessor.setDynamicBean(p, b)); + if (queries == null || queries.isEmpty()) { queries = Collections.singletonList("status:open"); } else if (queries.size() > 10) { @@ -141,9 +163,9 @@ } int cnt = queries.size(); - List<QueryResult<ChangeData>> results = imp.query(qb.parse(queries)); + List<QueryResult<ChangeData>> results = queryProcessor.query(qb.parse(queries)); List<List<ChangeInfo>> res = - json.create(options, this.imp.getAttributesFactory()).format(results); + json.create(options, queryProcessor.getAttributesFactory()).format(results); for (int n = 0; n < cnt; n++) { List<ChangeInfo> info = res.get(n); if (results.get(n).more() && !info.isEmpty()) {
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java index af8f971..7a9136b 100644 --- a/java/com/google/gerrit/server/restapi/change/Rebase.java +++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -47,8 +47,6 @@ 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.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.time.TimeUtil; import com.google.inject.Inject; @@ -63,13 +61,14 @@ import org.eclipse.jgit.revwalk.RevWalk; @Singleton -public class Rebase extends RetryingRestModifyView<RevisionResource, RebaseInput, ChangeInfo> +public class Rebase implements RestModifyView<RevisionResource, RebaseInput>, UiAction<RevisionResource> { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private static final ImmutableSet<ListChangesOption> OPTIONS = Sets.immutableEnumSet(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT); + private final BatchUpdate.Factory updateFactory; private final GitRepositoryManager repoManager; private final RebaseChangeOp.Factory rebaseFactory; private final RebaseUtil rebaseUtil; @@ -80,7 +79,7 @@ @Inject public Rebase( - RetryHelper retryHelper, + BatchUpdate.Factory updateFactory, GitRepositoryManager repoManager, RebaseChangeOp.Factory rebaseFactory, RebaseUtil rebaseUtil, @@ -88,7 +87,7 @@ PermissionBackend permissionBackend, ProjectCache projectCache, PatchSetUtil patchSetUtil) { - super(retryHelper); + this.updateFactory = updateFactory; this.repoManager = repoManager; this.rebaseFactory = rebaseFactory; this.rebaseUtil = rebaseUtil; @@ -99,8 +98,7 @@ } @Override - protected Response<ChangeInfo> applyImpl( - BatchUpdate.Factory updateFactory, RevisionResource rsrc, RebaseInput input) + 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()); @@ -260,28 +258,24 @@ return description; } - public static class CurrentRevision - extends RetryingRestModifyView<ChangeResource, RebaseInput, ChangeInfo> { + public static class CurrentRevision implements RestModifyView<ChangeResource, RebaseInput> { private final PatchSetUtil psUtil; private final Rebase rebase; @Inject - CurrentRevision(RetryHelper retryHelper, PatchSetUtil psUtil, Rebase rebase) { - super(retryHelper); + CurrentRevision(PatchSetUtil psUtil, Rebase rebase) { this.psUtil = psUtil; this.rebase = rebase; } @Override - protected Response<ChangeInfo> applyImpl( - BatchUpdate.Factory updateFactory, ChangeResource rsrc, RebaseInput input) - throws Exception { + public Response<ChangeInfo> apply(ChangeResource rsrc, RebaseInput input) + throws RestApiException, UpdateException, IOException, PermissionBackendException { PatchSet ps = psUtil.current(rsrc.getNotes()); if (ps == null) { throw new ResourceConflictException("current revision is missing"); } - return Response.ok( - rebase.applyImpl(updateFactory, new RevisionResource(rsrc, ps), input).value()); + return Response.ok(rebase.apply(new RevisionResource(rsrc, ps), input).value()); } } }
diff --git a/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java b/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java index 7be8765..9fb8de8 100644 --- a/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java +++ b/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java
@@ -19,37 +19,30 @@ 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.RestModifyView; import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.edit.ChangeEditModifier; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.InvalidChangeOperationException; -import com.google.gerrit.server.update.BatchUpdate; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.inject.Inject; import com.google.inject.Singleton; import java.io.IOException; import org.eclipse.jgit.lib.Repository; @Singleton -public class RebaseChangeEdit extends RetryingRestModifyView<ChangeResource, Input, Object> { +public class RebaseChangeEdit implements RestModifyView<ChangeResource, Input> { private final GitRepositoryManager repositoryManager; private final ChangeEditModifier editModifier; @Inject - RebaseChangeEdit( - RetryHelper retryHelper, - GitRepositoryManager repositoryManager, - ChangeEditModifier editModifier) { - super(retryHelper); + RebaseChangeEdit(GitRepositoryManager repositoryManager, ChangeEditModifier editModifier) { this.repositoryManager = repositoryManager; this.editModifier = editModifier; } @Override - protected Response<Object> applyImpl( - BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input in) + public Response<Object> apply(ChangeResource rsrc, Input in) throws AuthException, ResourceConflictException, IOException, PermissionBackendException { Project.NameKey project = rsrc.getProject(); try (Repository repository = repositoryManager.openRepository(project)) {
diff --git a/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java b/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java index 8040847..af65483 100644 --- a/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java +++ b/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
@@ -16,6 +16,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toMap; import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; @@ -76,7 +77,15 @@ // Map of all patch sets, keyed by commit SHA-1. Map<ObjectId, PatchSetData> byId = collectById(in); PatchSetData start = byId.get(startPs.commitId()); - checkArgument(start != null, "%s not found in %s", startPs, in); + 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())))); // Map of patch set -> immediate parent. ListMultimap<PatchSetData, PatchSetData> parents =
diff --git a/java/com/google/gerrit/server/restapi/change/Restore.java b/java/com/google/gerrit/server/restapi/change/Restore.java index 679d4f8..54575bb 100644 --- a/java/com/google/gerrit/server/restapi/change/Restore.java +++ b/java/com/google/gerrit/server/restapi/change/Restore.java
@@ -26,6 +26,7 @@ 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.ChangeMessagesUtil; import com.google.gerrit.server.ChangeUtil; @@ -43,8 +44,6 @@ import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.server.update.Context; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.time.TimeUtil; import com.google.inject.Inject; @@ -52,10 +51,11 @@ import java.io.IOException; @Singleton -public class Restore extends RetryingRestModifyView<ChangeResource, RestoreInput, ChangeInfo> - implements UiAction<ChangeResource> { +public class Restore + implements RestModifyView<ChangeResource, RestoreInput>, UiAction<ChangeResource> { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private final BatchUpdate.Factory updateFactory; private final RestoredSender.Factory restoredSenderFactory; private final ChangeJson.Factory json; private final ChangeMessagesUtil cmUtil; @@ -65,14 +65,14 @@ @Inject Restore( + BatchUpdate.Factory updateFactory, RestoredSender.Factory restoredSenderFactory, ChangeJson.Factory json, ChangeMessagesUtil cmUtil, PatchSetUtil psUtil, - RetryHelper retryHelper, ChangeRestored changeRestored, ProjectCache projectCache) { - super(retryHelper); + this.updateFactory = updateFactory; this.restoredSenderFactory = restoredSenderFactory; this.json = json; this.cmUtil = cmUtil; @@ -82,8 +82,7 @@ } @Override - protected Response<ChangeInfo> applyImpl( - BatchUpdate.Factory updateFactory, ChangeResource rsrc, RestoreInput input) + public Response<ChangeInfo> apply(ChangeResource rsrc, RestoreInput input) throws RestApiException, UpdateException, PermissionBackendException, IOException { // Not allowed to restore if the current patch set is locked. psUtil.checkPatchSetNotLocked(rsrc.getNotes());
diff --git a/java/com/google/gerrit/server/restapi/change/Revert.java b/java/com/google/gerrit/server/restapi/change/Revert.java index dad87e5..d0b2562 100644 --- a/java/com/google/gerrit/server/restapi/change/Revert.java +++ b/java/com/google/gerrit/server/restapi/change/Revert.java
@@ -31,6 +31,7 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 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.ApprovalsUtil; import com.google.gerrit.server.ChangeMessagesUtil; @@ -59,8 +60,6 @@ import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.server.update.Context; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.time.TimeUtil; import com.google.inject.Inject; @@ -79,11 +78,12 @@ import org.eclipse.jgit.revwalk.RevWalk; @Singleton -public class Revert extends RetryingRestModifyView<ChangeResource, RevertInput, ChangeInfo> - implements UiAction<ChangeResource> { +public class Revert + implements RestModifyView<ChangeResource, RevertInput>, UiAction<ChangeResource> { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private final PermissionBackend permissionBackend; + private final BatchUpdate.Factory updateFactory; private final GitRepositoryManager repoManager; private final ChangeInserter.Factory changeInserterFactory; private final ChangeMessagesUtil cmUtil; @@ -101,10 +101,10 @@ @Inject Revert( PermissionBackend permissionBackend, + BatchUpdate.Factory updateFactory, GitRepositoryManager repoManager, ChangeInserter.Factory changeInserterFactory, ChangeMessagesUtil cmUtil, - RetryHelper retryHelper, Sequences seq, PatchSetUtil psUtil, RevertedSender.Factory revertedSenderFactory, @@ -115,8 +115,8 @@ ProjectCache projectCache, NotifyResolver notifyResolver, CommitUtil commitUtil) { - super(retryHelper); this.permissionBackend = permissionBackend; + this.updateFactory = updateFactory; this.repoManager = repoManager; this.changeInserterFactory = changeInserterFactory; this.cmUtil = cmUtil; @@ -133,8 +133,7 @@ } @Override - public Response<ChangeInfo> applyImpl( - BatchUpdate.Factory updateFactory, ChangeResource rsrc, RevertInput input) + public Response<ChangeInfo> apply(ChangeResource rsrc, RevertInput input) throws IOException, RestApiException, UpdateException, NoSuchChangeException, PermissionBackendException, NoSuchProjectException, ConfigInvalidException { Change change = rsrc.getChange();
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java new file mode 100644 index 0000000..a7d19d1 --- /dev/null +++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -0,0 +1,598 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.restapi.change; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.gerrit.extensions.conditions.BooleanCondition.and; +import static com.google.gerrit.server.permissions.RefPermission.CREATE_CHANGE; +import static java.util.Objects.requireNonNull; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.entities.BranchNameKey; +import com.google.gerrit.entities.Change; +import com.google.gerrit.entities.ChangeMessage; +import com.google.gerrit.entities.PatchSet; +import com.google.gerrit.entities.Project; +import com.google.gerrit.entities.RefNames; +import com.google.gerrit.exceptions.StorageException; +import com.google.gerrit.extensions.api.changes.CherryPickInput; +import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.RevertInput; +import com.google.gerrit.extensions.common.ChangeInfo; +import com.google.gerrit.extensions.common.RevertSubmissionInfo; +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.ChangeMessagesUtil; +import com.google.gerrit.server.ChangeUtil; +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.ChangeMessages; +import com.google.gerrit.server.change.ChangeResource; +import com.google.gerrit.server.change.NotifyResolver; +import com.google.gerrit.server.change.WalkSorter; +import com.google.gerrit.server.change.WalkSorter.PatchSetData; +import com.google.gerrit.server.extensions.events.ChangeReverted; +import com.google.gerrit.server.git.CommitUtil; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.mail.send.RevertedSender; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.notedb.Sequences; +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.ContributorAgreementsChecker; +import com.google.gerrit.server.project.NoSuchProjectException; +import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gerrit.server.query.change.InternalChangeQuery; +import com.google.gerrit.server.restapi.change.CherryPickChange.Result; +import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.BatchUpdateOp; +import com.google.gerrit.server.update.ChangeContext; +import com.google.gerrit.server.update.Context; +import com.google.gerrit.server.update.UpdateException; +import com.google.gerrit.server.util.time.TimeUtil; +import com.google.inject.Inject; +import com.google.inject.Provider; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.lang.RandomStringUtils; +import org.eclipse.jgit.errors.ConfigInvalidException; +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.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; + +public class RevertSubmission + implements RestModifyView<ChangeResource, RevertInput>, UiAction<ChangeResource> { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private final Provider<InternalChangeQuery> queryProvider; + private final Provider<CurrentUser> user; + private final PermissionBackend permissionBackend; + private final ProjectCache projectCache; + private final PatchSetUtil psUtil; + private final ContributorAgreementsChecker contributorAgreements; + private final CherryPickChange cherryPickChange; + private final ChangeJson.Factory json; + private final GitRepositoryManager repoManager; + private final WalkSorter sorter; + private final ChangeMessagesUtil cmUtil; + private final CommitUtil commitUtil; + private final ChangeNotes.Factory changeNotesFactory; + private final ChangeResource.Factory changeResourceFactory; + private final ChangeReverted changeReverted; + private final RevertedSender.Factory revertedSenderFactory; + private final Sequences seq; + private final NotifyResolver notifyResolver; + private final Revert revert; + private final BatchUpdate.Factory updateFactory; + + private CherryPickInput cherryPickInput; + private List<ChangeInfo> results; + + @Inject + RevertSubmission( + Provider<InternalChangeQuery> queryProvider, + Provider<CurrentUser> user, + PermissionBackend permissionBackend, + ProjectCache projectCache, + PatchSetUtil psUtil, + ContributorAgreementsChecker contributorAgreements, + CherryPickChange cherryPickChange, + ChangeJson.Factory json, + GitRepositoryManager repoManager, + WalkSorter sorter, + ChangeMessagesUtil cmUtil, + CommitUtil commitUtil, + ChangeNotes.Factory changeNotesFactory, + ChangeResource.Factory changeResourceFactory, + ChangeReverted changeReverted, + RevertedSender.Factory revertedSenderFactory, + Sequences seq, + NotifyResolver notifyResolver, + Revert revert, + BatchUpdate.Factory updateFactory) { + this.queryProvider = queryProvider; + this.user = user; + this.permissionBackend = permissionBackend; + this.projectCache = projectCache; + this.psUtil = psUtil; + this.contributorAgreements = contributorAgreements; + this.cherryPickChange = cherryPickChange; + this.json = json; + this.repoManager = repoManager; + this.sorter = sorter; + this.cmUtil = cmUtil; + this.commitUtil = commitUtil; + this.changeNotesFactory = changeNotesFactory; + this.changeResourceFactory = changeResourceFactory; + this.changeReverted = changeReverted; + this.revertedSenderFactory = revertedSenderFactory; + this.seq = seq; + this.notifyResolver = notifyResolver; + this.revert = revert; + this.updateFactory = updateFactory; + results = new ArrayList<>(); + cherryPickInput = null; + } + + @Override + public Response<RevertSubmissionInfo> apply(ChangeResource changeResource, RevertInput input) + throws RestApiException, IOException, UpdateException, PermissionBackendException, + NoSuchProjectException, ConfigInvalidException, StorageException { + + if (!changeResource.getChange().isMerged()) { + throw new ResourceConflictException( + String.format("change is %s.", ChangeUtil.status(changeResource.getChange()))); + } + + String submissionId = changeResource.getChange().getSubmissionId(); + if (submissionId == null) { + throw new ResourceConflictException( + "This change is merged but doesn't have a submission id," + + " meaning it was not submitted through Gerrit."); + } + List<ChangeData> changeDatas = queryProvider.get().bySubmissionId(submissionId); + + for (ChangeData changeData : changeDatas) { + Change change = changeData.change(); + + // Might do the permission tests multiple times, but these are necessary to ensure that the + // user has permissions to revert all changes. If they lack any permission, no revert will be + // done. + + contributorAgreements.check(change.getProject(), changeResource.getUser()); + permissionBackend.currentUser().ref(change.getDest()).check(CREATE_CHANGE); + permissionBackend.currentUser().change(changeData).check(ChangePermission.READ); + projectCache.checkedGet(change.getProject()).checkStatePermitsWrite(); + + requireNonNull( + psUtil.get(changeData.notes(), change.currentPatchSetId()), + String.format( + "current patch set %s of change %s not found", + change.currentPatchSetId(), change.currentPatchSetId())); + } + + if (input.topic == null) { + input.topic = + String.format( + "revert-%s-%s", submissionId, RandomStringUtils.randomAlphabetic(10).toUpperCase()); + } + + return Response.ok(revertSubmission(changeDatas, input)); + } + + private RevertSubmissionInfo revertSubmission( + List<ChangeData> changeData, RevertInput revertInput) + throws RestApiException, IOException, UpdateException, PermissionBackendException, + NoSuchProjectException, ConfigInvalidException, StorageException { + Multimap<BranchNameKey, ChangeData> changesPerProjectAndBranch = ArrayListMultimap.create(); + changeData.stream().forEach(c -> changesPerProjectAndBranch.put(c.change().getDest(), c)); + + cherryPickInput = new CherryPickInput(); + // To create a revert change, we create a revert commit that is then cherry-picked. The revert + // change is created for the cherry-picked commit. Notifications are sent only for this change, + // but not for the intermediately created revert commit. + cherryPickInput.notify = revertInput.notify; + cherryPickInput.notifyDetails = revertInput.notifyDetails; + cherryPickInput.parent = 1; + cherryPickInput.keepReviewers = true; + + for (BranchNameKey projectAndBranch : changesPerProjectAndBranch.keySet()) { + String groupName = null; + Project.NameKey project = projectAndBranch.project(); + cherryPickInput.destination = projectAndBranch.branch(); + Collection<ChangeData> changesInProjectAndBranch = + changesPerProjectAndBranch.get(projectAndBranch); + + // Sort the changes topologically. + Iterator<PatchSetData> sortedChangesInProject = + sorter.sort(changesInProjectAndBranch).iterator(); + + Set<ObjectId> commitIdsInProjectAndBranch = + changesInProjectAndBranch.stream() + .map(c -> c.currentPatchSet().commitId()) + .collect(Collectors.toSet()); + + while (sortedChangesInProject.hasNext()) { + ChangeNotes changeNotes = sortedChangesInProject.next().data().notes(); + if (cherryPickInput.base == null) { + cherryPickInput.base = getBase(changeNotes, commitIdsInProjectAndBranch).name(); + } + + revertInput.message = getMessage(revertInput, changeNotes); + // This is the code in case this is the first revert of this project + branch, and the + // revert would be on top of the change being reverted. + if (cherryPickInput.base.equals(changeNotes.getCurrentPatchSet().commitId().getName())) { + ChangeInfo revertChangeInfo = + revert + .apply(changeResourceFactory.create(changeNotes, user.get()), revertInput) + .value(); + results.add(revertChangeInfo); + cherryPickInput.base = + changeNotesFactory + .createChecked(Change.id(revertChangeInfo._number)) + .getCurrentPatchSet() + .commitId() + .getName(); + groupName = cherryPickInput.base; + } else { + // This is the code in case this is the second revert (or more) of this project + branch. + ObjectId revCommitId = + commitUtil.createRevertCommit(revertInput.message, changeNotes, user.get()); + // TODO (paiking): As a future change, the revert should just be done directly on the + // target rather than just creating a commit and then cherry-picking it. + cherryPickInput.message = revertInput.message; + ObjectId generatedChangeId = Change.generateChangeId(); + Change.Id cherryPickRevertChangeId = Change.id(seq.nextChangeId()); + if (groupName == null) { + groupName = cherryPickInput.base; + } + // TODO (paiking): In the the future, the timestamp should be the same for all the revert + // changes. + try (BatchUpdate bu = updateFactory.create(project, user.get(), TimeUtil.nowTs())) { + bu.setNotify( + notifyResolver.resolve( + firstNonNull(cherryPickInput.notify, NotifyHandling.ALL), + cherryPickInput.notifyDetails)); + bu.addOp( + changeNotes.getChange().getId(), + new CreateCherryPickOp( + revCommitId, + revertInput.topic, + generatedChangeId, + cherryPickRevertChangeId, + groupName)); + bu.addOp(changeNotes.getChange().getId(), new PostRevertedMessageOp(generatedChangeId)); + bu.addOp( + cherryPickRevertChangeId, + new NotifyOp(changeNotes.getChange(), cherryPickRevertChangeId)); + + bu.execute(); + } + } + } + cherryPickInput.base = null; + } + results.sort(Comparator.comparing(c -> c.revertOf)); + RevertSubmissionInfo revertSubmissionInfo = new RevertSubmissionInfo(); + revertSubmissionInfo.revertChanges = results; + return revertSubmissionInfo; + } + + private String getMessage(RevertInput revertInput, ChangeNotes changeNotes) { + String subject = changeNotes.getChange().getSubject(); + if (subject.length() > 63) { + subject = subject.substring(0, 59) + "..."; + } + if (revertInput.message == null) { + return MessageFormat.format( + ChangeMessages.get().revertChangeDefaultMessage, + subject, + changeNotes.getCurrentPatchSet().commitId().name()); + } + + return String.format("Revert \"%s\"\n\n%s", subject, revertInput.message); + } + + /** + * This function finds the base that the first revert in a project + branch should be based on. It + * searches using BFS for the first commit that is either: 1. Has 2 or more parents, and has as + * parents at least one commit that is part of the submission. 2. A commit that is part of the + * submission. If neither of those are true, it just continues the search by going to the parents. + * + * <p>If 1 is true, if all the parents are part of the submission, it just returns that commit. If + * only some of them are in the submission, the function changes and starts checking only the + * commits that are not part of the submission. If all of them are part of the submission (or they + * are also merge commits that have as parents only other merge commits, or other changes that are + * part of the submission), we will return possibleMergeCommitToReturn which is the original + * commit we started with when 1 was determined to be true. + * + * <p>If 2 is true, it will return the commit that WalkSorter has decided that it should be the + * first commit reverted (e.g changeNotes, which is also the commit that is the first in the + * topological sorting). Unless possibleMergeCommitToReturn is not null, which means we already + * encountered a merge commit with a part of the submission earlier, which means we should return + * that merge commit. + * + * <p>It doesn't run through the entire graph since it will stop once it finds at least one commit + * that is part of the submission. + * + * @param changeNotes changeNotes for the change that is found by WalkSorter to be the first one + * that should be reverted, the first in the topological sorting. + * @param commitIds The commitIds of this project and branch. + * @return the base of the first revert. + */ + private ObjectId getBase(ChangeNotes changeNotes, Set<ObjectId> commitIds) + throws StorageException, IOException { + try (Repository git = repoManager.openRepository(changeNotes.getProjectName()); + ObjectInserter oi = git.newObjectInserter(); + ObjectReader reader = oi.newReader(); + RevWalk revWalk = new RevWalk(reader)) { + + ObjectId startCommit = + git.getRefDatabase().findRef(changeNotes.getChange().getDest().branch()).getObjectId(); + Queue<ObjectId> commitsToSearch = new ArrayDeque<>(); + commitsToSearch.add(startCommit); + + while (!commitsToSearch.isEmpty()) { + + RevCommit revCommit = revWalk.parseCommit(commitsToSearch.poll()); + if (commitIds.contains(revCommit.getId())) { + return changeNotes.getCurrentPatchSet().commitId(); + } + if (revCommit.getParentCount() > 1) { + List<RevCommit> parentsInSubmission = + Arrays.stream(revCommit.getParents()) + .filter(parent -> commitIds.contains(parent.getId())) + .collect(Collectors.toList()); + if (parentsInSubmission.size() > 1) { + // Found a merge commit that has multiple parent commits that are part of the + // submission. + return revCommit.getId(); + } + if (!parentsInSubmission.isEmpty()) { + // Found a merge commit that has only one parent in this submission, but also other + // parents not in the submission. Now we need to check if the others are merge commits + // that have as parents only other merge commits, or other changes from the + // submission. + commitsToSearch.clear(); + commitsToSearch.addAll( + Arrays.stream(revCommit.getParents()) + .filter(parent -> !commitIds.contains(parent.getId())) + .collect(Collectors.toList())); + + if (isMergeCommitDescendantOfAllChangesInTheProjectAndBranchOfTheSubmission( + commitsToSearch, commitIds, revWalk, revCommit, changeNotes)) { + // Found a second commit of that submission that share the same merge commit. + return revCommit.getId(); + } + // Couldn't find a second commit of that submission that share the same merge commit. + return changeNotes.getCurrentPatchSet().commitId(); + } + } + commitsToSearch.addAll(Arrays.asList(revCommit.getParents())); + } + // This should never happen since it can only happen if we go through the entire repository + // without finding a single commit that matches any commit from the submission. + throw new StorageException( + String.format( + "Couldn't find change %s in the repository %s", + changeNotes.getChangeId(), changeNotes.getProjectName().get())); + } + } + + /** + * This helper function calculates whether or not there exists a second commit that is part of the + * submission and ancestor of the same merge commit. + * + * @param commitsToSearch Starts as all the parents of the potential merge commit, except the one + * that is part of the submission. + * @param commitIds The commitIds of this project and branch. + * @param revWalk Used for walking through the revisions. + * @param potentialCommitToReturn The merge commit that is potentially a descendant of all changes + * in the project and branch of the submission. + * @param changeNotes changeNotes for the change that is found by WalkSorter to be the first one + * that should be reverted, the first in the topological sorting. + * @return True if found another commit of this submission, false if not found. + */ + private boolean isMergeCommitDescendantOfAllChangesInTheProjectAndBranchOfTheSubmission( + Queue<ObjectId> commitsToSearch, + Set<ObjectId> commitIds, + RevWalk revWalk, + RevCommit potentialCommitToReturn, + ChangeNotes changeNotes) + throws StorageException, IOException { + while (!commitsToSearch.isEmpty()) { + RevCommit revCommit = revWalk.parseCommit(commitsToSearch.poll()); + if (revCommit.getParentCount() > 1) { + List<RevCommit> parents = Arrays.asList(revCommit.getParents()); + if (parents.stream().anyMatch(p -> commitIds.contains(p.getId()))) { + // found another commit with a common descendant. + return true; + } + Arrays.asList(revCommit.getParents()).forEach(parent -> commitsToSearch.add(parent)); + } else { + // We found a merge commit, we found that one of the parents is not a merge commit nor a + // change of this submission. Therefore the merge commit is not useful, and we just + // rebase on the most recent revert as usual. + return false; + } + } + // This should never happen since it can only happen if we go through the entire repository + // encountering only merge commits after encountering the change whose submission we are + // reverting. + throw new StorageException( + String.format( + "Couldn't find a non-merge commit after encountering commit %s when trying to revert" + + " the submission of change %d", + potentialCommitToReturn.getName(), changeNotes.getChange().getChangeId())); + } + + @Override + public Description getDescription(ChangeResource rsrc) { + Change change = rsrc.getChange(); + boolean projectStatePermitsWrite = false; + try { + projectStatePermitsWrite = projectCache.checkedGet(rsrc.getProject()).statePermitsWrite(); + } catch (IOException e) { + logger.atSevere().withCause(e).log( + "Failed to check if project state permits write: %s", rsrc.getProject()); + } + return new UiAction.Description() + .setLabel("Revert submission") + .setTitle( + "Revert this change and all changes that have been submitted together with this change") + .setVisible( + and( + change.isMerged() + && change.getSubmissionId() != null + && isChangePartOfSubmission(change.getSubmissionId()) + && projectStatePermitsWrite, + permissionBackend + .user(rsrc.getUser()) + .ref(change.getDest()) + .testCond(CREATE_CHANGE))); + } + + /** + * @param submissionId the submission id of the change. + * @return True if the submission has more than one change, false otherwise. + */ + private Boolean isChangePartOfSubmission(String submissionId) { + return (queryProvider.get().setLimit(2).bySubmissionId(submissionId).size() > 1); + } + + private class CreateCherryPickOp implements BatchUpdateOp { + private final ObjectId revCommitId; + private final String topic; + private final ObjectId computedChangeId; + private final Change.Id cherryPickRevertChangeId; + private final String groupName; + + CreateCherryPickOp( + ObjectId revCommitId, + String topic, + ObjectId computedChangeId, + Change.Id cherryPickRevertChangeId, + String groupName) { + this.revCommitId = revCommitId; + this.topic = topic; + this.computedChangeId = computedChangeId; + this.cherryPickRevertChangeId = cherryPickRevertChangeId; + this.groupName = groupName; + } + + @Override + public boolean updateChange(ChangeContext ctx) throws Exception { + Change change = ctx.getChange(); + Result cherryPickResult = + cherryPickChange.cherryPick( + updateFactory, + change, + change.getProject(), + revCommitId, + cherryPickInput, + BranchNameKey.create( + change.getProject(), RefNames.fullName(cherryPickInput.destination)), + true, + topic, + change.getId(), + computedChangeId, + cherryPickRevertChangeId, + groupName); + // save the commit as base for next cherryPick of that branch + cherryPickInput.base = + changeNotesFactory + .createChecked(cherryPickResult.changeId()) + .getCurrentPatchSet() + .commitId() + .getName(); + results.add( + json.noOptions() + .format(change.getProject(), cherryPickResult.changeId(), ChangeInfo::new)); + return true; + } + } + + private class NotifyOp implements BatchUpdateOp { + private final Change change; + private final Change.Id revertChangeId; + + NotifyOp(Change change, Change.Id revertChangeId) { + this.change = change; + this.revertChangeId = revertChangeId; + } + + @Override + public void postUpdate(Context ctx) throws Exception { + changeReverted.fire( + change, changeNotesFactory.createChecked(revertChangeId).getChange(), ctx.getWhen()); + try { + RevertedSender cm = revertedSenderFactory.create(ctx.getProject(), change.getId()); + cm.setFrom(ctx.getAccountId()); + cm.setNotify(ctx.getNotify(change.getId())); + cm.send(); + } catch (Exception err) { + logger.atSevere().withCause(err).log( + "Cannot send email for revert change %s", change.getId()); + } + } + } + + /** + * create a message that describes the revert if the cherry-pick is successful, and point the + * revert of the change towards the cherry-pick. The cherry-pick is the updated change that acts + * as "revert-of" the original change. + */ + private class PostRevertedMessageOp implements BatchUpdateOp { + private final ObjectId computedChangeId; + + PostRevertedMessageOp(ObjectId computedChangeId) { + this.computedChangeId = computedChangeId; + } + + @Override + public boolean updateChange(ChangeContext ctx) throws Exception { + Change change = ctx.getChange(); + PatchSet.Id patchSetId = change.currentPatchSetId(); + ChangeMessage changeMessage = + ChangeMessagesUtil.newMessage( + ctx, + "Created a revert of this change as I" + computedChangeId.getName(), + ChangeMessagesUtil.TAG_REVERT); + cmUtil.addChangeMessage(ctx.getUpdate(patchSetId), changeMessage); + return true; + } + } +}
diff --git a/java/com/google/gerrit/server/restapi/change/Reviewed.java b/java/com/google/gerrit/server/restapi/change/Reviewed.java index 7152799..2793059 100644 --- a/java/com/google/gerrit/server/restapi/change/Reviewed.java +++ b/java/com/google/gerrit/server/restapi/change/Reviewed.java
@@ -43,7 +43,7 @@ resource.getPatchKey().patchSetId(), resource.getAccountId(), resource.getPatchKey().fileName())); - return reviewFlagUpdated ? Response.created("") : Response.ok(""); + return reviewFlagUpdated ? Response.created() : Response.ok(); } }
diff --git a/java/com/google/gerrit/server/restapi/change/Reviewers.java b/java/com/google/gerrit/server/restapi/change/Reviewers.java index b2714da..d702142 100644 --- a/java/com/google/gerrit/server/restapi/change/Reviewers.java +++ b/java/com/google/gerrit/server/restapi/change/Reviewers.java
@@ -21,12 +21,11 @@ import com.google.gerrit.extensions.restapi.IdString; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestView; -import com.google.gerrit.extensions.restapi.TopLevelResource; import com.google.gerrit.mail.Address; import com.google.gerrit.server.ApprovalsUtil; +import com.google.gerrit.server.account.AccountResolver; import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.change.ReviewerResource; -import com.google.gerrit.server.restapi.account.AccountsCollection; import com.google.inject.Inject; import com.google.inject.Singleton; import java.io.IOException; @@ -37,22 +36,22 @@ public class Reviewers implements ChildCollection<ChangeResource, ReviewerResource> { private final DynamicMap<RestView<ReviewerResource>> views; private final ApprovalsUtil approvalsUtil; - private final AccountsCollection accounts; private final ReviewerResource.Factory resourceFactory; private final ListReviewers list; + private final AccountResolver accountResolver; @Inject Reviewers( ApprovalsUtil approvalsUtil, - AccountsCollection accounts, ReviewerResource.Factory resourceFactory, DynamicMap<RestView<ReviewerResource>> views, - ListReviewers list) { + ListReviewers list, + AccountResolver accountResolver) { this.approvalsUtil = approvalsUtil; - this.accounts = accounts; this.resourceFactory = resourceFactory; this.views = views; this.list = list; + this.accountResolver = accountResolver; } @Override @@ -68,22 +67,18 @@ @Override public ReviewerResource parse(ChangeResource rsrc, IdString id) throws ResourceNotFoundException, AuthException, IOException, ConfigInvalidException { - Address address = Address.tryParse(id.get()); - - Account.Id accountId = null; try { - accountId = accounts.parse(TopLevelResource.INSTANCE, id).getUser().getAccountId(); - } catch (ResourceNotFoundException e) { - if (address == null) { - throw e; + + AccountResolver.Result result = accountResolver.resolveIgnoreVisibility(id.get()); + if (fetchAccountIds(rsrc).contains(result.asUniqueUser().getAccountId())) { + return resourceFactory.create(rsrc, result.asUniqueUser().getAccountId()); + } + } catch (AccountResolver.UnresolvableAccountException e) { + if (e.isSelf()) { + throw new AuthException(e.getMessage(), e); } } - // See if the id exists as a reviewer for this change - if (accountId != null && fetchAccountIds(rsrc).contains(accountId)) { - return resourceFactory.create(rsrc, accountId); - } - - // See if the address exists as a reviewer on the change + Address address = Address.tryParse(id.get()); if (address != null && rsrc.getNotes().getReviewersByEmail().all().contains(address)) { return new ReviewerResource(rsrc, address); }
diff --git a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java index 288806c..8362e95 100644 --- a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java +++ b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
@@ -22,6 +22,7 @@ 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.ChangeUtil; import com.google.gerrit.server.change.ChangeResource; @@ -31,27 +32,25 @@ import com.google.gerrit.server.permissions.ChangePermission; import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.update.BatchUpdate; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; 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; @Singleton -public class SetReadyForReview extends RetryingRestModifyView<ChangeResource, Input, String> - implements UiAction<ChangeResource> { +public class SetReadyForReview + implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> { + private final BatchUpdate.Factory updateFactory; private final WorkInProgressOp.Factory opFactory; @Inject - SetReadyForReview(RetryHelper retryHelper, WorkInProgressOp.Factory opFactory) { - super(retryHelper); + SetReadyForReview(BatchUpdate.Factory updateFactory, WorkInProgressOp.Factory opFactory) { + this.updateFactory = updateFactory; this.opFactory = opFactory; } @Override - protected Response<String> applyImpl( - BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input) + public Response<String> apply(ChangeResource rsrc, Input input) throws RestApiException, UpdateException, PermissionBackendException { rsrc.permissions().check(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE); @@ -69,7 +68,7 @@ bu.setNotify(NotifyResolver.Result.create(firstNonNull(input.notify, NotifyHandling.ALL))); bu.addOp(rsrc.getChange().getId(), opFactory.create(false, input)); bu.execute(); - return Response.ok(""); + return Response.ok(); } }
diff --git a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java index 3fb0295..fdaad9d 100644 --- a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java +++ b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
@@ -22,6 +22,7 @@ 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.ChangeUtil; import com.google.gerrit.server.change.ChangeResource; @@ -31,27 +32,25 @@ import com.google.gerrit.server.permissions.ChangePermission; import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.update.BatchUpdate; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; 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; @Singleton -public class SetWorkInProgress extends RetryingRestModifyView<ChangeResource, Input, String> - implements UiAction<ChangeResource> { +public class SetWorkInProgress + implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> { + private final BatchUpdate.Factory updateFactory; private final WorkInProgressOp.Factory opFactory; @Inject - SetWorkInProgress(WorkInProgressOp.Factory opFactory, RetryHelper retryHelper) { - super(retryHelper); + SetWorkInProgress(BatchUpdate.Factory updateFactory, WorkInProgressOp.Factory opFactory) { + this.updateFactory = updateFactory; this.opFactory = opFactory; } @Override - protected Response<String> applyImpl( - BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input) + public Response<String> apply(ChangeResource rsrc, Input input) throws RestApiException, UpdateException, PermissionBackendException { rsrc.permissions().check(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE); @@ -69,7 +68,7 @@ bu.setNotify(NotifyResolver.Result.create(firstNonNull(input.notify, NotifyHandling.NONE))); bu.addOp(rsrc.getChange().getId(), opFactory.create(true, input)); bu.execute(); - return Response.ok(""); + return Response.ok(); } }
diff --git a/java/com/google/gerrit/server/restapi/change/Submit.java b/java/com/google/gerrit/server/restapi/change/Submit.java index 56dee3f..287c2bc 100644 --- a/java/com/google/gerrit/server/restapi/change/Submit.java +++ b/java/com/google/gerrit/server/restapi/change/Submit.java
@@ -19,7 +19,6 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Strings; -import com.google.common.base.Throwables; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Sets; @@ -190,7 +189,8 @@ @UsedAt(UsedAt.Project.GOOGLE) public Response<Output> mergeChange( RevisionResource rsrc, IdentifiedUser submitter, SubmitInput input) - throws RestApiException, IOException { + throws RestApiException, IOException, UpdateException, ConfigInvalidException, + PermissionBackendException { Change change = rsrc.getChange(); if (!change.isNew()) { throw new ResourceConflictException("change is " + ChangeUtil.status(change)); @@ -207,13 +207,7 @@ try (MergeOp op = mergeOpProvider.get()) { Change updatedChange; - try { - updatedChange = op.merge(change, submitter, true, input, false); - } catch (Exception e) { - Throwables.throwIfInstanceOf(e, RestApiException.class); - return Response.<Output>internalServerError(e).traceId(op.getTraceId().orElse(null)); - } - + updatedChange = op.merge(change, submitter, true, input, false); if (updatedChange.isMerged()) { return Response.ok(new Output(change)); } @@ -471,12 +465,6 @@ } Response<Output> response = submit.apply(new RevisionResource(rsrc, ps), input); - if (response instanceof Response.InternalServerError) { - Response.InternalServerError<?> ise = (Response.InternalServerError<?>) response; - return Response.<ChangeInfo>internalServerError(ise.cause()) - .traceId(ise.traceId().orElse(null)); - } - return Response.ok(json.noOptions().format(response.value().change)); } }
diff --git a/java/com/google/gerrit/server/restapi/change/Unignore.java b/java/com/google/gerrit/server/restapi/change/Unignore.java index 26d3233..999e736 100644 --- a/java/com/google/gerrit/server/restapi/change/Unignore.java +++ b/java/com/google/gerrit/server/restapi/change/Unignore.java
@@ -50,7 +50,7 @@ if (isIgnored(rsrc)) { stars.unignore(rsrc); } - return Response.ok(""); + return Response.ok(); } private boolean isIgnored(ChangeResource rsrc) {
diff --git a/java/com/google/gerrit/server/restapi/config/FlushCache.java b/java/com/google/gerrit/server/restapi/config/FlushCache.java index 9ea9e33..f10ed8d 100644 --- a/java/com/google/gerrit/server/restapi/config/FlushCache.java +++ b/java/com/google/gerrit/server/restapi/config/FlushCache.java
@@ -50,6 +50,6 @@ } rsrc.getCache().invalidateAll(); - return Response.ok(""); + return Response.ok(); } }
diff --git a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java index 2d504c7..2d4bfe6 100644 --- a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java +++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -23,9 +23,11 @@ import com.google.gerrit.extensions.common.AccountsInfo; import com.google.gerrit.extensions.common.AuthInfo; import com.google.gerrit.extensions.common.ChangeConfigInfo; +import com.google.gerrit.extensions.common.ChangeIndexConfigInfo; import com.google.gerrit.extensions.common.DownloadInfo; import com.google.gerrit.extensions.common.DownloadSchemeInfo; import com.google.gerrit.extensions.common.GerritInfo; +import com.google.gerrit.extensions.common.IndexConfigInfo; import com.google.gerrit.extensions.common.PluginConfigInfo; import com.google.gerrit.extensions.common.ReceiveInfo; import com.google.gerrit.extensions.common.ServerInfo; @@ -141,6 +143,7 @@ info.change = getChangeInfo(); info.download = getDownloadInfo(); info.gerrit = getGerritInfo(); + info.index = getIndexInfo(); info.noteDbEnabled = true; info.plugin = getPluginInfo(); info.defaultTheme = getDefaultTheme(); @@ -296,6 +299,14 @@ return info; } + private IndexConfigInfo getIndexInfo() { + ChangeIndexConfigInfo change = new ChangeIndexConfigInfo(); + change.indexMergeable = toBoolean(config.getBoolean("index", "change", "indexMergeable", true)); + IndexConfigInfo index = new IndexConfigInfo(); + index.change = change; + return index; + } + private String getDocUrl() { String docUrl = config.getString("gerrit", null, "docUrl"); if (Strings.isNullOrEmpty(docUrl)) {
diff --git a/java/com/google/gerrit/server/restapi/config/PostCaches.java b/java/com/google/gerrit/server/restapi/config/PostCaches.java index c633af0..c9480c5 100644 --- a/java/com/google/gerrit/server/restapi/config/PostCaches.java +++ b/java/com/google/gerrit/server/restapi/config/PostCaches.java
@@ -84,13 +84,13 @@ "specifying caches is not allowed for operation 'FLUSH_ALL'"); } flushAll(); - return Response.ok(""); + return Response.ok(); case FLUSH: if (input.caches == null || input.caches.isEmpty()) { throw new BadRequestException("caches must be specified for operation 'FLUSH'"); } flush(input.caches); - return Response.ok(""); + return Response.ok(); default: throw new BadRequestException("unsupported operation: " + input.operation); }
diff --git a/java/com/google/gerrit/server/restapi/group/AddMembers.java b/java/com/google/gerrit/server/restapi/group/AddMembers.java index 86d1283..b60d78e 100644 --- a/java/com/google/gerrit/server/restapi/group/AddMembers.java +++ b/java/com/google/gerrit/server/restapi/group/AddMembers.java
@@ -28,6 +28,7 @@ import com.google.gerrit.extensions.restapi.IdString; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestCollectionCreateView; import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; @@ -222,7 +223,8 @@ @Override public Response<AccountInfo> apply(GroupResource resource, IdString id, Input input) - throws Exception { + throws RestApiException, NotInternalGroupException, IOException, ConfigInvalidException, + PermissionBackendException { AddMembers.Input in = new AddMembers.Input(); in._oneMember = id.get(); try {
diff --git a/java/com/google/gerrit/server/restapi/group/DeleteMembers.java b/java/com/google/gerrit/server/restapi/group/DeleteMembers.java index 9bae2e2..fa1c5c6 100644 --- a/java/com/google/gerrit/server/restapi/group/DeleteMembers.java +++ b/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.restapi.group; +import com.google.common.base.Strings; import com.google.common.collect.Sets; import com.google.gerrit.common.data.GroupDescription; import com.google.gerrit.entities.Account; @@ -68,6 +69,9 @@ Set<Account.Id> membersToRemove = new HashSet<>(); for (String nameOrEmail : input.members) { + if (Strings.isNullOrEmpty(nameOrEmail)) { + continue; + } membersToRemove.add(accountResolver.resolve(nameOrEmail).asUnique().account().id()); } AccountGroup.UUID groupUuid = internalGroup.getGroupUUID();
diff --git a/java/com/google/gerrit/server/restapi/group/GroupsCollection.java b/java/com/google/gerrit/server/restapi/group/GroupsCollection.java index 52fe9d0..65a7f4f 100644 --- a/java/com/google/gerrit/server/restapi/group/GroupsCollection.java +++ b/java/com/google/gerrit/server/restapi/group/GroupsCollection.java
@@ -42,7 +42,7 @@ private final GroupResolver groupResolver; private final Provider<CurrentUser> self; - private boolean hasQuery2; + private boolean hasQuery; @Inject public GroupsCollection( @@ -62,12 +62,7 @@ @Override public void setParams(ListMultimap<String, String> params) throws BadRequestException { - if (params.containsKey("query") && params.containsKey("query2")) { - throw new BadRequestException("\"query\" and \"query2\" options are mutually exclusive"); - } - - // The --query2 option is defined in QueryGroups - this.hasQuery2 = params.containsKey("query2"); + this.hasQuery = params.containsKey("query"); } @Override @@ -79,7 +74,7 @@ throw new ResourceNotFoundException(); } - if (hasQuery2) { + if (hasQuery) { return queryGroups.get(); }
diff --git a/java/com/google/gerrit/server/restapi/group/ListGroups.java b/java/com/google/gerrit/server/restapi/group/ListGroups.java index 899ed00..adc251c 100644 --- a/java/com/google/gerrit/server/restapi/group/ListGroups.java +++ b/java/com/google/gerrit/server/restapi/group/ListGroups.java
@@ -129,21 +129,6 @@ this.owned = owned; } - /** - * Add a group to inspect. - * - * @param uuid UUID of the group - * @deprecated use {@link #addGroup(AccountGroup.UUID)}. - */ - @Deprecated - @Option( - name = "--query", - aliases = {"-q"}, - usage = "group to inspect (deprecated: use --group/-g instead)") - void addGroup_Deprecated(AccountGroup.UUID uuid) { - addGroup(uuid); - } - @Option( name = "--group", aliases = {"-g"},
diff --git a/java/com/google/gerrit/server/restapi/group/QueryGroups.java b/java/com/google/gerrit/server/restapi/group/QueryGroups.java index a233111..380d42e 100644 --- a/java/com/google/gerrit/server/restapi/group/QueryGroups.java +++ b/java/com/google/gerrit/server/restapi/group/QueryGroups.java
@@ -48,12 +48,9 @@ private int start; private EnumSet<ListGroupsOption> options = EnumSet.noneOf(ListGroupsOption.class); - // TODO(ekempin): --query in ListGroups is marked as deprecated, once it is - // removed we want to rename --query2 to --query here. - /** --query (-q) is already used by {@link ListGroups} */ @Option( - name = "--query2", - aliases = {"-q2"}, + name = "--query", + aliases = {"-q"}, usage = "group query") public void setQuery(String query) { this.query = query;
diff --git a/java/com/google/gerrit/server/restapi/project/BanCommit.java b/java/com/google/gerrit/server/restapi/project/BanCommit.java index 64e38b0..eb5473d 100644 --- a/java/com/google/gerrit/server/restapi/project/BanCommit.java +++ b/java/com/google/gerrit/server/restapi/project/BanCommit.java
@@ -18,14 +18,11 @@ import com.google.gerrit.extensions.api.projects.BanCommitInput; 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.server.git.BanCommitResult; import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ProjectResource; -import com.google.gerrit.server.restapi.project.BanCommit.BanResultInfo; -import com.google.gerrit.server.update.BatchUpdate; -import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -34,20 +31,18 @@ import java.util.List; import org.eclipse.jgit.lib.ObjectId; +/** The REST endpoint that marks commits as banned in a project. */ @Singleton -public class BanCommit - extends RetryingRestModifyView<ProjectResource, BanCommitInput, BanResultInfo> { +public class BanCommit implements RestModifyView<ProjectResource, BanCommitInput> { private final com.google.gerrit.server.git.BanCommit banCommit; @Inject - BanCommit(RetryHelper retryHelper, com.google.gerrit.server.git.BanCommit banCommit) { - super(retryHelper); + BanCommit(com.google.gerrit.server.git.BanCommit banCommit) { this.banCommit = banCommit; } @Override - protected Response<BanResultInfo> applyImpl( - BatchUpdate.Factory updateFactory, ProjectResource rsrc, BanCommitInput input) + public Response<BanResultInfo> apply(ProjectResource rsrc, BanCommitInput input) throws RestApiException, UpdateException, IOException, PermissionBackendException { BanResultInfo r = new BanResultInfo(); if (input != null && input.commits != null && !input.commits.isEmpty()) {
diff --git a/java/com/google/gerrit/server/restapi/project/CheckMergeability.java b/java/com/google/gerrit/server/restapi/project/CheckMergeability.java index 69a6da8..4864fde 100644 --- a/java/com/google/gerrit/server/restapi/project/CheckMergeability.java +++ b/java/com/google/gerrit/server/restapi/project/CheckMergeability.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.restapi.project; +import com.google.gerrit.exceptions.InvalidMergeStrategyException; import com.google.gerrit.extensions.client.SubmitType; import com.google.gerrit.extensions.common.MergeableInfo; import com.google.gerrit.extensions.restapi.BadRequestException; @@ -120,7 +121,7 @@ result.conflicts = ((ResolveMerger) m).getUnmergedPaths(); } } - } catch (IllegalArgumentException e) { + } catch (InvalidMergeStrategyException e) { throw new BadRequestException(e.getMessage()); } return Response.ok(result);
diff --git a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java index 334447b..31fa8d3 100644 --- a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java +++ b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
@@ -16,16 +16,15 @@ import static com.google.common.collect.ImmutableList.toImmutableList; -import com.google.common.base.Throwables; import com.google.gerrit.entities.Project; import com.google.gerrit.entities.RefNames; -import com.google.gerrit.exceptions.StorageException; import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.restapi.ChildCollection; import com.google.gerrit.extensions.restapi.IdString; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestView; +import com.google.gerrit.index.query.Predicate; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.index.change.ChangeIndexCollection; import com.google.gerrit.server.project.CommitResource; @@ -33,15 +32,17 @@ import com.google.gerrit.server.project.ProjectState; import com.google.gerrit.server.project.Reachable; import com.google.gerrit.server.query.change.ChangeData; -import com.google.gerrit.server.query.change.InternalChangeQuery; +import com.google.gerrit.server.query.change.CommitPredicate; +import com.google.gerrit.server.query.change.ProjectPredicate; import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryHelper.Action; -import com.google.gerrit.server.update.RetryHelper.ActionType; import com.google.inject.Inject; -import com.google.inject.Provider; import com.google.inject.Singleton; import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.ObjectId; @@ -56,7 +57,6 @@ private final GitRepositoryManager repoManager; private final RetryHelper retryHelper; private final ChangeIndexCollection indexes; - private final Provider<InternalChangeQuery> queryProvider; private final Reachable reachable; @Inject @@ -65,13 +65,11 @@ GitRepositoryManager repoManager, RetryHelper retryHelper, ChangeIndexCollection indexes, - Provider<InternalChangeQuery> queryProvider, Reachable reachable) { this.views = views; this.repoManager = repoManager; this.retryHelper = retryHelper; this.indexes = indexes; - this.queryProvider = queryProvider; this.reachable = reachable; } @@ -116,24 +114,51 @@ public boolean canRead(ProjectState state, Repository repo, RevCommit commit) throws IOException { Project.NameKey project = state.getNameKey(); if (indexes.getSearchIndex() == null) { - // No index in slaves, fall back to scanning refs. + // No index in slaves, fall back to scanning refs. We must inspect change refs too + // as the commit might be a patchset of a not yet submitted change. return reachable.fromRefs(project, repo, commit, repo.getRefDatabase().getRefs()); } - // Check first if any change references the commit in question. This is much cheaper than ref - // visibility filtering and reachability computation. + // Check first if any patchset of any change references the commit in question. This is much + // cheaper than ref visibility filtering and reachability computation. List<ChangeData> changes = - executeIndexQuery( - () -> - queryProvider - .get() - .enforceVisibility(true) - .setLimit(1) - .byProjectCommit(project, commit)); + retryHelper + .changeIndexQuery( + "queryChangesByProjectCommitWithLimit1", + q -> q.enforceVisibility(true).setLimit(1).byProjectCommit(project, commit)) + .call(); if (!changes.isEmpty()) { return true; } + // Maybe the commit was a merge commit of a change. Try to find promising candidates for + // branches to check, by seeing if its parents were associated to changes. + Predicate<ChangeData> pred = + Predicate.and( + new ProjectPredicate(project.get()), + Predicate.or( + Arrays.stream(commit.getParents()) + .map(parent -> new CommitPredicate(parent.getId().getName())) + .collect(toImmutableList()))); + changes = + retryHelper + .changeIndexQuery( + "queryChangesByProjectCommit", q -> q.enforceVisibility(true).query(pred)) + .call(); + + Set<Ref> branchesForCommitParents = new HashSet<>(changes.size()); + for (ChangeData cd : changes) { + Ref ref = repo.exactRef(cd.change().getDest().branch()); + if (ref != null) { + branchesForCommitParents.add(ref); + } + } + + if (reachable.fromRefs( + project, repo, commit, branchesForCommitParents.stream().collect(Collectors.toList()))) { + return true; + } + // If we have already checked change refs using the change index, spare any further checks for // changes. List<Ref> refs = @@ -142,14 +167,4 @@ .collect(toImmutableList()); return reachable.fromRefs(project, repo, commit, refs); } - - private <T> T executeIndexQuery(Action<T> action) { - try { - return retryHelper.execute( - ActionType.INDEX_QUERY, action, StorageException.class::isInstance); - } catch (Exception e) { - Throwables.throwIfUnchecked(e); - throw new StorageException(e); - } - } }
diff --git a/java/com/google/gerrit/server/restapi/project/CreateBranch.java b/java/com/google/gerrit/server/restapi/project/CreateBranch.java index 36ad918..67213c5 100644 --- a/java/com/google/gerrit/server/restapi/project/CreateBranch.java +++ b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
@@ -28,6 +28,7 @@ import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestCollectionCreateView; +import com.google.gerrit.git.LockFailureException; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.extensions.events.GitReferenceUpdated; import com.google.gerrit.server.git.GitRepositoryManager; @@ -163,8 +164,7 @@ } refPrefix = RefUtil.getRefPrefix(refPrefix); } - // fall through - // $FALL-THROUGH$ + throw new LockFailureException(String.format("Failed to create %s", ref), u); case FORCED: case IO_FAILURE: case NOT_ATTEMPTED: @@ -174,9 +174,7 @@ case REJECTED_MISSING_OBJECT: case REJECTED_OTHER_REASON: default: - { - throw new IOException(result.name()); - } + throw new IOException(String.format("Failed to create %s: %s", ref, result.name())); } BranchInfo info = new BranchInfo();
diff --git a/java/com/google/gerrit/server/restapi/project/CreateDashboard.java b/java/com/google/gerrit/server/restapi/project/CreateDashboard.java index 314df73..9904b1f 100644 --- a/java/com/google/gerrit/server/restapi/project/CreateDashboard.java +++ b/java/com/google/gerrit/server/restapi/project/CreateDashboard.java
@@ -19,12 +19,15 @@ import com.google.gerrit.extensions.restapi.IdString; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestCollectionCreateView; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.DashboardResource; import com.google.gerrit.server.project.ProjectResource; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; +import java.io.IOException; import org.kohsuke.args4j.Option; @Singleton @@ -42,7 +45,7 @@ @Override public Response<DashboardInfo> apply(ProjectResource parent, IdString id, SetDashboardInput input) - throws Exception { + throws RestApiException, IOException, PermissionBackendException { parent.getProjectState().checkStatePermitsWrite(); if (!DashboardsCollection.isDefaultDashboard(id)) { throw new ResourceNotFoundException(id);
diff --git a/java/com/google/gerrit/server/restapi/project/CreateLabel.java b/java/com/google/gerrit/server/restapi/project/CreateLabel.java new file mode 100644 index 0000000..5d51527 --- /dev/null +++ b/java/com/google/gerrit/server/restapi/project/CreateLabel.java
@@ -0,0 +1,206 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.restapi.project; + +import com.google.common.base.Strings; +import com.google.gerrit.common.data.LabelFunction; +import com.google.gerrit.common.data.LabelType; +import com.google.gerrit.common.data.LabelValue; +import com.google.gerrit.extensions.common.LabelDefinitionInfo; +import com.google.gerrit.extensions.common.LabelDefinitionInput; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.IdString; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestCollectionCreateView; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.git.meta.MetaDataUpdate; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.ProjectPermission; +import com.google.gerrit.server.project.LabelDefinitionJson; +import com.google.gerrit.server.project.LabelResource; +import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.server.project.ProjectConfig; +import com.google.gerrit.server.project.ProjectResource; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import java.io.IOException; +import java.util.List; +import org.eclipse.jgit.errors.ConfigInvalidException; + +@Singleton +public class CreateLabel + implements RestCollectionCreateView<ProjectResource, LabelResource, LabelDefinitionInput> { + private final Provider<CurrentUser> user; + private final PermissionBackend permissionBackend; + private final MetaDataUpdate.User updateFactory; + private final ProjectConfig.Factory projectConfigFactory; + private final ProjectCache projectCache; + + @Inject + public CreateLabel( + Provider<CurrentUser> user, + PermissionBackend permissionBackend, + MetaDataUpdate.User updateFactory, + ProjectConfig.Factory projectConfigFactory, + ProjectCache projectCache) { + this.user = user; + this.permissionBackend = permissionBackend; + this.updateFactory = updateFactory; + this.projectConfigFactory = projectConfigFactory; + this.projectCache = projectCache; + } + + @Override + public Response<LabelDefinitionInfo> apply( + ProjectResource rsrc, IdString id, LabelDefinitionInput input) + throws AuthException, BadRequestException, ResourceConflictException, + PermissionBackendException, IOException, ConfigInvalidException { + if (!user.get().isIdentifiedUser()) { + throw new AuthException("Authentication required"); + } + + permissionBackend + .currentUser() + .project(rsrc.getNameKey()) + .check(ProjectPermission.WRITE_CONFIG); + + if (input == null) { + input = new LabelDefinitionInput(); + } + + if (input.name != null && !input.name.equals(id.get())) { + throw new BadRequestException("name in input must match name in URL"); + } + + try (MetaDataUpdate md = updateFactory.create(rsrc.getNameKey())) { + ProjectConfig config = projectConfigFactory.read(md); + + LabelType labelType = createLabel(config, id.get(), input); + + if (input.commitMessage != null) { + md.setMessage(Strings.emptyToNull(input.commitMessage.trim())); + } else { + md.setMessage("Update label"); + } + + config.commit(md); + + projectCache.evict(rsrc.getProjectState().getProject()); + + return Response.created(LabelDefinitionJson.format(rsrc.getNameKey(), labelType)); + } + } + + /** + * Creates a new label. + * + * @param config the project config + * @param label the name of the new label + * @param input the input that describes the new label + * @return the created label type + * @throws BadRequestException if there was invalid data in the input + * @throws ResourceConflictException if the label cannot be created due to a conflict + */ + public LabelType createLabel(ProjectConfig config, String label, LabelDefinitionInput input) + throws BadRequestException, ResourceConflictException { + if (config.getLabelSections().containsKey(label)) { + throw new ResourceConflictException(String.format("label %s already exists", label)); + } + + for (String labelName : config.getLabelSections().keySet()) { + if (labelName.equalsIgnoreCase(label)) { + throw new ResourceConflictException( + String.format("label %s conflicts with existing label %s", label, labelName)); + } + } + + if (input.values == null || input.values.isEmpty()) { + throw new BadRequestException("values are required"); + } + + List<LabelValue> values = LabelDefinitionInputParser.parseValues(input.values); + + LabelType labelType; + try { + labelType = new LabelType(label, values); + } catch (IllegalArgumentException e) { + throw new BadRequestException("invalid name: " + label, e); + } + + if (input.function != null && !input.function.trim().isEmpty()) { + labelType.setFunction(LabelDefinitionInputParser.parseFunction(input.function)); + } else { + labelType.setFunction(LabelFunction.MAX_WITH_BLOCK); + } + + if (input.defaultValue != null) { + labelType.setDefaultValue( + LabelDefinitionInputParser.parseDefaultValue(labelType, input.defaultValue)); + } + + if (input.branches != null) { + labelType.setRefPatterns(LabelDefinitionInputParser.parseBranches(input.branches)); + } + + if (input.canOverride != null) { + labelType.setCanOverride(input.canOverride); + } + + if (input.copyAnyScore != null) { + labelType.setCopyAnyScore(input.copyAnyScore); + } + + if (input.copyMinScore != null) { + labelType.setCopyMinScore(input.copyMinScore); + } + + if (input.copyMaxScore != null) { + labelType.setCopyMaxScore(input.copyMaxScore); + } + + if (input.copyAllScoresIfNoChange != null) { + labelType.setCopyAllScoresIfNoChange(input.copyAllScoresIfNoChange); + } + + if (input.copyAllScoresIfNoCodeChange != null) { + labelType.setCopyAllScoresIfNoCodeChange(input.copyAllScoresIfNoCodeChange); + } + + if (input.copyAllScoresOnTrivialRebase != null) { + labelType.setCopyAllScoresOnTrivialRebase(input.copyAllScoresOnTrivialRebase); + } + + if (input.copyAllScoresOnMergeFirstParentUpdate != null) { + labelType.setCopyAllScoresOnMergeFirstParentUpdate( + input.copyAllScoresOnMergeFirstParentUpdate); + } + + if (input.allowPostSubmit != null) { + labelType.setAllowPostSubmit(input.allowPostSubmit); + } + + if (input.ignoreSelfApproval != null) { + labelType.setIgnoreSelfApproval(input.ignoreSelfApproval); + } + + config.getLabelSections().put(labelType.getName(), labelType); + + return labelType; + } +}
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteLabel.java b/java/com/google/gerrit/server/restapi/project/DeleteLabel.java new file mode 100644 index 0000000..531640c --- /dev/null +++ b/java/com/google/gerrit/server/restapi/project/DeleteLabel.java
@@ -0,0 +1,113 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.restapi.project; + +import com.google.common.base.Strings; +import com.google.gerrit.extensions.common.InputWithCommitMessage; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.IdString; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.git.meta.MetaDataUpdate; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.ProjectPermission; +import com.google.gerrit.server.project.LabelResource; +import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.server.project.ProjectConfig; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import java.io.IOException; +import org.eclipse.jgit.errors.ConfigInvalidException; + +@Singleton +public class DeleteLabel implements RestModifyView<LabelResource, InputWithCommitMessage> { + private final Provider<CurrentUser> user; + private final PermissionBackend permissionBackend; + private final MetaDataUpdate.User updateFactory; + private final ProjectConfig.Factory projectConfigFactory; + private final ProjectCache projectCache; + + @Inject + public DeleteLabel( + Provider<CurrentUser> user, + PermissionBackend permissionBackend, + MetaDataUpdate.User updateFactory, + ProjectConfig.Factory projectConfigFactory, + ProjectCache projectCache) { + this.user = user; + this.permissionBackend = permissionBackend; + this.updateFactory = updateFactory; + this.projectConfigFactory = projectConfigFactory; + this.projectCache = projectCache; + } + + @Override + public Response<?> apply(LabelResource rsrc, InputWithCommitMessage input) + throws AuthException, ResourceNotFoundException, PermissionBackendException, IOException, + ConfigInvalidException { + if (!user.get().isIdentifiedUser()) { + throw new AuthException("Authentication required"); + } + + permissionBackend + .currentUser() + .project(rsrc.getProject().getNameKey()) + .check(ProjectPermission.WRITE_CONFIG); + + if (input == null) { + input = new InputWithCommitMessage(); + } + + try (MetaDataUpdate md = updateFactory.create(rsrc.getProject().getNameKey())) { + ProjectConfig config = projectConfigFactory.read(md); + + if (!deleteLabel(config, rsrc.getLabelType().getName())) { + throw new ResourceNotFoundException(IdString.fromDecoded(rsrc.getLabelType().getName())); + } + + if (input.commitMessage != null) { + md.setMessage(Strings.emptyToNull(input.commitMessage.trim())); + } else { + md.setMessage("Delete label"); + } + + config.commit(md); + } + + projectCache.evict(rsrc.getProject().getProjectState().getProject()); + + return Response.none(); + } + + /** + * Delete the given label from the given project config. + * + * @param config the project config from which the label should be deleted + * @param labelName the name of the label that should be deleted + * @return {@code true} if the label was deleted, {@code false} if the label was not found + */ + public boolean deleteLabel(ProjectConfig config, String labelName) { + if (!config.getLabelSections().containsKey(labelName)) { + return false; + } + + config.getLabelSections().remove(labelName); + return true; + } +}
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteRef.java b/java/com/google/gerrit/server/restapi/project/DeleteRef.java index 1979d61..2395bdd 100644 --- a/java/com/google/gerrit/server/restapi/project/DeleteRef.java +++ b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
@@ -30,6 +30,7 @@ import com.google.gerrit.exceptions.StorageException; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.git.LockFailureException; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.extensions.events.GitReferenceUpdated; import com.google.gerrit.server.git.GitRepositoryManager; @@ -43,7 +44,6 @@ import com.google.inject.Provider; import com.google.inject.Singleton; import java.io.IOException; -import org.eclipse.jgit.errors.LockFailedException; import org.eclipse.jgit.lib.BatchRefUpdate; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; @@ -58,9 +58,6 @@ public class DeleteRef { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - private static final int MAX_LOCK_FAILURE_CALLS = 10; - private static final long SLEEP_ON_LOCK_FAILURE_MS = 15; - private final Provider<IdentifiedUser> identifiedUser; private final PermissionBackend permissionBackend; private final GitRepositoryManager repoManager; @@ -126,23 +123,7 @@ u.setNewObjectId(ObjectId.zeroId()); u.setForceUpdate(true); refDeletionValidator.validateRefOperation(projectState.getName(), identifiedUser.get(), u); - int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS; - for (; ; ) { - try { - result = u.delete(); - } catch (LockFailedException e) { - result = RefUpdate.Result.LOCK_FAILURE; - } - if (result == RefUpdate.Result.LOCK_FAILURE && --remainingLockFailureCalls > 0) { - try { - Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS); - } catch (InterruptedException ie) { - // ignore - } - } else { - break; - } - } + result = u.delete(); switch (result) { case NEW: @@ -160,8 +141,9 @@ logger.atFine().log("Cannot delete current branch %s: %s", ref, result.name()); throw new ResourceConflictException("cannot delete current branch"); - case IO_FAILURE: case LOCK_FAILURE: + throw new LockFailureException(String.format("Cannot delete %s", ref), u); + case IO_FAILURE: case NOT_ATTEMPTED: case REJECTED: case RENAMED:
diff --git a/java/com/google/gerrit/server/restapi/project/GarbageCollect.java b/java/com/google/gerrit/server/restapi/project/GarbageCollect.java index 25a2c90..c5423e6 100644 --- a/java/com/google/gerrit/server/restapi/project/GarbageCollect.java +++ b/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
@@ -43,6 +43,7 @@ import java.util.Collections; import java.util.Optional; +/** REST endpoint that executes GC on a project. */ @RequiresCapability(GlobalCapability.RUN_GC) @Singleton public class GarbageCollect
diff --git a/java/com/google/gerrit/server/restapi/project/GetLabel.java b/java/com/google/gerrit/server/restapi/project/GetLabel.java new file mode 100644 index 0000000..626cb42 --- /dev/null +++ b/java/com/google/gerrit/server/restapi/project/GetLabel.java
@@ -0,0 +1,34 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.restapi.project; + +import com.google.gerrit.extensions.common.LabelDefinitionInfo; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestReadView; +import com.google.gerrit.server.project.LabelDefinitionJson; +import com.google.gerrit.server.project.LabelResource; +import com.google.inject.Singleton; + +@Singleton +public class GetLabel implements RestReadView<LabelResource> { + @Override + public Response<LabelDefinitionInfo> apply(LabelResource rsrc) + throws AuthException, BadRequestException { + return Response.ok( + LabelDefinitionJson.format(rsrc.getProject().getNameKey(), rsrc.getLabelType())); + } +}
diff --git a/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java b/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java new file mode 100644 index 0000000..1e288f4 --- /dev/null +++ b/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java
@@ -0,0 +1,93 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.restapi.project; + +import com.google.common.primitives.Shorts; +import com.google.gerrit.common.data.LabelFunction; +import com.google.gerrit.common.data.LabelType; +import com.google.gerrit.common.data.LabelValue; +import com.google.gerrit.common.data.PermissionRule; +import com.google.gerrit.entities.RefNames; +import com.google.gerrit.exceptions.InvalidNameException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.server.project.RefPattern; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; + +public class LabelDefinitionInputParser { + public static LabelFunction parseFunction(String functionString) throws BadRequestException { + Optional<LabelFunction> function = LabelFunction.parse(functionString.trim()); + return function.orElseThrow( + () -> new BadRequestException("unknown function: " + functionString)); + } + + public static List<LabelValue> parseValues(Map<String, String> values) + throws BadRequestException { + List<LabelValue> valueList = new ArrayList<>(); + Set<Short> allValues = new HashSet<>(); + for (Entry<String, String> e : values.entrySet()) { + short value; + try { + value = Shorts.checkedCast(PermissionRule.parseInt(e.getKey().trim())); + } catch (NumberFormatException ex) { + throw new BadRequestException("invalid value: " + e.getKey(), ex); + } + if (!allValues.add(value)) { + throw new BadRequestException("duplicate value: " + value); + } + String valueDescription = e.getValue().trim(); + if (valueDescription.isEmpty()) { + throw new BadRequestException("description for value '" + e.getKey() + "' cannot be empty"); + } + valueList.add(new LabelValue(value, valueDescription)); + } + return valueList; + } + + public static short parseDefaultValue(LabelType labelType, short defaultValue) + throws BadRequestException { + if (labelType.getValue(defaultValue) == null) { + throw new BadRequestException("invalid default value: " + defaultValue); + } + return defaultValue; + } + + public static List<String> parseBranches(List<String> branches) throws BadRequestException { + List<String> validBranches = new ArrayList<>(); + for (String branch : branches) { + String newBranch = branch.trim(); + if (newBranch.isEmpty()) { + continue; + } + if (!RefPattern.isRE(newBranch) && !newBranch.startsWith(RefNames.REFS)) { + newBranch = RefNames.REFS_HEADS + newBranch; + } + try { + RefPattern.validate(newBranch); + } catch (InvalidNameException e) { + throw new BadRequestException("invalid branch: " + branch, e); + } + validBranches.add(newBranch); + } + return validBranches; + } + + private LabelDefinitionInputParser() {} +}
diff --git a/java/com/google/gerrit/server/restapi/project/LabelsCollection.java b/java/com/google/gerrit/server/restapi/project/LabelsCollection.java new file mode 100644 index 0000000..0409729 --- /dev/null +++ b/java/com/google/gerrit/server/restapi/project/LabelsCollection.java
@@ -0,0 +1,81 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.restapi.project; + +import com.google.gerrit.common.data.LabelType; +import com.google.gerrit.extensions.registration.DynamicMap; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.ChildCollection; +import com.google.gerrit.extensions.restapi.IdString; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestView; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.ProjectPermission; +import com.google.gerrit.server.project.LabelResource; +import com.google.gerrit.server.project.ProjectResource; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +@Singleton +public class LabelsCollection implements ChildCollection<ProjectResource, LabelResource> { + private final Provider<ListLabels> list; + private final DynamicMap<RestView<LabelResource>> views; + private final Provider<CurrentUser> user; + private final PermissionBackend permissionBackend; + + @Inject + LabelsCollection( + Provider<ListLabels> list, + DynamicMap<RestView<LabelResource>> views, + Provider<CurrentUser> user, + PermissionBackend permissionBackend) { + this.list = list; + this.views = views; + this.user = user; + this.permissionBackend = permissionBackend; + } + + @Override + public RestView<ProjectResource> list() throws RestApiException { + return list.get(); + } + + @Override + public LabelResource parse(ProjectResource parent, IdString id) + throws AuthException, ResourceNotFoundException, PermissionBackendException { + if (!user.get().isIdentifiedUser()) { + throw new AuthException("Authentication required"); + } + + permissionBackend + .currentUser() + .project(parent.getNameKey()) + .check(ProjectPermission.READ_CONFIG); + LabelType labelType = parent.getProjectState().getConfig().getLabelSections().get(id.get()); + if (labelType == null) { + throw new ResourceNotFoundException(id); + } + return new LabelResource(parent, labelType); + } + + @Override + public DynamicMap<RestView<LabelResource>> views() { + return views; + } +}
diff --git a/java/com/google/gerrit/server/restapi/project/ListLabels.java b/java/com/google/gerrit/server/restapi/project/ListLabels.java new file mode 100644 index 0000000..19a8915 --- /dev/null +++ b/java/com/google/gerrit/server/restapi/project/ListLabels.java
@@ -0,0 +1,91 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.restapi.project; + +import com.google.gerrit.common.data.LabelType; +import com.google.gerrit.extensions.common.LabelDefinitionInfo; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestReadView; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.ProjectPermission; +import com.google.gerrit.server.project.LabelDefinitionJson; +import com.google.gerrit.server.project.ProjectResource; +import com.google.gerrit.server.project.ProjectState; +import com.google.inject.Inject; +import com.google.inject.Provider; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import org.kohsuke.args4j.Option; + +public class ListLabels implements RestReadView<ProjectResource> { + private final Provider<CurrentUser> user; + private final PermissionBackend permissionBackend; + + @Inject + public ListLabels(Provider<CurrentUser> user, PermissionBackend permissionBackend) { + this.user = user; + this.permissionBackend = permissionBackend; + } + + @Option(name = "--inherited", usage = "to include inherited label definitions") + private boolean inherited; + + public ListLabels withInherited(boolean inherited) { + this.inherited = inherited; + return this; + } + + @Override + public Response<List<LabelDefinitionInfo>> apply(ProjectResource rsrc) + throws AuthException, PermissionBackendException { + if (!user.get().isIdentifiedUser()) { + throw new AuthException("Authentication required"); + } + + if (inherited) { + List<LabelDefinitionInfo> allLabels = new ArrayList<>(); + for (ProjectState projectState : rsrc.getProjectState().treeInOrder()) { + try { + permissionBackend + .currentUser() + .project(projectState.getNameKey()) + .check(ProjectPermission.READ_CONFIG); + } catch (AuthException e) { + throw new AuthException(projectState.getNameKey() + ": " + e.getMessage(), e); + } + allLabels.addAll(listLabels(projectState)); + } + return Response.ok(allLabels); + } + + permissionBackend.currentUser().project(rsrc.getNameKey()).check(ProjectPermission.READ_CONFIG); + return Response.ok(listLabels(rsrc.getProjectState())); + } + + private List<LabelDefinitionInfo> listLabels(ProjectState projectState) { + Collection<LabelType> labelTypes = projectState.getConfig().getLabelSections().values(); + List<LabelDefinitionInfo> labels = new ArrayList<>(labelTypes.size()); + for (LabelType labelType : labelTypes) { + labels.add(LabelDefinitionJson.format(projectState.getNameKey(), labelType)); + } + labels.sort(Comparator.comparing(l -> l.name)); + return labels; + } +}
diff --git a/java/com/google/gerrit/server/restapi/project/ListTags.java b/java/com/google/gerrit/server/restapi/project/ListTags.java index 36cc1ac..8cea7f5 100644 --- a/java/com/google/gerrit/server/restapi/project/ListTags.java +++ b/java/com/google/gerrit/server/restapi/project/ListTags.java
@@ -41,8 +41,8 @@ import java.io.IOException; import java.sql.Timestamp; import java.util.ArrayList; +import java.util.Collection; import java.util.List; -import java.util.Map; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.PersonIdent; @@ -127,10 +127,10 @@ permissionBackend.currentUser().project(resource.getNameKey()); try (Repository repo = getRepository(resource.getNameKey()); RevWalk rw = new RevWalk(repo)) { - Map<String, Ref> all = + Collection<Ref> all = visibleTags( resource.getNameKey(), repo, repo.getRefDatabase().getRefsByPrefix(Constants.R_TAGS)); - for (Ref ref : all.values()) { + for (Ref ref : all) { tags.add( createTagInfo(perm.ref(ref.getName()), ref, rw, resource.getProjectState(), links)); } @@ -223,7 +223,7 @@ } } - private Map<String, Ref> visibleTags(Project.NameKey project, Repository repo, List<Ref> tags) + private Collection<Ref> visibleTags(Project.NameKey project, Repository repo, List<Ref> tags) throws PermissionBackendException { return permissionBackend .currentUser()
diff --git a/java/com/google/gerrit/server/restapi/project/Module.java b/java/com/google/gerrit/server/restapi/project/Module.java index de5661d..5b3ea30 100644 --- a/java/com/google/gerrit/server/restapi/project/Module.java +++ b/java/com/google/gerrit/server/restapi/project/Module.java
@@ -19,6 +19,7 @@ import static com.google.gerrit.server.project.CommitResource.COMMIT_KIND; import static com.google.gerrit.server.project.DashboardResource.DASHBOARD_KIND; import static com.google.gerrit.server.project.FileResource.FILE_KIND; +import static com.google.gerrit.server.project.LabelResource.LABEL_KIND; import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND; import static com.google.gerrit.server.project.TagResource.TAG_KIND; @@ -42,6 +43,7 @@ DynamicMap.mapOf(binder(), FILE_KIND); DynamicMap.mapOf(binder(), COMMIT_KIND); DynamicMap.mapOf(binder(), TAG_KIND); + DynamicMap.mapOf(binder(), LABEL_KIND); DynamicSet.bind(binder(), GerritConfigListener.class).to(SetParent.class); @@ -65,6 +67,13 @@ child(PROJECT_KIND, "children").to(ChildProjectsCollection.class); get(CHILD_PROJECT_KIND).to(GetChildProject.class); + child(PROJECT_KIND, "labels").to(LabelsCollection.class); + create(LABEL_KIND).to(CreateLabel.class); + get(LABEL_KIND).to(GetLabel.class); + put(LABEL_KIND).to(SetLabel.class); + delete(LABEL_KIND).to(DeleteLabel.class); + postOnCollection(LABEL_KIND).to(PostLabels.class); + get(PROJECT_KIND, "HEAD").to(GetHead.class); put(PROJECT_KIND, "HEAD").to(SetHead.class);
diff --git a/java/com/google/gerrit/server/restapi/project/PostLabels.java b/java/com/google/gerrit/server/restapi/project/PostLabels.java new file mode 100644 index 0000000..8835359 --- /dev/null +++ b/java/com/google/gerrit/server/restapi/project/PostLabels.java
@@ -0,0 +1,148 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.restapi.project; + +import com.google.common.base.Strings; +import com.google.gerrit.common.data.LabelType; +import com.google.gerrit.extensions.common.BatchLabelInput; +import com.google.gerrit.extensions.common.LabelDefinitionInput; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestCollectionModifyView; +import com.google.gerrit.extensions.restapi.UnprocessableEntityException; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.git.meta.MetaDataUpdate; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.ProjectPermission; +import com.google.gerrit.server.project.LabelResource; +import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.server.project.ProjectConfig; +import com.google.gerrit.server.project.ProjectResource; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import java.io.IOException; +import java.util.Map.Entry; +import org.eclipse.jgit.errors.ConfigInvalidException; + +/** REST endpoint that allows to add, update and delete label definitions in a batch. */ +@Singleton +public class PostLabels + implements RestCollectionModifyView<ProjectResource, LabelResource, BatchLabelInput> { + private final Provider<CurrentUser> user; + private final PermissionBackend permissionBackend; + private final MetaDataUpdate.User updateFactory; + private final ProjectConfig.Factory projectConfigFactory; + private final DeleteLabel deleteLabel; + private final CreateLabel createLabel; + private final SetLabel setLabel; + private final ProjectCache projectCache; + + @Inject + public PostLabels( + Provider<CurrentUser> user, + PermissionBackend permissionBackend, + MetaDataUpdate.User updateFactory, + ProjectConfig.Factory projectConfigFactory, + DeleteLabel deleteLabel, + CreateLabel createLabel, + SetLabel setLabel, + ProjectCache projectCache) { + this.user = user; + this.permissionBackend = permissionBackend; + this.updateFactory = updateFactory; + this.projectConfigFactory = projectConfigFactory; + this.deleteLabel = deleteLabel; + this.createLabel = createLabel; + this.setLabel = setLabel; + this.projectCache = projectCache; + } + + @Override + public Response<?> apply(ProjectResource rsrc, BatchLabelInput input) + throws AuthException, UnprocessableEntityException, PermissionBackendException, IOException, + ConfigInvalidException, BadRequestException, ResourceConflictException { + if (!user.get().isIdentifiedUser()) { + throw new AuthException("Authentication required"); + } + + permissionBackend + .currentUser() + .project(rsrc.getNameKey()) + .check(ProjectPermission.WRITE_CONFIG); + + if (input == null) { + input = new BatchLabelInput(); + } + + try (MetaDataUpdate md = updateFactory.create(rsrc.getNameKey())) { + boolean dirty = false; + + ProjectConfig config = projectConfigFactory.read(md); + + if (input.delete != null && !input.delete.isEmpty()) { + for (String labelName : input.delete) { + if (!deleteLabel.deleteLabel(config, labelName.trim())) { + throw new UnprocessableEntityException(String.format("label %s not found", labelName)); + } + } + dirty = true; + } + + if (input.create != null && !input.create.isEmpty()) { + for (LabelDefinitionInput labelInput : input.create) { + if (labelInput.name == null || labelInput.name.trim().isEmpty()) { + throw new BadRequestException("label name is required for new label"); + } + if (labelInput.commitMessage != null) { + throw new BadRequestException("commit message on label definition input not supported"); + } + createLabel.createLabel(config, labelInput.name.trim(), labelInput); + } + dirty = true; + } + + if (input.update != null && !input.update.isEmpty()) { + for (Entry<String, LabelDefinitionInput> e : input.update.entrySet()) { + LabelType labelType = config.getLabelSections().get(e.getKey().trim()); + if (labelType == null) { + throw new UnprocessableEntityException(String.format("label %s not found", e.getKey())); + } + if (e.getValue().commitMessage != null) { + throw new BadRequestException("commit message on label definition input not supported"); + } + setLabel.updateLabel(config, labelType, e.getValue()); + } + dirty = true; + } + + if (input.commitMessage != null) { + md.setMessage(Strings.emptyToNull(input.commitMessage.trim())); + } else { + md.setMessage("Update labels"); + } + + if (dirty) { + config.commit(md); + projectCache.evict(rsrc.getProjectState().getProject()); + } + } + + return Response.ok(""); + } +}
diff --git a/java/com/google/gerrit/server/restapi/project/PutConfig.java b/java/com/google/gerrit/server/restapi/project/PutConfig.java index 696ac37..a0badd7 100644 --- a/java/com/google/gerrit/server/restapi/project/PutConfig.java +++ b/java/com/google/gerrit/server/restapi/project/PutConfig.java
@@ -14,11 +14,17 @@ package com.google.gerrit.server.restapi.project; +import static com.google.gerrit.server.project.ProjectConfig.COMMENTLINK; +import static com.google.gerrit.server.project.ProjectConfig.KEY_ENABLED; +import static com.google.gerrit.server.project.ProjectConfig.KEY_LINK; +import static com.google.gerrit.server.project.ProjectConfig.KEY_MATCH; + import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.flogger.FluentLogger; import com.google.gerrit.entities.BooleanProjectConfig; import com.google.gerrit.entities.Project; +import com.google.gerrit.extensions.api.projects.CommentLinkInput; import com.google.gerrit.extensions.api.projects.ConfigInfo; import com.google.gerrit.extensions.api.projects.ConfigInput; import com.google.gerrit.extensions.api.projects.ConfigValue; @@ -59,6 +65,7 @@ import java.util.regex.Pattern; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.lib.Config; @Singleton public class PutConfig implements RestModifyView<ProjectResource, ConfigInput> { @@ -154,6 +161,10 @@ setPluginConfigValues(projectState, projectConfig, input.pluginConfigValues); } + if (input.commentLinks != null) { + updateCommentLinks(projectConfig, input.commentLinks); + } + md.setMessage("Modified project settings\n"); try { projectConfig.commit(md); @@ -278,6 +289,25 @@ } } + private void updateCommentLinks( + ProjectConfig projectConfig, Map<String, CommentLinkInput> input) { + for (Map.Entry<String, CommentLinkInput> e : input.entrySet()) { + String name = e.getKey(); + CommentLinkInput value = e.getValue(); + if (value != null) { + // Add or update the commentlink section + Config cfg = new Config(); + cfg.setString(COMMENTLINK, name, KEY_MATCH, value.match); + cfg.setString(COMMENTLINK, name, KEY_LINK, value.link); + cfg.setBoolean(COMMENTLINK, name, KEY_ENABLED, value.enabled == null || value.enabled); + projectConfig.addCommentLinkSection(ProjectConfig.buildCommentLink(cfg, name, false)); + } else { + // Delete the commentlink section + projectConfig.removeCommentLinkSection(name); + } + } + } + private static void validateProjectConfigEntryIsEditable( ProjectConfigEntry projectConfigEntry, ProjectState projectState,
diff --git a/java/com/google/gerrit/server/restapi/project/QueryProjects.java b/java/com/google/gerrit/server/restapi/project/QueryProjects.java index 7066d9a..e4f7df5 100644 --- a/java/com/google/gerrit/server/restapi/project/QueryProjects.java +++ b/java/com/google/gerrit/server/restapi/project/QueryProjects.java
@@ -31,6 +31,7 @@ import com.google.gerrit.server.query.project.ProjectQueryBuilder; import com.google.gerrit.server.query.project.ProjectQueryProcessor; import com.google.inject.Inject; +import com.google.inject.Provider; import java.util.ArrayList; import java.util.List; import org.kohsuke.args4j.Option; @@ -38,7 +39,7 @@ public class QueryProjects implements RestReadView<TopLevelResource> { private final ProjectIndexCollection indexes; private final ProjectQueryBuilder queryBuilder; - private final ProjectQueryProcessor queryProcessor; + private final Provider<ProjectQueryProcessor> queryProcessorProvider; private final ProjectJson json; private String query; @@ -78,11 +79,11 @@ protected QueryProjects( ProjectIndexCollection indexes, ProjectQueryBuilder queryBuilder, - ProjectQueryProcessor queryProcessor, + Provider<ProjectQueryProcessor> queryProcessorProvider, ProjectJson json) { this.indexes = indexes; this.queryBuilder = queryBuilder; - this.queryProcessor = queryProcessor; + this.queryProcessorProvider = queryProcessorProvider; this.json = json; } @@ -102,6 +103,8 @@ throw new MethodNotAllowedException("no project index"); } + ProjectQueryProcessor queryProcessor = queryProcessorProvider.get(); + if (start != 0) { queryProcessor.setStart(start); }
diff --git a/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java b/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java index bac71de..8879fae 100644 --- a/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java +++ b/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java
@@ -24,9 +24,11 @@ import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 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.server.git.meta.MetaDataUpdate; import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.permissions.ProjectPermission; import com.google.gerrit.server.project.DashboardResource; import com.google.gerrit.server.project.ProjectCache; @@ -34,6 +36,7 @@ import com.google.gerrit.server.project.ProjectResource; import com.google.inject.Inject; import com.google.inject.Provider; +import java.io.IOException; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.kohsuke.args4j.Option; @@ -67,7 +70,7 @@ @Override public Response<DashboardInfo> apply(DashboardResource rsrc, SetDashboardInput input) - throws Exception { + throws RestApiException, IOException, PermissionBackendException { if (input == null) { input = new SetDashboardInput(); // Delete would set input to null. }
diff --git a/java/com/google/gerrit/server/restapi/project/SetHead.java b/java/com/google/gerrit/server/restapi/project/SetHead.java index 42c0559..946695e 100644 --- a/java/com/google/gerrit/server/restapi/project/SetHead.java +++ b/java/com/google/gerrit/server/restapi/project/SetHead.java
@@ -25,6 +25,7 @@ import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; +import com.google.gerrit.git.LockFailureException; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.extensions.events.AbstractNoNotifyEvent; import com.google.gerrit.server.git.GitRepositoryManager; @@ -96,9 +97,10 @@ case FORCED: case NEW: break; + case LOCK_FAILURE: + throw new LockFailureException("Setting HEAD failed", u); case FAST_FORWARD: case IO_FAILURE: - case LOCK_FAILURE: case NOT_ATTEMPTED: case REJECTED: case REJECTED_CURRENT_BRANCH:
diff --git a/java/com/google/gerrit/server/restapi/project/SetLabel.java b/java/com/google/gerrit/server/restapi/project/SetLabel.java new file mode 100644 index 0000000..824b4ed --- /dev/null +++ b/java/com/google/gerrit/server/restapi/project/SetLabel.java
@@ -0,0 +1,222 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.restapi.project; + +import com.google.common.base.Strings; +import com.google.gerrit.common.data.LabelType; +import com.google.gerrit.extensions.common.LabelDefinitionInfo; +import com.google.gerrit.extensions.common.LabelDefinitionInput; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.git.meta.MetaDataUpdate; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.ProjectPermission; +import com.google.gerrit.server.project.LabelDefinitionJson; +import com.google.gerrit.server.project.LabelResource; +import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.server.project.ProjectConfig; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import java.io.IOException; +import org.eclipse.jgit.errors.ConfigInvalidException; + +@Singleton +public class SetLabel implements RestModifyView<LabelResource, LabelDefinitionInput> { + private final Provider<CurrentUser> user; + private final PermissionBackend permissionBackend; + private final MetaDataUpdate.User updateFactory; + private final ProjectConfig.Factory projectConfigFactory; + private final ProjectCache projectCache; + + @Inject + public SetLabel( + Provider<CurrentUser> user, + PermissionBackend permissionBackend, + MetaDataUpdate.User updateFactory, + ProjectConfig.Factory projectConfigFactory, + ProjectCache projectCache) { + this.user = user; + this.permissionBackend = permissionBackend; + this.updateFactory = updateFactory; + this.projectConfigFactory = projectConfigFactory; + this.projectCache = projectCache; + } + + @Override + public Response<LabelDefinitionInfo> apply(LabelResource rsrc, LabelDefinitionInput input) + throws AuthException, BadRequestException, ResourceConflictException, + PermissionBackendException, IOException, ConfigInvalidException { + if (!user.get().isIdentifiedUser()) { + throw new AuthException("Authentication required"); + } + + permissionBackend + .currentUser() + .project(rsrc.getProject().getNameKey()) + .check(ProjectPermission.WRITE_CONFIG); + + if (input == null) { + input = new LabelDefinitionInput(); + } + + LabelType labelType = rsrc.getLabelType(); + + try (MetaDataUpdate md = updateFactory.create(rsrc.getProject().getNameKey())) { + ProjectConfig config = projectConfigFactory.read(md); + + if (updateLabel(config, labelType, input)) { + if (input.commitMessage != null) { + md.setMessage(Strings.emptyToNull(input.commitMessage.trim())); + } else { + md.setMessage("Update label"); + } + + config.commit(md); + projectCache.evict(rsrc.getProject().getProjectState().getProject()); + } + } + return Response.ok(LabelDefinitionJson.format(rsrc.getProject().getNameKey(), labelType)); + } + + /** + * Updates the given label. + * + * @param config the project config + * @param labelType the label type that should be updated + * @param input the input that describes the label update + * @return whether the label type was modified + * @throws BadRequestException if there was invalid data in the input + * @throws ResourceConflictException if the update cannot be applied due to a conflict + */ + public boolean updateLabel(ProjectConfig config, LabelType labelType, LabelDefinitionInput input) + throws BadRequestException, ResourceConflictException { + boolean dirty = false; + + config.getLabelSections().remove(labelType.getName()); + + if (input.name != null) { + String newName = input.name.trim(); + if (newName.isEmpty()) { + throw new BadRequestException("name cannot be empty"); + } + if (!newName.equals(labelType.getName())) { + if (config.getLabelSections().containsKey(newName)) { + throw new ResourceConflictException(String.format("name %s already in use", newName)); + } + + for (String labelName : config.getLabelSections().keySet()) { + if (labelName.equalsIgnoreCase(newName)) { + throw new ResourceConflictException( + String.format("name %s conflicts with existing label %s", newName, labelName)); + } + } + + try { + labelType.setName(newName); + } catch (IllegalArgumentException e) { + throw new BadRequestException("invalid name: " + input.name, e); + } + dirty = true; + } + } + + if (input.function != null) { + if (input.function.trim().isEmpty()) { + throw new BadRequestException("function cannot be empty"); + } + labelType.setFunction(LabelDefinitionInputParser.parseFunction(input.function)); + dirty = true; + } + + if (input.values != null) { + if (input.values.isEmpty()) { + throw new BadRequestException("values cannot be empty"); + } + labelType.setValues(LabelDefinitionInputParser.parseValues(input.values)); + dirty = true; + } + + if (input.defaultValue != null) { + labelType.setDefaultValue( + LabelDefinitionInputParser.parseDefaultValue(labelType, input.defaultValue)); + dirty = true; + } + + if (input.branches != null) { + labelType.setRefPatterns(LabelDefinitionInputParser.parseBranches(input.branches)); + dirty = true; + } + + if (input.canOverride != null) { + labelType.setCanOverride(input.canOverride); + dirty = true; + } + + if (input.copyAnyScore != null) { + labelType.setCopyAnyScore(input.copyAnyScore); + dirty = true; + } + + if (input.copyMinScore != null) { + labelType.setCopyMinScore(input.copyMinScore); + dirty = true; + } + + if (input.copyMaxScore != null) { + labelType.setCopyMaxScore(input.copyMaxScore); + dirty = true; + } + + if (input.copyAllScoresIfNoChange != null) { + labelType.setCopyAllScoresIfNoChange(input.copyAllScoresIfNoChange); + } + + if (input.copyAllScoresIfNoCodeChange != null) { + labelType.setCopyAllScoresIfNoCodeChange(input.copyAllScoresIfNoCodeChange); + dirty = true; + } + + if (input.copyAllScoresOnTrivialRebase != null) { + labelType.setCopyAllScoresOnTrivialRebase(input.copyAllScoresOnTrivialRebase); + dirty = true; + } + + if (input.copyAllScoresOnMergeFirstParentUpdate != null) { + labelType.setCopyAllScoresOnMergeFirstParentUpdate( + input.copyAllScoresOnMergeFirstParentUpdate); + dirty = true; + } + + if (input.allowPostSubmit != null) { + labelType.setAllowPostSubmit(input.allowPostSubmit); + dirty = true; + } + + if (input.ignoreSelfApproval != null) { + labelType.setIgnoreSelfApproval(input.ignoreSelfApproval); + dirty = true; + } + + config.getLabelSections().put(labelType.getName(), labelType); + + return dirty; + } +}
diff --git a/java/com/google/gerrit/server/rules/PrologEnvironment.java b/java/com/google/gerrit/server/rules/PrologEnvironment.java index ba78724..1a563ad 100644 --- a/java/com/google/gerrit/server/rules/PrologEnvironment.java +++ b/java/com/google/gerrit/server/rules/PrologEnvironment.java
@@ -205,17 +205,8 @@ this.anonymousUser = anonymousUser; this.patchsetUtil = patchsetUtil; this.emails = emails; - - int limit = config.getInt("rules", null, "reductionLimit", 100000); - reductionLimit = limit <= 0 ? Integer.MAX_VALUE : limit; - - limit = - config.getInt( - "rules", - null, - "compileReductionLimit", - (int) Math.min(10L * limit, Integer.MAX_VALUE)); - compileLimit = limit <= 0 ? Integer.MAX_VALUE : limit; + this.reductionLimit = RuleUtil.reductionLimit(config); + this.compileLimit = RuleUtil.compileReductionLimit(config); logger.atInfo().log("reductionLimit: %d, compileLimit: %d", reductionLimit, compileLimit); }
diff --git a/java/com/google/gerrit/server/rules/RuleUtil.java b/java/com/google/gerrit/server/rules/RuleUtil.java new file mode 100644 index 0000000..f4e7eff --- /dev/null +++ b/java/com/google/gerrit/server/rules/RuleUtil.java
@@ -0,0 +1,45 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.rules; + +import org.eclipse.jgit.lib.Config; + +/** Provides utility methods for configuring and running Prolog rules inside Gerrit. */ +public class RuleUtil { + + /** + * Returns the reduction limit to be applied to the Prolog machine to prevent infinite loops and + * other forms of computational overflow. + */ + public static int reductionLimit(Config gerritConfig) { + int limit = gerritConfig.getInt("rules", null, "reductionLimit", 100000); + return limit <= 0 ? Integer.MAX_VALUE : limit; + } + + /** + * Returns the compile reduction limit to be applied to the Prolog machine to prevent infinite + * loops and other forms of computational overflow. The compiled reduction limit should be used + * when user-provided Prolog code is compiled by the interpreter before the limit gets applied. + */ + public static int compileReductionLimit(Config gerritConfig) { + int limit = + gerritConfig.getInt( + "rules", + null, + "compileReductionLimit", + (int) Math.min(10L * reductionLimit(gerritConfig), Integer.MAX_VALUE)); + return limit <= 0 ? Integer.MAX_VALUE : limit; + } +}
diff --git a/java/com/google/gerrit/server/rules/RulesCache.java b/java/com/google/gerrit/server/rules/RulesCache.java index 0cd21a4..ed67e68 100644 --- a/java/com/google/gerrit/server/rules/RulesCache.java +++ b/java/com/google/gerrit/server/rules/RulesCache.java
@@ -87,6 +87,7 @@ private final boolean enableProjectRules; private final int maxDbSize; + private final int compileReductionLimit; private final int maxSrcBytes; private final Path cacheDir; private final Path rulesDir; @@ -104,6 +105,7 @@ PluginSetContext<PredicateProvider> predicateProviders, @Named(CACHE_NAME) Cache<ObjectId, PrologMachineCopy> machineCache) { maxDbSize = config.getInt("rules", null, "maxPrologDatabaseSize", 256); + compileReductionLimit = RuleUtil.compileReductionLimit(config); maxSrcBytes = config.getInt("rules", null, "maxSourceBytes", 128 << 10); enableProjectRules = config.getBoolean("rules", null, "enable", true) && maxSrcBytes > 0; cacheDir = site.resolve(config.getString("cache", null, "directory")); @@ -252,6 +254,9 @@ private BufferingPrologControl newEmptyMachine(ClassLoader cl) { BufferingPrologControl ctl = new BufferingPrologControl(); ctl.setMaxDatabaseSize(maxDbSize); + // Use the compiled reduction limit because the first term evaluation is done with + // consult_stream - an internal, combined Prolog term. + ctl.setReductionLimit(compileReductionLimit); ctl.setPrologClassLoader( new PrologClassLoader(new PredicateClassLoader(predicateProviders, cl))); ctl.setEnabled(EnumSet.allOf(Prolog.Feature.class), false);
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java index adb75a4..79ae43b 100644 --- a/java/com/google/gerrit/server/submit/MergeOp.java +++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -16,7 +16,6 @@ import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkState; import static java.util.Comparator.comparing; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toSet; @@ -41,6 +40,7 @@ import com.google.gerrit.entities.ChangeMessage; import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.Project; +import com.google.gerrit.entities.SubmissionId; import com.google.gerrit.exceptions.StorageException; import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.api.changes.SubmitInput; @@ -76,7 +76,6 @@ import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.server.update.RetryHelper; -import com.google.gerrit.server.update.RetryHelper.ActionType; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.time.TimeUtil; import com.google.inject.Inject; @@ -91,7 +90,6 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; @@ -238,7 +236,7 @@ private final Map<Change.Id, Change> updatedChanges; private Timestamp ts; - private RequestId submissionId; + private SubmissionId submissionId; private IdentifiedUser caller; private MergeOpRepoManager orm; @@ -248,7 +246,6 @@ private Set<Project.NameKey> allProjects; private boolean dryrun; private TopicMetrics topicMetrics; - private String traceId; @Inject MergeOp( @@ -451,7 +448,7 @@ this.dryrun = dryrun; this.caller = caller; this.ts = TimeUtil.nowTs(); - this.submissionId = new RequestId(change.getId().toString()); + this.submissionId = new SubmissionId(change); try (TraceContext traceContext = TraceContext.open().addTag(RequestId.Type.SUBMISSION_ID, submissionId)) { @@ -461,11 +458,17 @@ try { ChangeSet indexBackedChangeSet = mergeSuperSet.setMergeOpRepoManager(orm).completeChangeSet(change, caller); - checkState( - indexBackedChangeSet.ids().contains(change.getId()), - "change %s missing from %s", - change.getId(), - indexBackedChangeSet); + if (!indexBackedChangeSet.ids().contains(change.getId())) { + // indexBackedChangeSet contains only open changes, if the change is missing in this set + // it might be that the change was concurrently submitted in the meantime. + change = changeDataFactory.create(change).reloadChange(); + if (!change.isNew()) { + throw new ResourceConflictException("change is " + ChangeUtil.status(change)); + } + throw new IllegalStateException( + String.format("change %s missing from %s", change.getId(), indexBackedChangeSet)); + } + if (indexBackedChangeSet.furtherHiddenChanges()) { throw new AuthException( "A change to be submitted with " + change.getId() + " is not visible"); @@ -484,44 +487,39 @@ } RetryTracker retryTracker = new RetryTracker(); - retryHelper.execute( - updateFactory -> { - long attempt = retryTracker.lastAttemptNumber + 1; - boolean isRetry = attempt > 1; - if (isRetry) { - logger.atFine().log("Retrying, attempt #%d; skipping merged changes", attempt); - this.ts = TimeUtil.nowTs(); - openRepoManager(); - } - this.commitStatus = new CommitStatus(cs, isRetry); - if (checkSubmitRules) { - logger.atFine().log("Checking submit rules and state"); - checkSubmitRulesAndState(cs, isRetry); - } else { - logger.atFine().log("Bypassing submit rules"); - bypassSubmitRules(cs, isRetry); - } - try { - integrateIntoHistory(cs); - } catch (IntegrationException e) { - logger.atSevere().withCause(e).log("Error from integrateIntoHistory"); - throw new ResourceConflictException(e.getMessage(), e); - } - return null; - }, - RetryHelper.options() - .listener(retryTracker) - // Up to the entire submit operation is retried, including possibly many projects. - // Multiply the timeout by the number of projects we're actually attempting to - // submit. - .timeout( - retryHelper - .getDefaultTimeout(ActionType.CHANGE_UPDATE) - .multipliedBy(cs.projects().size())) - .caller(getClass()) - .retryWithTrace(t -> !(t instanceof RestApiException)) - .onAutoTrace(traceId -> this.traceId = traceId) - .build()); + retryHelper + .changeUpdate( + "integrateIntoHistory", + updateFactory -> { + long attempt = retryTracker.lastAttemptNumber + 1; + boolean isRetry = attempt > 1; + if (isRetry) { + logger.atFine().log("Retrying, attempt #%d; skipping merged changes", attempt); + this.ts = TimeUtil.nowTs(); + openRepoManager(); + } + this.commitStatus = new CommitStatus(cs, isRetry); + if (checkSubmitRules) { + logger.atFine().log("Checking submit rules and state"); + checkSubmitRulesAndState(cs, isRetry); + } else { + logger.atFine().log("Bypassing submit rules"); + bypassSubmitRules(cs, isRetry); + } + try { + integrateIntoHistory(cs); + } catch (IntegrationException e) { + logger.atWarning().withCause(e).log("Error from integrateIntoHistory"); + throw new ResourceConflictException(e.getMessage(), e); + } + return null; + }) + .listener(retryTracker) + // Up to the entire submit operation is retried, including possibly many projects. + // Multiply the timeout by the number of projects we're actually attempting to + // submit. + .defaultTimeoutMultiplier(cs.projects().size()) + .call(); if (projects > 1) { topicMetrics.topicSubmissionsCompleted.increment(); @@ -541,10 +539,6 @@ } } - public Optional<String> getTraceId() { - return Optional.ofNullable(traceId); - } - private void openRepoManager() { if (orm != null) { orm.close();
diff --git a/java/com/google/gerrit/server/submit/MergeOpRepoManager.java b/java/com/google/gerrit/server/submit/MergeOpRepoManager.java index c2577e7..9a7ced5 100644 --- a/java/com/google/gerrit/server/submit/MergeOpRepoManager.java +++ b/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
@@ -42,10 +42,9 @@ import java.util.Map; import java.util.Objects; import org.eclipse.jgit.errors.RepositoryNotFoundException; -import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectReader; -import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevFlag; import org.eclipse.jgit.revwalk.RevSort; @@ -130,19 +129,17 @@ } public static class OpenBranch { - final RefUpdate update; final CodeReviewCommit oldTip; MergeTip mergeTip; OpenBranch(OpenRepo or, BranchNameKey name) throws IntegrationException { try { - update = or.repo.updateRef(name.branch()); - if (update.getOldObjectId() != null) { - oldTip = or.rw.parseCommit(update.getOldObjectId()); + Ref ref = or.getRepo().exactRef(name.branch()); + if (ref != null) { + oldTip = or.rw.parseCommit(ref.getObjectId()); } else if (Objects.equals(or.repo.getFullBranch(), name.branch()) || Objects.equals(RefNames.REFS_CONFIG, name.branch())) { oldTip = null; - update.setExpectedOldObjectId(ObjectId.zeroId()); } else { throw new IntegrationException( "The destination branch " + name + " does not exist anymore.");
diff --git a/java/com/google/gerrit/server/submit/MergeSuperSet.java b/java/com/google/gerrit/server/submit/MergeSuperSet.java index bcebc7f..b36eb7d 100644 --- a/java/com/google/gerrit/server/submit/MergeSuperSet.java +++ b/java/com/google/gerrit/server/submit/MergeSuperSet.java
@@ -19,16 +19,19 @@ import com.google.common.base.Strings; import com.google.common.collect.Iterables; +import com.google.common.flogger.FluentLogger; import com.google.gerrit.entities.Change; import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.logging.TraceContext; +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.plugincontext.PluginContext; +import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectState; import com.google.gerrit.server.query.change.ChangeData; @@ -53,6 +56,7 @@ * included. */ public class MergeSuperSet { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private final Provider<InternalChangeQuery> queryProvider; private final Provider<MergeOpRepoManager> repoManagerProvider; @@ -60,6 +64,7 @@ private final PermissionBackend permissionBackend; private final Config cfg; private final ProjectCache projectCache; + private final ChangeNotes.Factory notesFactory; private MergeOpRepoManager orm; private boolean closeOrm; @@ -71,13 +76,15 @@ Provider<MergeOpRepoManager> repoManagerProvider, DynamicItem<MergeSuperSetComputation> mergeSuperSetComputation, PermissionBackend permissionBackend, - ProjectCache projectCache) { + ProjectCache projectCache, + ChangeNotes.Factory notesFactory) { this.cfg = cfg; this.queryProvider = queryProvider; this.repoManagerProvider = repoManagerProvider; this.mergeSuperSetComputation = mergeSuperSetComputation; this.permissionBackend = permissionBackend; this.projectCache = projectCache; + this.notesFactory = notesFactory; } public static boolean wholeTopicEnabled(Config config) { @@ -215,8 +222,23 @@ return false; } + ChangeNotes notes; try { - permissionBackend.user(user).change(cd).check(ChangePermission.READ); + notes = cd.notes(); + } catch (NoSuchChangeException e) { + // The change was found in the index but is missing in NoteDb. + // This can happen in systems with multiple primary nodes when the replication of the index + // documents is faster than the replication of the Git data. + // Instead of failing, create the change notes from the index data so that the read permission + // check can be performed successfully. + logger.atWarning().log( + "Got change %d of project %s from index, but couldn't find it in NoteDb", + cd.getId().get(), cd.project().get()); + notes = notesFactory.createFromIndexedChange(cd.change()); + } + + try { + permissionBackend.user(user).change(notes).check(ChangePermission.READ); return true; } catch (AuthException e) { return false;
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategy.java b/java/com/google/gerrit/server/submit/SubmitStrategy.java index 4c68e1b..efb2f76 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.Sets; import com.google.gerrit.entities.BranchNameKey; import com.google.gerrit.entities.Change; +import com.google.gerrit.entities.SubmissionId; import com.google.gerrit.extensions.api.changes.SubmitInput; import com.google.gerrit.extensions.client.SubmitType; import com.google.gerrit.extensions.config.FactoryModule; @@ -42,7 +43,6 @@ import com.google.gerrit.server.git.MergeUtil; import com.google.gerrit.server.git.TagCache; import com.google.gerrit.server.git.validators.OnSubmitValidators; -import com.google.gerrit.server.logging.RequestId; import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectConfig; @@ -94,7 +94,7 @@ RevFlag canMergeFlag, Set<RevCommit> alreadyAccepted, Set<CodeReviewCommit> incoming, - RequestId submissionId, + SubmissionId submissionId, SubmitInput submitInput, SubmoduleOp submoduleOp, boolean dryrun); @@ -125,7 +125,7 @@ final MergeTip mergeTip; final RevFlag canMergeFlag; final Set<RevCommit> alreadyAccepted; - final RequestId submissionId; + final SubmissionId submissionId; final SubmitType submitType; final SubmitInput submitInput; final SubmoduleOp submoduleOp; @@ -164,7 +164,7 @@ @Assisted RevFlag canMergeFlag, @Assisted Set<RevCommit> alreadyAccepted, @Assisted Set<CodeReviewCommit> incoming, - @Assisted RequestId submissionId, + @Assisted SubmissionId submissionId, @Assisted SubmitType submitType, @Assisted SubmitInput submitInput, @Assisted SubmoduleOp submoduleOp,
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java b/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java index cba572bc..a015665 100644 --- a/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java +++ b/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java
@@ -16,13 +16,13 @@ import com.google.common.flogger.FluentLogger; import com.google.gerrit.entities.BranchNameKey; +import com.google.gerrit.entities.SubmissionId; import com.google.gerrit.extensions.api.changes.SubmitInput; import com.google.gerrit.extensions.client.SubmitType; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.git.CodeReviewCommit; import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk; import com.google.gerrit.server.git.MergeTip; -import com.google.gerrit.server.logging.RequestId; import com.google.gerrit.server.submit.MergeOp.CommitStatus; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -52,7 +52,7 @@ IdentifiedUser caller, MergeTip mergeTip, CommitStatus commitStatus, - RequestId submissionId, + SubmissionId submissionId, SubmitInput submitInput, SubmoduleOp submoduleOp, boolean dryrun)
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java index b7bc58a..6be4a91 100644 --- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java +++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -453,7 +453,7 @@ Change c = ctx.getChange(); logger.atFine().log("Setting change %s merged", c.getId()); c.setStatus(Change.Status.MERGED); - c.setSubmissionId(args.submissionId.toStringForStorage()); + c.setSubmissionId(args.submissionId.toString()); // TODO(dborowitz): We need to be able to change the author of the message, // which is not the user from the update context. addMergedMessage was able
diff --git a/java/com/google/gerrit/server/update/BatchUpdate.java b/java/com/google/gerrit/server/update/BatchUpdate.java index ce16706..1e86d52 100644 --- a/java/com/google/gerrit/server/update/BatchUpdate.java +++ b/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -52,8 +52,8 @@ import com.google.gerrit.server.logging.RequestId; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.ChangeUpdate; +import com.google.gerrit.server.notedb.LimitExceededException; import com.google.gerrit.server.notedb.NoteDbUpdateManager; -import com.google.gerrit.server.notedb.TooManyUpdatesException; import com.google.gerrit.server.project.InvalidChangeOperationException; import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.project.NoSuchProjectException; @@ -128,23 +128,23 @@ try { List<ListenableFuture<?>> indexFutures = new ArrayList<>(); - List<ChangesHandle> handles = new ArrayList<>(updates.size()); + List<ChangesHandle> changesHandles = new ArrayList<>(updates.size()); try { for (BatchUpdate u : updates) { u.executeUpdateRepo(); } listener.afterUpdateRepos(); for (BatchUpdate u : updates) { - handles.add(u.executeChangeOps(dryrun)); + changesHandles.add(u.executeChangeOps(dryrun)); } - for (ChangesHandle h : handles) { + for (ChangesHandle h : changesHandles) { h.execute(); indexFutures.addAll(h.startIndexFutures()); } listener.afterUpdateRefs(); listener.afterUpdateChanges(); } finally { - for (ChangesHandle h : handles) { + for (ChangesHandle h : changesHandles) { h.close(); } } @@ -181,7 +181,7 @@ private static void wrapAndThrowException(Exception e) throws UpdateException, RestApiException { // Convert common non-REST exception types with user-visible messages to corresponding REST // exception types. - if (e instanceof InvalidChangeOperationException || e instanceof TooManyUpdatesException) { + if (e instanceof InvalidChangeOperationException || e instanceof LimitExceededException) { throw new ResourceConflictException(e.getMessage(), e); } else if (e instanceof NoSuchChangeException || e instanceof NoSuchRefException
diff --git a/java/com/google/gerrit/server/update/RetryHelper.java b/java/com/google/gerrit/server/update/RetryHelper.java index bea3867..e13cab1 100644 --- a/java/com/google/gerrit/server/update/RetryHelper.java +++ b/java/com/google/gerrit/server/update/RetryHelper.java
@@ -28,14 +28,11 @@ import com.github.rholder.retry.WaitStrategy; import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Throwables; -import com.google.common.collect.Maps; import com.google.common.flogger.FluentLogger; import com.google.gerrit.common.Nullable; -import com.google.gerrit.extensions.restapi.RestApiException; -import com.google.gerrit.git.LockFailureException; -import com.google.gerrit.metrics.Counter1; -import com.google.gerrit.metrics.Counter2; +import com.google.gerrit.common.UsedAt; +import com.google.gerrit.exceptions.StorageException; +import com.google.gerrit.metrics.Counter3; import com.google.gerrit.metrics.Description; import com.google.gerrit.metrics.Field; import com.google.gerrit.metrics.MetricMaker; @@ -45,10 +42,18 @@ import com.google.gerrit.server.logging.RequestId; import com.google.gerrit.server.logging.TraceContext; import com.google.gerrit.server.plugincontext.PluginSetContext; +import com.google.gerrit.server.query.account.InternalAccountQuery; +import com.google.gerrit.server.query.change.InternalChangeQuery; +import com.google.gerrit.server.update.RetryableAction.Action; +import com.google.gerrit.server.update.RetryableAction.ActionType; +import com.google.gerrit.server.update.RetryableChangeAction.ChangeAction; +import com.google.gerrit.server.update.RetryableIndexQueryAction.IndexQueryAction; import com.google.inject.Inject; +import com.google.inject.Provider; import com.google.inject.Singleton; import java.time.Duration; import java.util.Arrays; +import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutionException; @@ -60,24 +65,6 @@ public class RetryHelper { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - @FunctionalInterface - public interface ChangeAction<T> { - T call(BatchUpdate.Factory batchUpdateFactory) throws Exception; - } - - @FunctionalInterface - public interface Action<T> { - T call() throws Exception; - } - - public enum ActionType { - ACCOUNT_UPDATE, - CHANGE_UPDATE, - GROUP_UPDATE, - INDEX_QUERY, - PLUGIN_UPDATE - } - /** * Options for retrying a single operation. * @@ -100,7 +87,7 @@ @Nullable abstract Duration timeout(); - abstract Optional<Class<?>> caller(); + abstract Optional<String> actionName(); abstract Optional<Predicate<Throwable>> retryWithTrace(); @@ -112,7 +99,7 @@ public abstract Builder timeout(Duration timeout); - public abstract Builder caller(Class<?> caller); + public abstract Builder actionName(String caller); public abstract Builder retryWithTrace(Predicate<Throwable> exceptionPredicate); @@ -125,15 +112,28 @@ @VisibleForTesting @Singleton public static class Metrics { - final Counter1<ActionType> attemptCounts; - final Counter1<ActionType> timeoutCount; - final Counter2<ActionType, String> autoRetryCount; - final Counter2<ActionType, String> failuresOnAutoRetryCount; + final Counter3<String, String, String> attemptCounts; + final Counter3<String, String, String> timeoutCount; + final Counter3<String, String, String> autoRetryCount; + final Counter3<String, String, String> failuresOnAutoRetryCount; @Inject Metrics(MetricMaker metricMaker) { - Field<ActionType> actionTypeField = - Field.ofEnum(ActionType.class, "action_type", Metadata.Builder::actionType).build(); + Field<String> actionTypeField = + Field.ofString("action_type", Metadata.Builder::actionType).build(); + Field<String> operationNameField = + Field.ofString("operation_name", Metadata.Builder::operationName) + .description("The name of the operation that was retried.") + .build(); + Field<String> originalCauseField = + Field.ofString("cause", Metadata.Builder::cause) + .description("The original cause that triggered the retry.") + .build(); + Field<String> causeField = + Field.ofString("cause", Metadata.Builder::cause) + .description("The cause for the retry.") + .build(); + attemptCounts = metricMaker.newCounter( "action/retry_attempt_count", @@ -142,7 +142,9 @@ + " (0 == single attempt, no retry)") .setCumulative() .setUnit("attempts"), - actionTypeField); + actionTypeField, + operationNameField, + originalCauseField); timeoutCount = metricMaker.newCounter( "action/retry_timeout_count", @@ -150,7 +152,9 @@ "Number of action executions of RetryHelper that ultimately timed out") .setCumulative() .setUnit("timeouts"), - actionTypeField); + actionTypeField, + operationNameField, + originalCauseField); autoRetryCount = metricMaker.newCounter( "action/auto_retry_count", @@ -158,9 +162,8 @@ .setCumulative() .setUnit("retries"), actionTypeField, - Field.ofString("operation_name", Metadata.Builder::operationName) - .description("The name of the operation that was retried.") - .build()); + operationNameField, + causeField); failuresOnAutoRetryCount = metricMaker.newCounter( "action/failures_on_auto_retry_count", @@ -168,9 +171,8 @@ .setCumulative() .setUnit("failures"), actionTypeField, - Field.ofString("operation_name", Metadata.Builder::operationName) - .description("The name of the operation that was retried.") - .build()); + operationNameField, + causeField); } } @@ -178,14 +180,14 @@ return new AutoValue_RetryHelper_Options.Builder(); } - private static Options defaults() { - return options().build(); - } - + private final Config cfg; private final Metrics metrics; private final BatchUpdate.Factory updateFactory; + private final Provider<InternalAccountQuery> internalAccountQuery; + private final Provider<InternalChangeQuery> internalChangeQuery; private final PluginSetContext<ExceptionHook> exceptionHooks; - private final Map<ActionType, Duration> defaultTimeouts; + private final Duration defaultTimeout; + private final Map<String, Duration> defaultTimeouts; private final WaitStrategy waitStrategy; @Nullable private final Consumer<RetryerBuilder<?>> overwriteDefaultRetryerStrategySetup; private final boolean retryWithTraceOnFailure; @@ -195,8 +197,17 @@ @GerritServerConfig Config cfg, Metrics metrics, PluginSetContext<ExceptionHook> exceptionHooks, - BatchUpdate.Factory updateFactory) { - this(cfg, metrics, updateFactory, exceptionHooks, null); + BatchUpdate.Factory updateFactory, + Provider<InternalAccountQuery> internalAccountQuery, + Provider<InternalChangeQuery> internalChangeQuery) { + this( + cfg, + metrics, + updateFactory, + internalAccountQuery, + internalChangeQuery, + exceptionHooks, + null); } @VisibleForTesting @@ -204,21 +215,25 @@ @GerritServerConfig Config cfg, Metrics metrics, BatchUpdate.Factory updateFactory, + Provider<InternalAccountQuery> internalAccountQuery, + Provider<InternalChangeQuery> internalChangeQuery, PluginSetContext<ExceptionHook> exceptionHooks, @Nullable Consumer<RetryerBuilder<?>> overwriteDefaultRetryerStrategySetup) { + this.cfg = cfg; this.metrics = metrics; this.updateFactory = updateFactory; + this.internalAccountQuery = internalAccountQuery; + this.internalChangeQuery = internalChangeQuery; this.exceptionHooks = exceptionHooks; - - Duration defaultTimeout = + this.defaultTimeout = Duration.ofMillis( cfg.getTimeUnit("retry", null, "timeout", SECONDS.toMillis(20), MILLISECONDS)); - this.defaultTimeouts = Maps.newEnumMap(ActionType.class); + this.defaultTimeouts = new HashMap<>(); Arrays.stream(ActionType.values()) .forEach( at -> defaultTimeouts.put( - at, + at.name(), Duration.ofMillis( cfg.getTimeUnit( "retry", @@ -237,54 +252,182 @@ this.retryWithTraceOnFailure = cfg.getBoolean("retry", "retryWithTraceOnFailure", false); } - public Duration getDefaultTimeout(ActionType actionType) { - return defaultTimeouts.get(actionType); + /** + * Creates an action that is executed with retrying when called. + * + * <p>This method allows to use a custom action type. If the action type is one of {@link + * ActionType} the usage of {@link #action(ActionType, String, Action)} is preferred. + * + * <p>The action type is used as metric bucket and decides which default timeout is used. + * + * @param actionType the type of the action, used as metric bucket + * @param actionName the name of the action, used as metric bucket + * @param action the action that should be executed + * @return the retryable action, callers need to call {@link RetryableAction#call()} to execute + * the action + */ + @UsedAt(UsedAt.Project.GOOGLE) + public <T> RetryableAction<T> action(String actionType, String actionName, Action<T> action) { + return new RetryableAction<>(this, actionType, actionName, action); } - public <T> T execute( - ActionType actionType, Action<T> action, Predicate<Throwable> exceptionPredicate) - throws Exception { - return execute(actionType, action, defaults(), exceptionPredicate); + /** + * Creates an action that is executed with retrying when called. + * + * @param actionType the type of the action, used as metric bucket + * @param actionName the name of the action, used as metric bucket + * @param action the action that should be executed + * @return the retryable action, callers need to call {@link RetryableAction#call()} to execute + * the action + */ + public <T> RetryableAction<T> action(ActionType actionType, String actionName, Action<T> action) { + return new RetryableAction<>(this, actionType, actionName, action); } - public <T> T execute( - ActionType actionType, - Action<T> action, - Options opts, - Predicate<Throwable> exceptionPredicate) - throws Exception { - try { - return executeWithAttemptAndTimeoutCount(actionType, action, opts, exceptionPredicate); - } catch (Throwable t) { - Throwables.throwIfUnchecked(t); - Throwables.throwIfInstanceOf(t, Exception.class); - throw new IllegalStateException(t); + /** + * Creates an action for updating an account that is executed with retrying when called. + * + * @param actionName the name of the action, used as metric bucket + * @param action the action that should be executed + * @return the retryable action, callers need to call {@link RetryableAction#call()} to execute + * the action + */ + public <T> RetryableAction<T> accountUpdate(String actionName, Action<T> action) { + return new RetryableAction<>(this, ActionType.ACCOUNT_UPDATE, actionName, action); + } + + /** + * Creates an action for updating a change that is executed with retrying when called. + * + * @param actionName the name of the action, used as metric bucket + * @param action the action that should be executed + * @return the retryable action, callers need to call {@link RetryableAction#call()} to execute + * the action + */ + public <T> RetryableAction<T> changeUpdate(String actionName, Action<T> action) { + return new RetryableAction<>(this, ActionType.CHANGE_UPDATE, actionName, action); + } + + /** + * Creates an action for updating a change that is executed with retrying when called. + * + * <p>The change action gets a {@link BatchUpdate.Factory} provided that can be used to update the + * change. + * + * @param actionName the name of the action, used as metric bucket + * @param changeAction the action that should be executed + * @return the retryable action, callers need to call {@link RetryableChangeAction#call()} to + * execute the action + */ + public <T> RetryableChangeAction<T> changeUpdate( + String actionName, ChangeAction<T> changeAction) { + return new RetryableChangeAction<>(this, updateFactory, actionName, changeAction); + } + + /** + * Creates an action for updating a group that is executed with retrying when called. + * + * @param actionName the name of the action, used as metric bucket + * @param action the action that should be executed + * @return the retryable action, callers need to call {@link RetryableAction#call()} to execute + * the action + */ + public <T> RetryableAction<T> groupUpdate(String actionName, Action<T> action) { + return new RetryableAction<>(this, ActionType.GROUP_UPDATE, actionName, action); + } + + /** + * Creates an action for updating of plugin-specific data that is executed with retrying when + * called. + * + * @param actionName the name of the action, used as metric bucket + * @param action the action that should be executed + * @return the retryable action, callers need to call {@link RetryableAction#call()} to execute + * the action + */ + public <T> RetryableAction<T> pluginUpdate(String actionName, Action<T> action) { + return new RetryableAction<>(this, ActionType.PLUGIN_UPDATE, actionName, action); + } + + /** + * Creates an action for querying the account index that is executed with retrying when called. + * + * <p>The index query action gets a {@link InternalAccountQuery} provided that can be used to + * query the account index. + * + * @param actionName the name of the action, used as metric bucket + * @param indexQueryAction the action that should be executed + * @return the retryable action, callers need to call {@link RetryableIndexQueryAction#call()} to + * execute the action + */ + public <T> RetryableIndexQueryAction<InternalAccountQuery, T> accountIndexQuery( + String actionName, IndexQueryAction<T, InternalAccountQuery> indexQueryAction) { + return new RetryableIndexQueryAction<>( + this, internalAccountQuery, actionName, indexQueryAction); + } + + /** + * Creates an action for querying the change index that is executed with retrying when called. + * + * <p>The index query action gets a {@link InternalChangeQuery} provided that can be used to query + * the change index. + * + * @param actionName the name of the action, used as metric bucket + * @param indexQueryAction the action that should be executed + * @return the retryable action, callers need to call {@link RetryableIndexQueryAction#call()} to + * execute the action + */ + public <T> RetryableIndexQueryAction<InternalChangeQuery, T> changeIndexQuery( + String actionName, IndexQueryAction<T, InternalChangeQuery> indexQueryAction) { + return new RetryableIndexQueryAction<>(this, internalChangeQuery, actionName, indexQueryAction); + } + + /** + * Returns the default timeout for an action type. + * + * <p>The default timeout for an action type is defined by the 'retry.<action-type>.timeout' + * parameter in gerrit.config. If this parameter is not set the value from the 'retry.timeout' + * parameter is used (if this is also not set we fall back to to a hard-coded timeout of 20s). + * + * <p>Callers can overwrite the default timeout by setting another timeout in the {@link Options}, + * see {@link Options#timeout()}. + * + * @param actionType the action type for which the default timeout should be retrieved + * @return the default timeout for the given action type + */ + Duration getDefaultTimeout(String actionType) { + Duration timeout = defaultTimeouts.get(actionType); + if (timeout != null) { + return timeout; } + return readDefaultTimeoutFromConfig(actionType); } - public <T> T execute(ChangeAction<T> changeAction) throws RestApiException, UpdateException { - return execute(changeAction, defaults()); - } - - public <T> T execute(ChangeAction<T> changeAction, Options opts) - throws RestApiException, UpdateException { - try { - return execute( - ActionType.CHANGE_UPDATE, - () -> changeAction.call(updateFactory), - opts, - t -> { - if (t instanceof UpdateException) { - t = t.getCause(); - } - return t instanceof LockFailureException; - }); - } catch (Throwable t) { - Throwables.throwIfUnchecked(t); - Throwables.throwIfInstanceOf(t, UpdateException.class); - Throwables.throwIfInstanceOf(t, RestApiException.class); - throw new UpdateException(t); + /** + * Thread-safe method to read and cache a default timeout from gerrit.config. + * + * <p>After reading the default timeout from gerrit.config it is cached in the {@link + * #defaultTimeouts} map, so that it's read only once. + * + * @param actionType the action type for which the default timeout should be retrieved + * @return the default timeout for the given action type + */ + private synchronized Duration readDefaultTimeoutFromConfig(String actionType) { + Duration timeout = defaultTimeouts.get(actionType); + if (timeout != null) { + // some other thread has read the default timeout from the config in the meantime + return timeout; } + timeout = + Duration.ofMillis( + cfg.getTimeUnit( + "retry", + actionType, + "timeout", + SECONDS.toMillis(defaultTimeout.getSeconds()), + MILLISECONDS)); + defaultTimeouts.put(actionType, timeout); + return timeout; } /** @@ -298,11 +441,8 @@ * @throws Throwable any error or exception that made the action fail, callers are expected to * catch and inspect this Throwable to decide carefully whether it should be re-thrown */ - private <T> T executeWithAttemptAndTimeoutCount( - ActionType actionType, - Action<T> action, - Options opts, - Predicate<Throwable> exceptionPredicate) + <T> T execute( + String actionType, Action<T> action, Options opts, Predicate<Throwable> exceptionPredicate) throws Throwable { MetricListener listener = new MetricListener(); try (TraceContext traceContext = TraceContext.open()) { @@ -317,8 +457,11 @@ return true; } + String actionName = opts.actionName().orElse("N/A"); + // Exception hooks may identify additional exceptions for retry. - if (exceptionHooks.stream().anyMatch(h -> h.shouldRetry(t))) { + if (exceptionHooks.stream() + .anyMatch(h -> h.shouldRetry(actionType, actionName, t))) { return true; } @@ -327,14 +470,21 @@ if (retryWithTraceOnFailure && opts.retryWithTrace().isPresent() && opts.retryWithTrace().get().test(t)) { - String caller = opts.caller().map(Class::getSimpleName).orElse("N/A"); + // Exception hooks may identify exceptions for which retrying with trace should be + // skipped. + if (exceptionHooks.stream() + .anyMatch(h -> h.skipRetryWithTrace(actionType, actionName, t))) { + return false; + } + + String cause = formatCause(t); if (!traceContext.isTracing()) { String traceId = "retry-on-failure-" + new RequestId(); traceContext.addTag(RequestId.Type.TRACE_ID, traceId).forceLogging(); - opts.onAutoTrace().ifPresent(c -> c.accept(traceId)); logger.atFine().withCause(t).log( - "AutoRetry: %s failed, retry with tracing enabled", caller); - metrics.autoRetryCount.increment(actionType, caller); + "AutoRetry: %s failed, retry with tracing enabled", actionName); + opts.onAutoTrace().ifPresent(c -> c.accept(traceId)); + metrics.autoRetryCount.increment(actionType, actionName, cause); return true; } @@ -342,40 +492,78 @@ // enabled and it failed again. Log the failure so that admin can see if it // differs from the failure that triggered the retry. logger.atFine().withCause(t).log( - "AutoRetry: auto-retry of %s has failed", caller); - metrics.failuresOnAutoRetryCount.increment(actionType, caller); + "AutoRetry: auto-retry of %s has failed", actionName); + metrics.failuresOnAutoRetryCount.increment(actionType, actionName, cause); return false; } return false; }); retryerBuilder.withRetryListener(listener); - return executeWithTimeoutCount(actionType, action, retryerBuilder.build()); + return executeWithTimeoutCount(actionType, action, opts, retryerBuilder.build(), listener); } finally { if (listener.getAttemptCount() > 1) { logger.atFine().log("%s was attempted %d times", actionType, listener.getAttemptCount()); - metrics.attemptCounts.incrementBy(actionType, listener.getAttemptCount() - 1); + metrics.attemptCounts.incrementBy( + actionType, + opts.actionName().orElse("N/A"), + listener.getOriginalCause().map(this::formatCause).orElse("_unknown"), + listener.getAttemptCount() - 1); } } } + public String formatCause(Throwable t) { + while ((t instanceof UpdateException + || t instanceof StorageException + || t instanceof ExecutionException) + && t.getCause() != null) { + t = t.getCause(); + } + + Optional<String> formattedCause = getFormattedCauseFromHooks(t); + if (formattedCause.isPresent()) { + return formattedCause.get(); + } + + return t.getClass().getSimpleName(); + } + + private Optional<String> getFormattedCauseFromHooks(Throwable t) { + return exceptionHooks.stream() + .map(h -> h.formatCause(t)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); + } + /** * Executes an action and records the timeout as metric. * * @param actionType the type of the action * @param action the action which should be executed and retried on failure + * @param opts options for retrying the action on failure * @param retryer the retryer + * @param listener metric listener * @return the result of executing the action * @throws Throwable any error or exception that made the action fail, callers are expected to * catch and inspect this Throwable to decide carefully whether it should be re-thrown */ - private <T> T executeWithTimeoutCount(ActionType actionType, Action<T> action, Retryer<T> retryer) + private <T> T executeWithTimeoutCount( + String actionType, + Action<T> action, + Options opts, + Retryer<T> retryer, + MetricListener listener) throws Throwable { try { return retryer.call(action::call); } catch (ExecutionException | RetryException e) { if (e instanceof RetryException) { - metrics.timeoutCount.increment(actionType); + metrics.timeoutCount.increment( + actionType, + opts.actionName().orElse("N/A"), + listener.getOriginalCause().map(this::formatCause).orElse("_unknown")); } if (e.getCause() != null) { throw e.getCause(); @@ -385,7 +573,7 @@ } private <O> RetryerBuilder<O> createRetryerBuilder( - ActionType actionType, Options opts, Predicate<Throwable> exceptionPredicate) { + String actionType, Options opts, Predicate<Throwable> exceptionPredicate) { RetryerBuilder<O> retryerBuilder = RetryerBuilder.<O>newBuilder().retryIfException(exceptionPredicate::test); if (opts.listener() != null) { @@ -407,18 +595,27 @@ private static class MetricListener implements RetryListener { private long attemptCount; + private Optional<Throwable> originalCause; MetricListener() { attemptCount = 1; + originalCause = Optional.empty(); } @Override public <V> void onRetry(Attempt<V> attempt) { attemptCount = attempt.getAttemptNumber(); + if (attemptCount == 1 && attempt.hasException()) { + originalCause = Optional.of(attempt.getExceptionCause()); + } } long getAttemptCount() { return attemptCount; } + + Optional<Throwable> getOriginalCause() { + return originalCause; + } } }
diff --git a/java/com/google/gerrit/server/update/RetryableAction.java b/java/com/google/gerrit/server/update/RetryableAction.java new file mode 100644 index 0000000..167b209 --- /dev/null +++ b/java/com/google/gerrit/server/update/RetryableAction.java
@@ -0,0 +1,183 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.update; + +import static java.util.Objects.requireNonNull; + +import com.github.rholder.retry.RetryListener; +import com.google.common.base.Throwables; +import com.google.gerrit.server.ExceptionHook; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * An action that is executed with retrying. + * + * <p>Instances of this class are created via {@link RetryHelper} (see {@link + * RetryHelper#action(ActionType, String, Action)}, {@link RetryHelper#accountUpdate(String, + * Action)}, {@link RetryHelper#changeUpdate(String, Action)}, {@link + * RetryHelper#groupUpdate(String, Action)}, {@link RetryHelper#pluginUpdate(String, Action)}). + * + * <p>Which exceptions cause a retry is controlled by {@link ExceptionHook#shouldRetry(String, + * String, Throwable)}. In addition callers can specify additional exception that should cause a + * retry via {@link #retryOn(Predicate)}. + */ +public class RetryableAction<T> { + /** + * Type of an retryable action. + * + * <p>The action type is used for two purposes: + * + * <ul> + * <li>to determine the default timeout for executing the action (see {@link + * RetryHelper#getDefaultTimeout(String)}) + * <li>as bucket for all retry metrics (see {@link RetryHelper.Metrics}) + * </ul> + */ + public enum ActionType { + ACCOUNT_UPDATE, + CHANGE_UPDATE, + GIT_UPDATE, + GROUP_UPDATE, + INDEX_QUERY, + PLUGIN_UPDATE, + REST_READ_REQUEST, + REST_WRITE_REQUEST, + } + + @FunctionalInterface + public interface Action<T> { + T call() throws Exception; + } + + private final RetryHelper retryHelper; + private final String actionType; + private final Action<T> action; + private final RetryHelper.Options.Builder options = RetryHelper.options(); + private final List<Predicate<Throwable>> exceptionPredicates = new ArrayList<>(); + + RetryableAction( + RetryHelper retryHelper, ActionType actionType, String actionName, Action<T> action) { + this(retryHelper, requireNonNull(actionType, "actionType").name(), actionName, action); + } + + RetryableAction(RetryHelper retryHelper, String actionType, String actionName, Action<T> action) { + this.retryHelper = requireNonNull(retryHelper, "retryHelper"); + this.actionType = requireNonNull(actionType, "actionType"); + this.action = requireNonNull(action, "action"); + options.actionName(requireNonNull(actionName, "actionName")); + } + + /** + * Adds an additional condition that should trigger retries. + * + * <p>For some exceptions retrying is enabled globally (see {@link + * ExceptionHook#shouldRetry(String, String, Throwable)}). Conditions for those exceptions do not + * need to be specified here again. + * + * <p>This method can be invoked multiple times to add further conditions that should trigger + * retries. + * + * @param exceptionPredicate predicate that decides if the action should be retried for a given + * exception + * @return this instance to enable chaining of calls + */ + public RetryableAction<T> retryOn(Predicate<Throwable> exceptionPredicate) { + exceptionPredicates.add(exceptionPredicate); + return this; + } + + /** + * Sets a condition that should trigger auto-retry with tracing. + * + * <p>This condition is only relevant if an exception occurs that doesn't trigger (normal) retry. + * + * <p>Auto-retry with tracing automatically captures traces for unexpected exceptions so that they + * can be investigated. + * + * <p>Every call of this method overwrites any previously set condition for auto-retry with + * tracing. + * + * @param exceptionPredicate predicate that decides if the action should be retried with tracing + * for a given exception + * @return this instance to enable chaining of calls + */ + public RetryableAction<T> retryWithTrace(Predicate<Throwable> exceptionPredicate) { + options.retryWithTrace(exceptionPredicate); + return this; + } + + /** + * Sets a callback that is invoked when auto-retry with tracing is triggered. + * + * <p>Via the callback callers can find out with trace ID was used for the retry. + * + * <p>Every call of this method overwrites any previously set trace ID consumer. + * + * @param traceIdConsumer trace ID consumer + * @return this instance to enable chaining of calls + */ + public RetryableAction<T> onAutoTrace(Consumer<String> traceIdConsumer) { + options.onAutoTrace(traceIdConsumer); + return this; + } + + /** + * Sets a listener that is invoked when the action is retried. + * + * <p>Every call of this method overwrites any previously set listener. + * + * @param retryListener retry listener + * @return this instance to enable chaining of calls + */ + public RetryableAction<T> listener(RetryListener retryListener) { + options.listener(retryListener); + return this; + } + + /** + * Increases the default timeout by the given multiplier. + * + * <p>Every call of this method overwrites any previously set timeout. + * + * @param multiplier multiplier for the default timeout + * @return this instance to enable chaining of calls + */ + public RetryableAction<T> defaultTimeoutMultiplier(int multiplier) { + options.timeout(retryHelper.getDefaultTimeout(actionType).multipliedBy(multiplier)); + return this; + } + + /** + * Executes this action with retry. + * + * @return the result of the action + */ + public T call() throws Exception { + try { + return retryHelper.execute( + actionType, + action, + options.build(), + t -> exceptionPredicates.stream().anyMatch(p -> p.test(t))); + } catch (Throwable t) { + Throwables.throwIfUnchecked(t); + Throwables.throwIfInstanceOf(t, Exception.class); + throw new IllegalStateException(t); + } + } +}
diff --git a/java/com/google/gerrit/server/update/RetryableChangeAction.java b/java/com/google/gerrit/server/update/RetryableChangeAction.java new file mode 100644 index 0000000..152db2c --- /dev/null +++ b/java/com/google/gerrit/server/update/RetryableChangeAction.java
@@ -0,0 +1,92 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.update; + +import com.github.rholder.retry.RetryListener; +import com.google.common.base.Throwables; +import com.google.gerrit.extensions.restapi.RestApiException; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * A change action that is executed with retrying. + * + * <p>Instances of this class are created via {@link RetryHelper#changeUpdate(String, + * ChangeAction)}. + * + * <p>In contrast to normal {@link RetryableAction.Action}s that are called via {@link + * RetryableAction} {@link ChangeAction}s get a {@link BatchUpdate.Factory} provided. + * + * <p>In addition when a change action is called any exception that is not an unchecked exception + * and neither {@link UpdateException} nor {@link RestApiException} get wrapped into an {@link + * UpdateException}. + */ +public class RetryableChangeAction<T> extends RetryableAction<T> { + @FunctionalInterface + public interface ChangeAction<T> { + T call(BatchUpdate.Factory batchUpdateFactory) throws Exception; + } + + RetryableChangeAction( + RetryHelper retryHelper, + BatchUpdate.Factory updateFactory, + String actionName, + ChangeAction<T> changeAction) { + super( + retryHelper, ActionType.CHANGE_UPDATE, actionName, () -> changeAction.call(updateFactory)); + } + + @Override + public RetryableChangeAction<T> retryOn(Predicate<Throwable> exceptionPredicate) { + super.retryOn(exceptionPredicate); + return this; + } + + @Override + public RetryableChangeAction<T> retryWithTrace(Predicate<Throwable> exceptionPredicate) { + super.retryWithTrace(exceptionPredicate); + return this; + } + + @Override + public RetryableChangeAction<T> onAutoTrace(Consumer<String> traceIdConsumer) { + super.onAutoTrace(traceIdConsumer); + return this; + } + + @Override + public RetryableChangeAction<T> listener(RetryListener retryListener) { + super.listener(retryListener); + return this; + } + + @Override + public RetryableChangeAction<T> defaultTimeoutMultiplier(int multiplier) { + super.defaultTimeoutMultiplier(multiplier); + return this; + } + + @Override + public T call() throws UpdateException, RestApiException { + try { + return super.call(); + } catch (Throwable t) { + Throwables.throwIfUnchecked(t); + Throwables.throwIfInstanceOf(t, UpdateException.class); + Throwables.throwIfInstanceOf(t, RestApiException.class); + throw new UpdateException(t); + } + } +}
diff --git a/java/com/google/gerrit/server/update/RetryableIndexQueryAction.java b/java/com/google/gerrit/server/update/RetryableIndexQueryAction.java new file mode 100644 index 0000000..cf733a6 --- /dev/null +++ b/java/com/google/gerrit/server/update/RetryableIndexQueryAction.java
@@ -0,0 +1,95 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.update; + +import com.github.rholder.retry.RetryListener; +import com.google.common.base.Throwables; +import com.google.gerrit.exceptions.StorageException; +import com.google.gerrit.index.query.InternalQuery; +import com.google.inject.Provider; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * An action to query an index that is executed with retrying. + * + * <p>Instances of this class are created via {@link RetryHelper#accountIndexQuery(String, + * IndexQueryAction)} and {@link RetryHelper#changeIndexQuery(String, IndexQueryAction)}. + * + * <p>In contrast to normal {@link RetryableAction.Action}s that are called via {@link + * RetryableAction} {@link IndexQueryAction}s get a {@link InternalQuery} provided. + * + * <p>In addition when an index query action is called any exception that is not an unchecked + * exception gets wrapped into an {@link StorageException}. + */ +public class RetryableIndexQueryAction<Q extends InternalQuery<?, Q>, T> + extends RetryableAction<T> { + @FunctionalInterface + public interface IndexQueryAction<T, Q> { + T call(Q internalQuery) throws Exception; + } + + RetryableIndexQueryAction( + RetryHelper retryHelper, + Provider<Q> internalQuery, + String actionName, + IndexQueryAction<T, Q> indexQuery) { + super( + retryHelper, + ActionType.INDEX_QUERY, + actionName, + () -> indexQuery.call(internalQuery.get())); + } + + @Override + public RetryableIndexQueryAction<Q, T> retryOn(Predicate<Throwable> exceptionPredicate) { + super.retryOn(exceptionPredicate); + return this; + } + + @Override + public RetryableIndexQueryAction<Q, T> retryWithTrace(Predicate<Throwable> exceptionPredicate) { + super.retryWithTrace(exceptionPredicate); + return this; + } + + @Override + public RetryableIndexQueryAction<Q, T> onAutoTrace(Consumer<String> traceIdConsumer) { + super.onAutoTrace(traceIdConsumer); + return this; + } + + @Override + public RetryableIndexQueryAction<Q, T> listener(RetryListener retryListener) { + super.listener(retryListener); + return this; + } + + @Override + public RetryableIndexQueryAction<Q, T> defaultTimeoutMultiplier(int multiplier) { + super.defaultTimeoutMultiplier(multiplier); + return this; + } + + @Override + public T call() { + try { + return super.call(); + } catch (Throwable t) { + Throwables.throwIfUnchecked(t); + throw new StorageException(t); + } + } +}
diff --git a/java/com/google/gerrit/server/update/RetryingRestCollectionModifyView.java b/java/com/google/gerrit/server/update/RetryingRestCollectionModifyView.java deleted file mode 100644 index 96c2ed3..0000000 --- a/java/com/google/gerrit/server/update/RetryingRestCollectionModifyView.java +++ /dev/null
@@ -1,58 +0,0 @@ -// Copyright (C) 2018 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.update; - -import com.google.common.base.Throwables; -import com.google.gerrit.extensions.restapi.AuthException; -import com.google.gerrit.extensions.restapi.BadRequestException; -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.RestCollectionModifyView; -import com.google.gerrit.extensions.restapi.RestResource; -import java.util.concurrent.atomic.AtomicReference; - -public abstract class RetryingRestCollectionModifyView< - P extends RestResource, C extends RestResource, I, O> - implements RestCollectionModifyView<P, C, I> { - private final RetryHelper retryHelper; - - protected RetryingRestCollectionModifyView(RetryHelper retryHelper) { - this.retryHelper = retryHelper; - } - - @Override - public final Response<O> apply(P parentResource, I input) - throws AuthException, BadRequestException, ResourceConflictException, Exception { - AtomicReference<String> traceId = new AtomicReference<>(null); - try { - RetryHelper.Options retryOptions = - RetryHelper.options() - .caller(getClass()) - .retryWithTrace(t -> !(t instanceof RestApiException)) - .onAutoTrace(traceId::set) - .build(); - return retryHelper - .execute((updateFactory) -> applyImpl(updateFactory, parentResource, input), retryOptions) - .traceId(traceId.get()); - } catch (Exception e) { - Throwables.throwIfInstanceOf(e, RestApiException.class); - return Response.<O>internalServerError(e).traceId(traceId.get()); - } - } - - protected abstract Response<O> applyImpl( - BatchUpdate.Factory updateFactory, P parentResource, I input) throws Exception; -}
diff --git a/java/com/google/gerrit/server/update/RetryingRestModifyView.java b/java/com/google/gerrit/server/update/RetryingRestModifyView.java deleted file mode 100644 index 275dc55..0000000 --- a/java/com/google/gerrit/server/update/RetryingRestModifyView.java +++ /dev/null
@@ -1,53 +0,0 @@ -// 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.update; - -import com.google.common.base.Throwables; -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.RestResource; -import java.util.concurrent.atomic.AtomicReference; - -public abstract class RetryingRestModifyView<R extends RestResource, I, O> - implements RestModifyView<R, I> { - private final RetryHelper retryHelper; - - protected RetryingRestModifyView(RetryHelper retryHelper) { - this.retryHelper = retryHelper; - } - - @Override - public final Response<O> apply(R resource, I input) throws RestApiException { - AtomicReference<String> traceId = new AtomicReference<>(null); - try { - RetryHelper.Options retryOptions = - RetryHelper.options() - .caller(getClass()) - .retryWithTrace(t -> !(t instanceof RestApiException)) - .onAutoTrace(traceId::set) - .build(); - return retryHelper - .execute((updateFactory) -> applyImpl(updateFactory, resource, input), retryOptions) - .traceId(traceId.get()); - } catch (Exception e) { - Throwables.throwIfInstanceOf(e, RestApiException.class); - return Response.<O>internalServerError(e).traceId(traceId.get()); - } - } - - protected abstract Response<O> applyImpl(BatchUpdate.Factory updateFactory, R resource, I input) - throws Exception; -}
diff --git a/java/com/google/gerrit/server/validators/ValidationException.java b/java/com/google/gerrit/server/validators/ValidationException.java index 53ded1f..9562e88 100644 --- a/java/com/google/gerrit/server/validators/ValidationException.java +++ b/java/com/google/gerrit/server/validators/ValidationException.java
@@ -14,6 +14,11 @@ package com.google.gerrit.server.validators; +/** + * Exception to be thrown either directly or subclassed indicating that we failed to validate a Git + * operation. Failures range from internal checks for NoteDb format and consistency to + * plugin-provided checks. + */ public class ValidationException extends Exception { private static final long serialVersionUID = 1L;
diff --git a/java/com/google/gerrit/sshd/BUILD b/java/com/google/gerrit/sshd/BUILD index bc9a0ee..f567a3a 100644 --- a/java/com/google/gerrit/sshd/BUILD +++ b/java/com/google/gerrit/sshd/BUILD
@@ -32,7 +32,6 @@ "//lib/auto:auto-value", "//lib/auto:auto-value-annotations", "//lib/bouncycastle:bcprov-neverlink", - "//lib/commons:codec", "//lib/dropwizard:dropwizard-core", "//lib/flogger:api", "//lib/guice",
diff --git a/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java index 6c0f3af..916775d 100644 --- a/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java +++ b/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -19,6 +19,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.flogger.FluentLogger; +import com.google.common.io.BaseEncoding; import com.google.gerrit.common.FileUtil; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.PeerDaemonUser; @@ -39,7 +40,6 @@ import java.util.HashSet; import java.util.Locale; import java.util.Set; -import org.apache.commons.codec.binary.Base64; import org.apache.sshd.common.SshException; import org.apache.sshd.common.keyprovider.KeyPairProvider; import org.apache.sshd.common.util.buffer.ByteArrayBuffer; @@ -198,7 +198,8 @@ } try { - byte[] bin = Base64.decodeBase64(line.getBytes(ISO_8859_1)); + byte[] bin = + BaseEncoding.base64().decode(new String(line.getBytes(ISO_8859_1), ISO_8859_1)); keys.add(new ByteArrayBuffer(bin).getRawPublicKey()); } catch (RuntimeException | SshException e) { logBadKey(path, line, e);
diff --git a/java/com/google/gerrit/sshd/SshUtil.java b/java/com/google/gerrit/sshd/SshUtil.java index 39366f0..eac9737 100644 --- a/java/com/google/gerrit/sshd/SshUtil.java +++ b/java/com/google/gerrit/sshd/SshUtil.java
@@ -14,6 +14,7 @@ package com.google.gerrit.sshd; +import com.google.common.io.BaseEncoding; import com.google.gerrit.entities.Account; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; @@ -28,12 +29,10 @@ import java.security.interfaces.DSAPublicKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.InvalidKeySpecException; -import org.apache.commons.codec.binary.Base64; import org.apache.sshd.common.SshException; import org.apache.sshd.common.keyprovider.KeyPairProvider; import org.apache.sshd.common.util.buffer.ByteArrayBuffer; import org.apache.sshd.server.session.ServerSession; -import org.eclipse.jgit.lib.Constants; /** Utilities to support SSH operations. */ public class SshUtil { @@ -53,7 +52,7 @@ if (s == null) { throw new InvalidKeySpecException("No key string"); } - final byte[] bin = Base64.decodeBase64(Constants.encodeASCII(s)); + final byte[] bin = BaseEncoding.base64().decode(s); return new ByteArrayBuffer(bin).getRawPublicKey(); } catch (RuntimeException | SshException e) { throw new InvalidKeySpecException("Cannot parse key", e); @@ -91,8 +90,7 @@ } final PublicKey key = - new ByteArrayBuffer(Base64.decodeBase64(Constants.encodeASCII(strBuf.toString()))) - .getRawPublicKey(); + new ByteArrayBuffer(BaseEncoding.base64().decode(strBuf.toString())).getRawPublicKey(); if (key instanceof RSAPublicKey) { strBuf.insert(0, KeyPairProvider.SSH_RSA + " ");
diff --git a/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java index 9f420ed..17f80c0 100644 --- a/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java +++ b/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -119,7 +119,8 @@ } } - private GroupResource createGroup() throws Exception { + private GroupResource createGroup() + throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException { GroupInput input = new GroupInput(); input.description = groupDescription; input.visibleToAll = visibleToAll;
diff --git a/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/java/com/google/gerrit/sshd/commands/LsUserRefs.java index 8946688..b4a2b42 100644 --- a/java/com/google/gerrit/sshd/commands/LsUserRefs.java +++ b/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -35,7 +35,7 @@ import com.google.gerrit.sshd.SshCommand; import com.google.inject.Inject; import java.io.IOException; -import java.util.Map; +import java.util.Collection; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.Ref; @@ -89,14 +89,14 @@ try (Repository repo = repoManager.openRepository(projectName); ManualRequestContext ctx = requestContext.openAs(userAccountId)) { try { - Map<String, Ref> refsMap = + Collection<Ref> refsMap = permissionBackend .user(ctx.getUser()) .project(projectName) .filter(repo.getRefDatabase().getRefs(), repo, RefFilterOptions.defaults()); - for (String ref : refsMap.keySet()) { - if (!onlyRefsHeads || ref.startsWith(RefNames.REFS_HEADS)) { + for (Ref ref : refsMap) { + if (!onlyRefsHeads || ref.getName().startsWith(RefNames.REFS_HEADS)) { stdout.println(ref); } }
diff --git a/java/com/google/gerrit/sshd/commands/SequenceCommandsModule.java b/java/com/google/gerrit/sshd/commands/SequenceCommandsModule.java new file mode 100644 index 0000000..e716240 --- /dev/null +++ b/java/com/google/gerrit/sshd/commands/SequenceCommandsModule.java
@@ -0,0 +1,32 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.sshd.commands; + +import com.google.gerrit.sshd.CommandModule; +import com.google.gerrit.sshd.CommandName; +import com.google.gerrit.sshd.Commands; +import com.google.gerrit.sshd.DispatchCommandProvider; + +public class SequenceCommandsModule extends CommandModule { + + @Override + protected void configure() { + CommandName gerrit = Commands.named("gerrit"); + CommandName sequence = Commands.named(gerrit, "sequence"); + command(sequence).toProvider(new DispatchCommandProvider(sequence)); + command(sequence, SequenceSetCommand.class); + command(sequence, SequenceShowCommand.class); + } +}
diff --git a/java/com/google/gerrit/sshd/commands/SequenceSetCommand.java b/java/com/google/gerrit/sshd/commands/SequenceSetCommand.java new file mode 100644 index 0000000..197d61c --- /dev/null +++ b/java/com/google/gerrit/sshd/commands/SequenceSetCommand.java
@@ -0,0 +1,56 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.sshd.commands; + +import com.google.gerrit.common.data.GlobalCapability; +import com.google.gerrit.extensions.annotations.RequiresCapability; +import com.google.gerrit.server.notedb.Sequences; +import com.google.gerrit.sshd.CommandMetaData; +import com.google.gerrit.sshd.SshCommand; +import com.google.inject.Inject; +import org.kohsuke.args4j.Argument; + +/** Set sequence value. */ +@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER) +@CommandMetaData(name = "set", description = "Set the sequence value") +final class SequenceSetCommand extends SshCommand { + @Argument(index = 0, metaVar = "NAME", required = true, usage = "sequence name") + private String name; + + @Argument(index = 1, metaVar = "VALUE", required = true, usage = "sequence value") + private int value; + + @Inject Sequences sequences; + + @Override + public void run() throws Exception { + switch (name) { + case "changes": + sequences.setChangeIdValue(value); + break; + case "accounts": + sequences.setAccountIdValue(value); + break; + case "groups": + sequences.setGroupIdValue(value); + break; + default: + throw die("Unknown sequence name: " + name); + } + stdout.print("The value for the " + name + " sequence was set to " + value + "."); + stdout.print('\n'); + stdout.flush(); + } +}
diff --git a/java/com/google/gerrit/sshd/commands/SequenceShowCommand.java b/java/com/google/gerrit/sshd/commands/SequenceShowCommand.java new file mode 100644 index 0000000..490c7ca --- /dev/null +++ b/java/com/google/gerrit/sshd/commands/SequenceShowCommand.java
@@ -0,0 +1,54 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.sshd.commands; + +import com.google.gerrit.common.data.GlobalCapability; +import com.google.gerrit.extensions.annotations.RequiresCapability; +import com.google.gerrit.server.notedb.Sequences; +import com.google.gerrit.sshd.CommandMetaData; +import com.google.gerrit.sshd.SshCommand; +import com.google.inject.Inject; +import org.kohsuke.args4j.Argument; + +/** Display sequence value. */ +@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER) +@CommandMetaData(name = "show", description = "Display the sequence value") +final class SequenceShowCommand extends SshCommand { + @Argument(index = 0, metaVar = "NAME", required = true, usage = "sequence name") + private String name; + + @Inject Sequences sequences; + + @Override + public void run() throws Exception { + int current; + switch (name) { + case "changes": + current = sequences.currentChangeId(); + break; + case "accounts": + current = sequences.currentAccountId(); + break; + case "groups": + current = sequences.currentGroupId(); + break; + default: + throw die("Unknown sequence name: " + name); + } + stdout.print(current); + stdout.print('\n'); + stdout.flush(); + } +}
diff --git a/java/com/google/gerrit/sshd/commands/ShowCaches.java b/java/com/google/gerrit/sshd/commands/ShowCaches.java index db0a481..7e0439f 100644 --- a/java/com/google/gerrit/sshd/commands/ShowCaches.java +++ b/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -197,7 +197,7 @@ stdout.flush(); } - private Collection<CacheInfo> getCaches() throws Exception { + private Collection<CacheInfo> getCaches() { @SuppressWarnings("unchecked") Map<String, CacheInfo> caches = (Map<String, CacheInfo>) listCaches.apply(new ConfigResource()).value();
diff --git a/java/com/google/gerrit/testing/BUILD b/java/com/google/gerrit/testing/BUILD index 5e2fbea..a76275b 100644 --- a/java/com/google/gerrit/testing/BUILD +++ b/java/com/google/gerrit/testing/BUILD
@@ -13,6 +13,7 @@ "//lib/mockito", ], deps = [ + "//java/com/google/gerrit/acceptance/config", "//java/com/google/gerrit/acceptance/testsuite/project", "//java/com/google/gerrit/common:annotations", "//java/com/google/gerrit/entities",
diff --git a/java/com/google/gerrit/testing/GerritServerTests.java b/java/com/google/gerrit/testing/GerritServerTests.java index ad985b6..363a07d 100644 --- a/java/com/google/gerrit/testing/GerritServerTests.java +++ b/java/com/google/gerrit/testing/GerritServerTests.java
@@ -14,6 +14,9 @@ package com.google.gerrit.testing; +import com.google.gerrit.acceptance.config.ConfigAnnotationParser; +import com.google.gerrit.acceptance.config.GerritConfig; +import com.google.gerrit.acceptance.config.GerritConfigs; import org.eclipse.jgit.lib.Config; import org.junit.Rule; import org.junit.rules.TestRule; @@ -23,16 +26,24 @@ @RunWith(ConfigSuite.class) public class GerritServerTests { @ConfigSuite.Parameter public Config config; - @ConfigSuite.Name private String configName; @Rule public TestRule testRunner = - (base, description) -> - new Statement() { - @Override - public void evaluate() throws Throwable { - base.evaluate(); - } - }; + (base, description) -> { + GerritConfig gerritConfig = description.getAnnotation(GerritConfig.class); + if (gerritConfig != null) { + config = ConfigAnnotationParser.parse(config, gerritConfig); + } + GerritConfigs gerritConfigs = description.getAnnotation(GerritConfigs.class); + if (gerritConfigs != null) { + config = ConfigAnnotationParser.parse(config, gerritConfigs); + } + return new Statement() { + @Override + public void evaluate() throws Throwable { + base.evaluate(); + } + }; + }; }
diff --git a/java/com/google/gerrit/testing/IndexConfig.java b/java/com/google/gerrit/testing/IndexConfig.java index 21c49dd..fb6c926 100644 --- a/java/com/google/gerrit/testing/IndexConfig.java +++ b/java/com/google/gerrit/testing/IndexConfig.java
@@ -23,7 +23,9 @@ public static Config createFromExistingConfig(Config cfg) { cfg.setInt("index", null, "maxPages", 10); - cfg.setBoolean("index", null, "reindexAfterRefUpdate", false); + // To avoid this flakiness indexMergeable is switched off for the tests as it incurs background + // reindex calls. + cfg.setBoolean("index", "change", "indexMergeable", false); cfg.setString("trackingid", "query-bug", "footer", "Bug:"); cfg.setString("trackingid", "query-bug", "match", "QUERY\\d{2,8}"); cfg.setString("trackingid", "query-bug", "system", "querytests");
diff --git a/java/org/apache/commons/net/BUILD b/java/org/apache/commons/net/BUILD index c322ecd..d83d8ec 100644 --- a/java/org/apache/commons/net/BUILD +++ b/java/org/apache/commons/net/BUILD
@@ -6,7 +6,7 @@ visibility = ["//visibility:public"], deps = [ "//java/com/google/gerrit/util/ssl", - "//lib/commons:codec", + "//lib:guava", "//lib/commons:net", ], )
diff --git a/java/org/apache/commons/net/smtp/AuthSMTPClient.java b/java/org/apache/commons/net/smtp/AuthSMTPClient.java index 33dd609..85e4dbf 100644 --- a/java/org/apache/commons/net/smtp/AuthSMTPClient.java +++ b/java/org/apache/commons/net/smtp/AuthSMTPClient.java
@@ -16,6 +16,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; +import com.google.common.io.BaseEncoding; import com.google.gerrit.util.ssl.BlindSSLSocketFactory; import java.io.BufferedReader; import java.io.BufferedWriter; @@ -33,7 +34,6 @@ import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; -import org.apache.commons.codec.binary.Base64; public class AuthSMTPClient extends SMTPClient { private String authTypes; @@ -134,7 +134,7 @@ } final String enc = getReplyStrings()[0].split(" ", 2)[1]; - final byte[] nonce = Base64.decodeBase64(enc.getBytes(UTF_8)); + final byte[] nonce = BaseEncoding.base64().decode(enc); final String sec; try { Mac mac = Mac.getInstance(macName); @@ -187,6 +187,6 @@ } private static String encodeBase64(byte[] data) { - return new String(Base64.encodeBase64(data), UTF_8); + return BaseEncoding.base64().encode(data); } }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java index ba12a12..203f4a8 100644 --- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java +++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -59,12 +59,12 @@ import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.ExtensionRegistry; import com.google.gerrit.acceptance.ExtensionRegistry.Registration; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.Sandboxed; import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.acceptance.UseClockStep; import com.google.gerrit.acceptance.UseSsh; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.acceptance.testsuite.account.AccountOperations; import com.google.gerrit.acceptance.testsuite.account.TestSshKeys; import com.google.gerrit.acceptance.testsuite.group.GroupOperations; @@ -105,7 +105,6 @@ import com.google.gerrit.extensions.common.SshKeyInfo; import com.google.gerrit.extensions.events.AccountIndexedListener; import com.google.gerrit.extensions.events.GitReferenceUpdatedListener; -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.ResourceConflictException; @@ -117,6 +116,7 @@ import com.google.gerrit.gpg.PublicKeyStore; import com.google.gerrit.gpg.testing.TestKey; import com.google.gerrit.mail.Address; +import com.google.gerrit.server.ExceptionHook; import com.google.gerrit.server.ServerInitiated; import com.google.gerrit.server.account.AccountProperties; import com.google.gerrit.server.account.AccountState; @@ -135,7 +135,6 @@ import com.google.gerrit.server.notedb.Sequences; import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions; -import com.google.gerrit.server.plugincontext.PluginContext.PluginMetrics; import com.google.gerrit.server.plugincontext.PluginSetContext; import com.google.gerrit.server.project.ProjectConfig; import com.google.gerrit.server.project.RefPattern; @@ -221,6 +220,7 @@ @Inject private VersionedAuthorizedKeys.Accessor authorizedKeys; @Inject private PermissionBackend permissionBackend; @Inject private ExtensionRegistry extensionRegistry; + @Inject private PluginSetContext<ExceptionHook> exceptionHooks; @Inject protected Emails emails; @@ -1478,11 +1478,9 @@ public void refsUsersSelfIsAdvertised() throws Exception { try (Repository allUsersRepo = repoManager.openRepository(allUsers)) { assertThat( - permissionBackend - .currentUser() - .project(allUsers) - .filter(ImmutableList.of(), allUsersRepo, RefFilterOptions.defaults()) - .keySet()) + permissionBackend.currentUser().project(allUsers) + .filter(ImmutableList.of(), allUsersRepo, RefFilterOptions.defaults()).stream() + .map(Ref::getName)) .containsExactly(RefNames.REFS_USERS_SELF); } } @@ -2763,7 +2761,9 @@ cfg, retryMetrics, null, - new PluginSetContext<>(DynamicSet.emptySet(), PluginMetrics.DISABLED_INSTANCE), + null, + null, + exceptionHooks, r -> r.withBlockStrategy(noSleepBlockStrategy)), extIdNotesFactory, ident, @@ -2817,7 +2817,9 @@ cfg, retryMetrics, null, - new PluginSetContext<>(DynamicSet.emptySet(), PluginMetrics.DISABLED_INSTANCE), + null, + null, + exceptionHooks, r -> r.withStopStrategy(StopStrategies.stopAfterAttempt(status.size())) .withBlockStrategy(noSleepBlockStrategy)), @@ -2875,7 +2877,9 @@ cfg, retryMetrics, null, - new PluginSetContext<>(DynamicSet.emptySet(), PluginMetrics.DISABLED_INSTANCE), + null, + null, + exceptionHooks, r -> r.withBlockStrategy(noSleepBlockStrategy)), extIdNotesFactory, ident, @@ -2948,7 +2952,9 @@ cfg, retryMetrics, null, - new PluginSetContext<>(DynamicSet.emptySet(), PluginMetrics.DISABLED_INSTANCE), + null, + null, + exceptionHooks, r -> r.withBlockStrategy(noSleepBlockStrategy)), extIdNotesFactory, ident, @@ -3010,7 +3016,7 @@ // Newly created account is not stale. AccountInfo accountInfo = gApi.accounts().create(name("foo")).get(); Account.Id accountId = Account.id(accountInfo._accountId); - assertThat(stalenessChecker.isStale(accountId)).isFalse(); + assertThat(stalenessChecker.check(accountId).isStale()).isFalse(); // Manually updating the user ref makes the index document stale. String userRef = RefNames.refsUsers(accountId); @@ -3078,11 +3084,11 @@ // has to happen directly on the accounts cache because AccountCacheImpl triggers a reindex for // the account. accountsCache.invalidate(accountId); - assertThat(stalenessChecker.isStale(accountId)).isTrue(); + assertThat(stalenessChecker.check(accountId).isStale()).isTrue(); // Reindex fixes staleness accountIndexer.index(accountId); - assertThat(stalenessChecker.isStale(accountId)).isFalse(); + assertThat(stalenessChecker.check(accountId).isStale()).isFalse(); } @Test
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java index 25617d4..a09284e 100644 --- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java +++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
@@ -23,7 +23,7 @@ import com.google.common.collect.ImmutableSet; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.common.Nullable; import com.google.gerrit.entities.Account; import com.google.gerrit.server.ServerInitiated;
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java index 5550d98..11ca391 100644 --- a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java +++ b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
@@ -21,9 +21,9 @@ import com.google.common.collect.ImmutableList; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.UseClockStep; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.acceptance.testsuite.group.GroupOperations; import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; import com.google.gerrit.common.RawInputUtil; @@ -178,6 +178,18 @@ } @Test + public void listAgreementPermission() throws Exception { + assume().that(isContributorAgreementsEnabled()).isTrue(); + requestScopeOperations.setApiUser(admin.id()); + // Allowed. + gApi.accounts().id(user.id().get()).listAgreements(); + requestScopeOperations.setApiUser(user.id()); + + // Not allowed. + assertThrows(AuthException.class, () -> gApi.accounts().id(admin.id().get()).listAgreements()); + } + + @Test public void signAgreementAsOtherUser() throws Exception { assume().that(isContributorAgreementsEnabled()).isTrue(); assertThat(gApi.accounts().self().get().name).isNotEqualTo("admin"); @@ -239,6 +251,28 @@ } @Test + public void revertSubmissionWithoutCLA() throws Exception { + assume().that(isContributorAgreementsEnabled()).isTrue(); + + // Create a change succeeds when agreement is not required + setUseContributorAgreements(InheritableBoolean.FALSE); + ChangeInfo change = gApi.changes().create(newChangeInput()).get(); + + // Approve and submit it + requestScopeOperations.setApiUser(admin.id()); + gApi.changes().id(change.changeId).current().review(ReviewInput.approve()); + gApi.changes().id(change.changeId).current().submit(new SubmitInput()); + + // Revert Submission is not allowed when CLA is required but not signed + requestScopeOperations.setApiUser(user.id()); + setUseContributorAgreements(InheritableBoolean.TRUE); + AuthException thrown = + assertThrows( + AuthException.class, () -> gApi.changes().id(change.changeId).revertSubmission()); + assertThat(thrown).hasMessageThat().contains("Contributor Agreement"); + } + + @Test public void revertExcludedProjectChangeWithoutCLA() throws Exception { // Contributor agreements configured with excludeProjects = ExcludedProject // in AbstractDaemonTest.configureContributorAgreement(...)
diff --git a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java index 8aebc69..7ecbe69 100644 --- a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java +++ b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
@@ -21,20 +21,23 @@ import static com.google.gerrit.testing.GerritJUnit.assertThrows; import static java.util.concurrent.TimeUnit.HOURS; import static java.util.stream.Collectors.toList; +import static org.eclipse.jgit.lib.Constants.HEAD; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.UseClockStep; +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.common.data.Permission; import com.google.gerrit.entities.Project; +import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.change.AbandonUtil; @@ -45,6 +48,7 @@ import java.util.List; import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.ObjectId; import org.junit.Test; public class AbandonIT extends AbstractDaemonTest { @@ -136,6 +140,93 @@ } @Test + @UseClockStep + @GerritConfig(name = "changeCleanup.abandonAfter", value = "1w") + @GerritConfig(name = "changeCleanup.abandonIfMergeable", value = "false") + @GerritConfig(name = "index.change.indexMergeable", value = "true") + public void notAbandonedIfMergeableWhenMergeableOperatorIsEnabled() throws Exception { + ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId(); + + // create 2 changes + int id1 = createChange().getChange().getId().get(); + int id2 = createChange().getChange().getId().get(); + + // create 2 changes that conflict with each other + testRepo.reset(initial); + int id3 = createChange("change 3", "file.txt", "content").getChange().getId().get(); + testRepo.reset(initial); + int id4 = createChange("change 4", "file.txt", "other content").getChange().getId().get(); + + // make all 4 previously created changes older than 1 week + TestTimeUtil.incrementClock(7 * 24, HOURS); + + // create 1 new change that will not be abandoned because it is not older than 1 week + testRepo.reset(initial); + ChangeData cd = createChange().getChange(); + int id5 = cd.getId().get(); + + assertThat(toChangeNumbers(query("is:open"))).containsExactly(id1, id2, id3, id4, id5); + assertThat(query("is:abandoned")).isEmpty(); + + // submit one of the conflicting changes + gApi.changes().id(id3).current().review(ReviewInput.approve()); + gApi.changes().id(id3).current().submit(); + assertThat(toChangeNumbers(query("is:merged"))).containsExactly(id3); + assertThat(toChangeNumbers(query("-is:mergeable"))).containsExactly(id4); + + abandonUtil.abandonInactiveOpenChanges(batchUpdateFactory); + assertThat(toChangeNumbers(query("is:open"))).containsExactly(id5, id2, id1); + assertThat(toChangeNumbers(query("is:abandoned"))).containsExactly(id4); + } + + /** + * When indexMergeable is disabled then the abandonIfMergeable option is ineffective and the auto + * abandon behaves as though it were set to its default value (true). + */ + @Test + @UseClockStep + @GerritConfig(name = "changeCleanup.abandonAfter", value = "1w") + @GerritConfig(name = "changeCleanup.abandonIfMergeable", value = "false") + @GerritConfig(name = "index.change.indexMergeable", value = "false") + public void abandonedIfMergeableWhenMergeableOperatorIsDisabled() throws Exception { + ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId(); + + // create 2 changes + int id1 = createChange().getChange().getId().get(); + int id2 = createChange().getChange().getId().get(); + + // create 2 changes that conflict with each other + testRepo.reset(initial); + int id3 = createChange("change 3", "file.txt", "content").getChange().getId().get(); + testRepo.reset(initial); + int id4 = createChange("change 4", "file.txt", "other content").getChange().getId().get(); + + // make all 4 previously created changes older than 1 week + TestTimeUtil.incrementClock(7 * 24, HOURS); + + // create 1 new change that will not be abandoned because it is not older than 1 week + testRepo.reset(initial); + ChangeData cd = createChange().getChange(); + int id5 = cd.getId().get(); + + assertThat(toChangeNumbers(query("is:open"))).containsExactly(id1, id2, id3, id4, id5); + assertThat(query("is:abandoned")).isEmpty(); + + // submit one of the conflicting changes + gApi.changes().id(id3).current().review(ReviewInput.approve()); + gApi.changes().id(id3).current().submit(); + assertThat(toChangeNumbers(query("is:merged"))).containsExactly(id3); + + BadRequestException thrown = + assertThrows(BadRequestException.class, () -> query("-is:mergeable")); + assertThat(thrown).hasMessageThat().contains("operator is not supported"); + + abandonUtil.abandonInactiveOpenChanges(batchUpdateFactory); + assertThat(toChangeNumbers(query("is:open"))).containsExactly(id5); + assertThat(toChangeNumbers(query("is:abandoned"))).containsExactly(id4, id2, id1); + } + + @Test public void changeCleanupConfigDefaultAbandonMessage() throws Exception { assertThat(cleanupConfig.getAbandonMessage()) .startsWith(
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java index f176663..74f753d 100644 --- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java +++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -74,13 +74,13 @@ import com.google.gerrit.acceptance.ChangeIndexedCounter; import com.google.gerrit.acceptance.ExtensionRegistry; import com.google.gerrit.acceptance.ExtensionRegistry.Registration; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.GitUtil; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.TestProjectInput; import com.google.gerrit.acceptance.UseClockStep; import com.google.gerrit.acceptance.UseTimezone; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.acceptance.testsuite.account.AccountOperations; import com.google.gerrit.acceptance.testsuite.group.GroupOperations; import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; @@ -249,6 +249,7 @@ assertThat(c.owner.email).isNull(); assertThat(c.owner.username).isNull(); assertThat(c.owner.avatars).isNull(); + assertThat(c.submissionId).isNull(); } @Test @@ -314,6 +315,15 @@ } @Test + public void getSubmissionId() throws Exception { + PushOneCommit.Result changeResult = createChange(); + String changeId = changeResult.getChangeId(); + + merge(changeResult); + assertThat(gApi.changes().id(changeId).get().submissionId).isNotNull(); + } + + @Test public void setWorkInProgressNotAllowedWithoutPermission() throws Exception { PushOneCommit.Result rwip = createChange(); String changeId = rwip.getChangeId(); @@ -2194,7 +2204,7 @@ gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).votes(); assertThat(m).hasSize(1); - assertThat(m).containsEntry("Code-Review", Short.valueOf((short) 2)); + assertThat(m).containsExactly("Code-Review", Short.valueOf((short) 2)); requestScopeOperations.setApiUser(user.id()); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.dislike()); @@ -2202,7 +2212,26 @@ m = gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).votes(); assertThat(m).hasSize(1); - assertThat(m).containsEntry("Code-Review", Short.valueOf((short) -1)); + assertThat(m).containsExactly("Code-Review", Short.valueOf((short) -1)); + } + + @Test + @GerritConfig(name = "accounts.visibility", value = "NONE") + public void listVotesEvenWhenAccountsAreNotVisible() throws Exception { + PushOneCommit.Result r = createChange(); + gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); + + requestScopeOperations.setApiUser(user.id()); + + // check finding by address works + Map<String, Short> m = gApi.changes().id(r.getChangeId()).reviewer(admin.email()).votes(); + assertThat(m).hasSize(1); + assertThat(m).containsEntry("Code-Review", Short.valueOf((short) 2)); + + // check finding by id works + m = gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).votes(); + assertThat(m).hasSize(1); + assertThat(m).containsEntry("Code-Review", Short.valueOf((short) 2)); } @Test @@ -3380,6 +3409,7 @@ ChangeInfo change = gApi.changes().id(r.getChangeId()).get(); assertThat(change.status).isEqualTo(ChangeStatus.MERGED); + assertThat(change.submissionId).isNotNull(); assertThat(change.labels.keySet()).containsExactly("Code-Review"); assertThat(change.permittedLabels.keySet()).containsExactly("Code-Review"); assertPermitted(change, "Code-Review", 2); @@ -3540,6 +3570,7 @@ ChangeInfo change = gApi.changes().id(r.getChangeId()).get(); assertThat(change.status).isEqualTo(ChangeStatus.MERGED); + assertThat(change.submissionId).isNotNull(); assertThat(change.labels.keySet()).containsExactly("Code-Review", "Non-Author-Code-Review"); assertThat(change.permittedLabels.keySet()).containsExactly("Code-Review"); assertPermitted(change, "Code-Review", 0, 1, 2); @@ -3555,11 +3586,34 @@ ChangeInfo change = gApi.changes().id(r.getChangeId()).get(); assertThat(change.status).isEqualTo(ChangeStatus.MERGED); + assertThat(change.submissionId).isNotNull(); assertThat(change.labels.keySet()).containsExactly("Code-Review"); assertPermitted(change, "Code-Review", 0, 1, 2); } @Test + public void checkSubmissionIdForAutoClosedChange() throws Exception { + PushOneCommit.Result first = createChange(); + PushOneCommit.Result second = createChange(); + + PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo); + + PushOneCommit.Result result = push.to("refs/heads/master"); + result.assertOkStatus(); + + ChangeInfo firstChange = gApi.changes().id(first.getChangeId()).get(); + assertThat(firstChange.status).isEqualTo(ChangeStatus.MERGED); + assertThat(firstChange.submissionId).isNotNull(); + + ChangeInfo secondChange = gApi.changes().id(second.getChangeId()).get(); + assertThat(secondChange.status).isEqualTo(ChangeStatus.MERGED); + assertThat(secondChange.submissionId).isNotNull(); + + assertThat(secondChange.submissionId).isEqualTo(firstChange.submissionId); + assertThat(gApi.changes().id(second.getChangeId()).submittedTogether()).hasSize(2); + } + + @Test public void maxPermittedValueAllowed() throws Exception { final int minPermittedValue = -2; final int maxPermittedValue = +2; @@ -4383,6 +4437,20 @@ } } + @Test + @GerritConfig(name = "index.change.indexMergeable", value = "true") + public void changeQueryReturnsMergeableWhenGerritIndexMergeable() throws Exception { + String changeId = createChange().getChangeId(); + assertThat(gApi.changes().query(changeId).get().get(0).mergeable).isTrue(); + } + + @Test + @GerritConfig(name = "index.change.indexMergeable", value = "false") + public void changeQueryDoesNotReturnMergeableWhenGerritDoesNotIndexMergeable() throws Exception { + String changeId = createChange().getChangeId(); + assertThat(gApi.changes().query(changeId).get().get(0).mergeable).isNull(); + } + private PushOneCommit.Result createWorkInProgressChange() throws Exception { return pushTo("refs/for/master%wip"); }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java index b8a4871..a0a88f8 100644 --- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java +++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java
@@ -18,8 +18,8 @@ import static com.google.gerrit.testing.GerritJUnit.assertThrows; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; import com.google.gerrit.entities.Project; import com.google.gerrit.extensions.api.changes.ChangeApi;
diff --git a/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java b/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java index 42d62bd..eee25b8 100644 --- a/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java +++ b/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java
@@ -18,8 +18,8 @@ import static com.google.gerrit.testing.GerritJUnit.assertThrows; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.extensions.common.ChangeInput; import com.google.gerrit.extensions.restapi.MethodNotAllowedException; import org.junit.Test;
diff --git a/javatests/com/google/gerrit/acceptance/api/change/QueryChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/QueryChangeIT.java index 92f914b..b5d673c 100644 --- a/javatests/com/google/gerrit/acceptance/api/change/QueryChangeIT.java +++ b/javatests/com/google/gerrit/acceptance/api/change/QueryChangeIT.java
@@ -15,21 +15,32 @@ package com.google.gerrit.acceptance.api.change; import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; +import static java.util.stream.Collectors.toList; +import static javax.servlet.http.HttpServletResponse.SC_OK; import com.google.common.collect.ImmutableList; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.acceptance.UseClockStep; +import com.google.gerrit.acceptance.config.GerritConfig; +import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; +import com.google.gerrit.common.data.Permission; +import com.google.gerrit.entities.Project; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.restapi.TopLevelResource; import com.google.gerrit.server.restapi.change.QueryChanges; import com.google.inject.Inject; import com.google.inject.Provider; import java.util.List; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.junit.TestRepository; import org.junit.Test; @NoHttpd public class QueryChangeIT extends AbstractDaemonTest { - + @Inject private ProjectOperations projectOperations; @Inject private Provider<QueryChanges> queryChangesProvider; @Test @@ -97,6 +108,93 @@ assertThat(result2.get(1).get(0)._moreChanges).isTrue(); } + @Test + @SuppressWarnings("unchecked") + @GerritConfig(name = "operator-alias.change.numberaliastest", value = "change") + public void aliasQuery() throws Exception { + String cId1 = createChange().getChangeId(); + String cId2 = createChange().getChangeId(); + int numericId1 = gApi.changes().id(cId1).get()._number; + int numericId2 = gApi.changes().id(cId2).get()._number; + + QueryChanges queryChanges = queryChangesProvider.get(); + queryChanges.addQuery("numberaliastest:12345"); + queryChanges.addQuery("numberaliastest:" + numericId1); + queryChanges.addQuery("numberaliastest:" + numericId2); + + List<List<ChangeInfo>> result = + (List<List<ChangeInfo>>) queryChanges.apply(TopLevelResource.INSTANCE).value(); + assertThat(result).hasSize(3); + assertThat(result.get(0)).hasSize(0); + assertThat(result.get(1)).hasSize(1); + assertThat(result.get(2)).hasSize(1); + + assertThat(result.get(1).get(0)._number).isEqualTo(numericId1); + assertThat(result.get(2).get(0)._number).isEqualTo(numericId2); + } + + @Test + @UseClockStep + @SuppressWarnings("unchecked") + public void withPagedResults() throws Exception { + // Create 4 visible changes. + createChange(testRepo).getChange().getId().get(); + createChange(testRepo).getChange().getId().get(); + int changeId3 = createChange(testRepo).getChange().getId().get(); + int changeId4 = createChange(testRepo).getChange().getId().get(); + + // Create hidden project. + Project.NameKey hiddenProject = projectOperations.newProject().create(); + projectOperations + .project(hiddenProject) + .forUpdate() + .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS)) + .update(); + TestRepository<InMemoryRepository> hiddenRepo = cloneProject(hiddenProject, admin); + + // Create 2 hidden changes. + createChange(hiddenRepo); + createChange(hiddenRepo); + + // Create a change query that matches all changes (visible and hidden changes). + // The index returns the changes ordered by last updated timestamp: + // hiddenChange2, hiddenChange1, change4, change3, change2, change1 + QueryChanges queryChanges = queryChangesProvider.get(); + queryChanges.addQuery("branch:master"); + + // Set a limit on the query so that we need to paginate over the results from the index. + queryChanges.setLimit(2); + + // Execute the query and verify the results. + // Since the limit is set to 2, at most 2 changes are returned to user, but the index query is + // executed with limit 3 (+1 so that we can populate the _more_changes field on the last + // result). + // This means the index query with limit 3 returns these changes: + // hiddenChange2, hiddenChange1, change4 + // The 2 hidden changes are filtered out because they are not visible to the caller. + // This means we have only one matching result (change4) but the limit (3) is not exhausted + // yet. Hence the next page is loaded from the index (startIndex is 3 to skip the results + // that we already processed, limit is again 3). The results for the next page are: + // change3, change2, change1 + // change2 and change1 are dropped because they are over the limit. + List<ChangeInfo> result = + (List<ChangeInfo>) queryChanges.apply(TopLevelResource.INSTANCE).value(); + assertThat(result.stream().map(i -> i._number).collect(toList())) + .containsExactly(changeId3, changeId4); + } + + @Test + public void usingOutOfRangeLabelValuesDoesNotCauseError() throws Exception { + for (String operator : ImmutableList.of("=", ">", ">=", "<", "<=")) { + QueryChanges queryChanges = queryChangesProvider.get(); + queryChanges.addQuery("label:Code-Review" + operator + "10"); + queryChanges.addQuery("label:Code-Review" + operator + "-10"); + queryChanges.addQuery("Code-Review" + operator + "10"); + queryChanges.addQuery("Code-Review" + operator + "-10"); + assertThat(queryChanges.apply(TopLevelResource.INSTANCE).statusCode()).isEqualTo(SC_OK); + } + } + private static void assertNoChangeHasMoreChangesSet(List<ChangeInfo> results) { for (ChangeInfo info : results) { assertThat(info._moreChanges).isNull();
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java index 0607a3c..7703b1c 100644 --- a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java +++ b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
@@ -23,10 +23,14 @@ import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.TestProjectInput; +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.common.data.Permission; +import com.google.gerrit.entities.BranchNameKey; +import com.google.gerrit.entities.Project; import com.google.gerrit.entities.RefNames; +import com.google.gerrit.extensions.api.changes.ChangeApi; import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.api.changes.RevertInput; import com.google.gerrit.extensions.api.changes.ReviewInput; @@ -36,6 +40,8 @@ import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.ChangeMessageInfo; import com.google.gerrit.extensions.common.PureRevertInfo; +import com.google.gerrit.extensions.common.RevertSubmissionInfo; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; @@ -45,10 +51,13 @@ import com.google.inject.Inject; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.RefSpec; import org.junit.Test; public class RevertIT extends AbstractDaemonTest { @@ -275,6 +284,27 @@ } @Test + public void revertChangeWithLongSubject() throws Exception { + String changeTitle = + "This change has a very long title and therefore it will be cut to 50 characters when the" + + " revert change will revert this change"; + String result = createChange(changeTitle, "a.txt", "message").getChangeId(); + gApi.changes().id(result).current().review(ReviewInput.approve()); + gApi.changes().id(result).current().submit(); + RevertInput revertInput = new RevertInput(); + ChangeInfo revertChange = gApi.changes().id(result).revert(revertInput).get(); + assertThat(revertChange.subject) + .isEqualTo(String.format("Revert \"%s...\"", changeTitle.substring(0, 59))); + assertThat(gApi.changes().id(revertChange.id).current().commit(false).message) + .isEqualTo( + String.format( + "Revert \"%s...\"\n\nThis reverts commit %s.\n\nChange-Id: %s\n", + changeTitle.substring(0, 59), + gApi.changes().id(result).get().currentRevision, + revertChange.changeId)); + } + + @Test public void revertNotifications() throws Exception { PushOneCommit.Result r = createChange(); gApi.changes().id(r.getChangeId()).addReviewer(user.email()); @@ -361,17 +391,20 @@ @Test public void cantCreateRevertWithoutProjectWritePermission() throws Exception { - PushOneCommit.Result r = createChange(); - gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); - gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit(); + PushOneCommit.Result result = createChange(); + gApi.changes() + .id(result.getChangeId()) + .revision(result.getCommit().name()) + .review(ReviewInput.approve()); + gApi.changes().id(result.getChangeId()).revision(result.getCommit().name()).submit(); projectCache.checkedGet(project).getProject().setState(ProjectState.READ_ONLY); + String expected = "project state " + ProjectState.READ_ONLY + " does not permit write"; ResourceConflictException thrown = assertThrows( - ResourceConflictException.class, () -> gApi.changes().id(r.getChangeId()).revert()); - assertThat(thrown) - .hasMessageThat() - .contains("project state " + ProjectState.READ_ONLY + " does not permit write"); + ResourceConflictException.class, + () -> gApi.changes().id(result.getChangeId()).revert()); + assertThat(thrown).hasMessageThat().contains(expected); } @Test @@ -399,17 +432,627 @@ PushOneCommit.Result r = createChange(); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit(); + projectOperations + .project(project) + .forUpdate() + .add(block(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS)) + .update(); + ResourceNotFoundException thrown = + assertThrows( + ResourceNotFoundException.class, () -> gApi.changes().id(r.getChangeId()).revert()); + assertThat(thrown).hasMessageThat().contains("Not found: " + r.getChangeId()); + } + @Test + @GerritConfig(name = "change.submitWholeTopic", value = "true") + public void cantCreateRevertSubmissionWithoutProjectWritePermission() throws Exception { + String secondProject = "secondProject"; + projectOperations.newProject().name(secondProject).create(); + TestRepository<InMemoryRepository> secondRepo = + cloneProject(Project.nameKey("secondProject"), admin); + String topic = "topic"; + String change1 = + createChange(testRepo, "master", "first change", "a.txt", "message", topic).getChangeId(); + String change2 = + createChange(secondRepo, "master", "second change", "b.txt", "message", topic) + .getChangeId(); + gApi.changes().id(change1).current().review(ReviewInput.approve()); + gApi.changes().id(change2).current().review(ReviewInput.approve()); + gApi.changes().id(change1).current().submit(); + + // revoke write permissions for the first repository. + projectCache.checkedGet(project).getProject().setState(ProjectState.READ_ONLY); + + String expected = "project state " + ProjectState.READ_ONLY + " does not permit write"; + + // assert that if first repository has no write permissions, it will fail. + ResourceConflictException thrown = + assertThrows( + ResourceConflictException.class, () -> gApi.changes().id(change1).revertSubmission()); + assertThat(thrown).hasMessageThat().contains(expected); + + // assert that if the first repository has no write permissions and a change from another + // repository is trying to revert the submission, it will fail. + thrown = + assertThrows( + ResourceConflictException.class, () -> gApi.changes().id(change2).revertSubmission()); + assertThat(thrown).hasMessageThat().contains(expected); + } + + @Test + @GerritConfig(name = "change.submitWholeTopic", value = "true") + public void cantCreateRevertSubmissionWithoutCreateChangePermission() throws Exception { + String secondProject = "secondProject"; + projectOperations.newProject().name(secondProject).create(); + TestRepository<InMemoryRepository> secondRepo = + cloneProject(Project.nameKey("secondProject"), admin); + String topic = "topic"; + String change1 = + createChange(testRepo, "master", "first change", "a.txt", "message", topic).getChangeId(); + String change2 = + createChange(secondRepo, "master", "second change", "b.txt", "message", topic) + .getChangeId(); + gApi.changes().id(change1).current().review(ReviewInput.approve()); + gApi.changes().id(change2).current().review(ReviewInput.approve()); + gApi.changes().id(change1).current().submit(); + + // revoke create change permissions for the first repository. + projectOperations + .project(project) + .forUpdate() + .add(block(Permission.PUSH).ref("refs/for/*").group(REGISTERED_USERS)) + .update(); + + // assert that if first repository has no write create change, it will fail. + PermissionDeniedException thrown = + assertThrows( + PermissionDeniedException.class, () -> gApi.changes().id(change1).revertSubmission()); + assertThat(thrown) + .hasMessageThat() + .contains("not permitted: create change on refs/heads/master"); + + // assert that if the first repository has no create change permissions and a change from + // another repository is trying to revert the submission, it will fail. + thrown = + assertThrows( + PermissionDeniedException.class, () -> gApi.changes().id(change2).revertSubmission()); + assertThat(thrown) + .hasMessageThat() + .contains("not permitted: create change on refs/heads/master"); + } + + @Test + @GerritConfig(name = "change.submitWholeTopic", value = "true") + public void cantCreateRevertSubmissionWithoutReadPermission() throws Exception { + String secondProject = "secondProject"; + projectOperations.newProject().name(secondProject).create(); + TestRepository<InMemoryRepository> secondRepo = + cloneProject(Project.nameKey("secondProject"), admin); + String topic = "topic"; + String change1 = + createChange(testRepo, "master", "first change", "a.txt", "message", topic).getChangeId(); + String change2 = + createChange(secondRepo, "master", "second change", "b.txt", "message", topic) + .getChangeId(); + gApi.changes().id(change1).current().review(ReviewInput.approve()); + gApi.changes().id(change2).current().review(ReviewInput.approve()); + gApi.changes().id(change1).current().submit(); + + // revoke read permissions for the first repository. projectOperations .project(project) .forUpdate() .add(block(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS)) .update(); - ResourceNotFoundException thrown = + // assert that if first repository has no read permissions, it will fail. + ResourceNotFoundException resourceNotFoundException = assertThrows( - ResourceNotFoundException.class, () -> gApi.changes().id(r.getChangeId()).revert()); - assertThat(thrown).hasMessageThat().contains("Not found: " + r.getChangeId()); + ResourceNotFoundException.class, () -> gApi.changes().id(change1).revertSubmission()); + assertThat(resourceNotFoundException).hasMessageThat().isEqualTo("Not found: " + change1); + + // assert that if the first repository has no READ permissions and a change from another + // repository is trying to revert the submission, it will fail. + AuthException authException = + assertThrows(AuthException.class, () -> gApi.changes().id(change2).revertSubmission()); + assertThat(authException).hasMessageThat().isEqualTo("read not permitted"); + } + + @Test + public void revertSubmissionPreservesReviewersAndCcs() throws Exception { + String change = createChange("first change", "a.txt", "message").getChangeId(); + + ReviewInput in = ReviewInput.approve(); + in.reviewer(user.email()); + in.reviewer(accountCreator.user2().email(), ReviewerState.CC, true); + // Add user as reviewer that will create the revert + in.reviewer(accountCreator.admin2().email()); + + gApi.changes().id(change).current().review(in); + gApi.changes().id(change).current().submit(); + + // expect both the original reviewers and CCs to be preserved + // original owner should be added as reviewer, user requesting the revert (new owner) removed + requestScopeOperations.setApiUser(accountCreator.admin2().id()); + + Map<ReviewerState, Collection<AccountInfo>> result = + getChangeApis(gApi.changes().id(change).revertSubmission()).get(0).get().reviewers; + assertThat(result).containsKey(ReviewerState.REVIEWER); + + List<Integer> reviewers = + result.get(ReviewerState.REVIEWER).stream().map(a -> a._accountId).collect(toList()); + assertThat(result).containsKey(ReviewerState.CC); + List<Integer> ccs = + result.get(ReviewerState.CC).stream().map(a -> a._accountId).collect(toList()); + assertThat(ccs).containsExactly(accountCreator.user2().id().get()); + assertThat(reviewers).containsExactly(user.id().get(), admin.id().get()); + } + + @Test + public void revertSubmissionNotifications() throws Exception { + String firstResult = createChange("first change", "a.txt", "message").getChangeId(); + approve(firstResult); + gApi.changes().id(firstResult).addReviewer(user.email()); + String secondResult = createChange("second change", "b.txt", "other").getChangeId(); + approve(secondResult); + gApi.changes().id(secondResult).addReviewer(user.email()); + + gApi.changes().id(secondResult).current().submit(); + + sender.clear(); + RevertInput revertInput = new RevertInput(); + revertInput.notify = NotifyHandling.ALL; + + RevertSubmissionInfo revertChanges = + gApi.changes().id(secondResult).revertSubmission(revertInput); + + List<Message> messages = sender.getMessages(); + + assertThat(messages).hasSize(4); + assertThat(sender.getMessages(revertChanges.revertChanges.get(0).changeId, "newchange")) + .hasSize(1); + assertThat(sender.getMessages(firstResult, "revert")).hasSize(1); + assertThat(sender.getMessages(revertChanges.revertChanges.get(1).changeId, "newchange")) + .hasSize(1); + assertThat(sender.getMessages(secondResult, "revert")).hasSize(1); + } + + @Test + public void revertSubmissionIdenticalTreeIsAllowed() throws Exception { + String unrelatedChange = createChange("change1", "a.txt", "message").getChangeId(); + approve(unrelatedChange); + gApi.changes().id(unrelatedChange).current().submit(); + + String emptyChange = createChange("change1", "a.txt", "message").getChangeId(); + approve(emptyChange); + String changeToBeReverted = createChange("change2", "b.txt", "message").getChangeId(); + approve(changeToBeReverted); + + gApi.changes().id(changeToBeReverted).current().submit(); + + sender.clear(); + RevertInput revertInput = new RevertInput(); + revertInput.notify = NotifyHandling.ALL; + + List<ChangeApi> revertChanges = + getChangeApis(gApi.changes().id(changeToBeReverted).revertSubmission(revertInput)); + assertThat(revertChanges.size()).isEqualTo(2); + } + + @Test + public void suppressRevertSubmissionNotifications() throws Exception { + String firstResult = createChange("first change", "a.txt", "message").getChangeId(); + approve(firstResult); + gApi.changes().id(firstResult).addReviewer(user.email()); + String secondResult = createChange("second change", "b.txt", "other").getChangeId(); + approve(secondResult); + gApi.changes().id(secondResult).addReviewer(user.email()); + + gApi.changes().id(secondResult).current().submit(); + + RevertInput revertInput = new RevertInput(); + revertInput.notify = NotifyHandling.NONE; + + sender.clear(); + gApi.changes().id(secondResult).revertSubmission(revertInput); + assertThat(sender.getMessages()).isEmpty(); + } + + @Test + public void revertSubmissionOfSingleChange() throws Exception { + PushOneCommit.Result result = createChange("Change", "a.txt", "message"); + String resultId = result.getChangeId(); + approve(resultId); + gApi.changes().id(resultId).current().submit(); + List<ChangeApi> revertChanges = getChangeApis(gApi.changes().id(resultId).revertSubmission()); + + String sha1Commit = result.getCommit().getName(); + + assertThat(revertChanges.get(0).current().commit(false).parents.get(0).commit) + .isEqualTo(sha1Commit); + + assertThat(revertChanges.get(0).current().files().get("a.txt").linesDeleted).isEqualTo(1); + + assertThat(revertChanges.get(0).get().revertOf) + .isEqualTo(result.getChange().change().getChangeId()); + assertThat(revertChanges.get(0).get().topic) + .startsWith("revert-" + result.getChange().change().getSubmissionId() + "-"); + } + + @Test + public void revertSubmissionWithSetTopic() throws Exception { + String result = createChange().getChangeId(); + gApi.changes().id(result).current().review(ReviewInput.approve()); + gApi.changes().id(result).topic("topic"); + gApi.changes().id(result).current().submit(); + RevertInput revertInput = new RevertInput(); + revertInput.topic = "reverted-not-default"; + assertThat(gApi.changes().id(result).revertSubmission(revertInput).revertChanges.get(0).topic) + .isEqualTo(revertInput.topic); + } + + @Test + public void revertSubmissionWithSetMessage() throws Exception { + String result = createChange("change", "a.txt", "message").getChangeId(); + gApi.changes().id(result).current().review(ReviewInput.approve()); + gApi.changes().id(result).current().submit(); + RevertInput revertInput = new RevertInput(); + String commitMessage = "Message from input"; + revertInput.message = commitMessage; + ChangeInfo revertChange = + gApi.changes().id(result).revertSubmission(revertInput).revertChanges.get(0); + assertThat(revertChange.subject).isEqualTo("Revert \"change\""); + assertThat(gApi.changes().id(revertChange.id).current().commit(false).message) + .isEqualTo( + String.format( + "Revert \"change\"\n\n%s\n\nChange-Id: %s\n", + commitMessage, revertChange.changeId)); + } + + @Test + public void revertSubmissionWithoutMessage() throws Exception { + String result = createChange("change", "a.txt", "message").getChangeId(); + gApi.changes().id(result).current().review(ReviewInput.approve()); + gApi.changes().id(result).current().submit(); + RevertInput revertInput = new RevertInput(); + ChangeInfo revertChange = + gApi.changes().id(result).revertSubmission(revertInput).revertChanges.get(0); + assertThat(revertChange.subject).isEqualTo("Revert \"change\""); + assertThat(gApi.changes().id(revertChange.id).current().commit(false).message) + .isEqualTo( + String.format( + "Revert \"change\"\n\nThis reverts commit %s.\n\nChange-Id: %s\n", + gApi.changes().id(result).get().currentRevision, revertChange.changeId)); + } + + @Test + public void revertSubmissionRevertsChangeWithLongSubject() throws Exception { + String changeTitle = + "This change has a very long title and therefore it will be cut to 50 characters when the" + + " revert change will revert this change"; + String result = createChange(changeTitle, "a.txt", "message").getChangeId(); + gApi.changes().id(result).current().review(ReviewInput.approve()); + gApi.changes().id(result).current().submit(); + RevertInput revertInput = new RevertInput(); + ChangeInfo revertChange = + gApi.changes().id(result).revertSubmission(revertInput).revertChanges.get(0); + assertThat(revertChange.subject) + .isEqualTo(String.format("Revert \"%s...\"", changeTitle.substring(0, 59))); + assertThat(gApi.changes().id(revertChange.id).current().commit(false).message) + .isEqualTo( + String.format( + "Revert \"%s...\"\n\nThis reverts commit %s.\n\nChange-Id: %s\n", + changeTitle.substring(0, 59), + gApi.changes().id(result).get().currentRevision, + revertChange.changeId)); + } + + @Test + @GerritConfig(name = "change.submitWholeTopic", value = "true") + public void revertSubmissionDifferentRepositoriesWithDependantChange() throws Exception { + projectOperations.newProject().name("secondProject").create(); + TestRepository<InMemoryRepository> secondRepo = + cloneProject(Project.nameKey("secondProject"), admin); + List<PushOneCommit.Result> resultCommits = new ArrayList<>(); + String topic = "topic"; + resultCommits.add( + createChange(secondRepo, "master", "first change", "a.txt", "message", topic)); + resultCommits.add( + createChange(secondRepo, "master", "second change", "b.txt", "Other message", topic)); + resultCommits.add( + createChange(testRepo, "master", "main repo change", "a.txt", "message", topic)); + for (PushOneCommit.Result result : resultCommits) { + approve(result.getChangeId()); + } + // submit all changes + gApi.changes().id(resultCommits.get(1).getChangeId()).current().submit(); + List<ChangeApi> revertChanges = + getChangeApis(gApi.changes().id(resultCommits.get(1).getChangeId()).revertSubmission()); + + assertThat(revertChanges).hasSize(3); + + String sha1RevertOfTheSecondChange = revertChanges.get(1).current().commit(false).commit; + String sha1SecondChange = resultCommits.get(1).getCommit().getName(); + String sha1ThirdChange = resultCommits.get(2).getCommit().getName(); + assertThat(revertChanges.get(0).current().commit(false).parents.get(0).commit) + .isEqualTo(sha1RevertOfTheSecondChange); + assertThat(revertChanges.get(1).current().commit(false).parents.get(0).commit) + .isEqualTo(sha1SecondChange); + assertThat(revertChanges.get(2).current().commit(false).parents.get(0).commit) + .isEqualTo(sha1ThirdChange); + + assertThat(revertChanges.get(0).current().files().get("a.txt").linesDeleted).isEqualTo(1); + assertThat(revertChanges.get(1).current().files().get("b.txt").linesDeleted).isEqualTo(1); + assertThat(revertChanges.get(2).current().files().get("a.txt").linesDeleted).isEqualTo(1); + // has size 3 because of the same topic, and submitWholeTopic is true. + assertThat(gApi.changes().id(revertChanges.get(0).get()._number).submittedTogether()) + .hasSize(3); + + // expected messages on source change: + // 1. Uploaded patch set 1. + // 2. Patch Set 1: Code-Review+2 + // 3. Change has been successfully merged by Administrator + // 4. Created a revert of this change as %s + + for (int i = 0; i < resultCommits.size(); i++) { + assertThat(revertChanges.get(i).get().revertOf) + .isEqualTo(resultCommits.get(i).getChange().change().getChangeId()); + List<ChangeMessageInfo> sourceMessages = + new ArrayList<>(gApi.changes().id(resultCommits.get(i).getChangeId()).get().messages); + assertThat(sourceMessages).hasSize(4); + String expectedMessage = + String.format( + "Created a revert of this change as %s", revertChanges.get(i).get().changeId); + assertThat(sourceMessages.get(3).message).isEqualTo(expectedMessage); + // Expected message on the created change: "Uploaded patch set 1." + List<ChangeMessageInfo> messages = + revertChanges.get(i).get().messages.stream().collect(toList()); + assertThat(messages).hasSize(1); + assertThat(messages.get(0).message).isEqualTo("Uploaded patch set 1."); + assertThat(revertChanges.get(i).get().revertOf) + .isEqualTo(gApi.changes().id(resultCommits.get(i).getChangeId()).get()._number); + assertThat(revertChanges.get(i).get().topic) + .startsWith("revert-" + resultCommits.get(0).getChange().change().getSubmissionId()); + } + + assertThat(gApi.changes().id(revertChanges.get(1).id()).current().related().changes).hasSize(2); + } + + @Test + public void cantRevertSubmissionWithAnOpenChange() throws Exception { + String result = createChange("change", "a.txt", "message").getChangeId(); + approve(result); + ResourceConflictException thrown = + assertThrows( + ResourceConflictException.class, () -> gApi.changes().id(result).revertSubmission()); + assertThat(thrown).hasMessageThat().isEqualTo("change is new."); + } + + @Test + public void revertSubmissionWithDependantChange() throws Exception { + PushOneCommit.Result firstResult = createChange("first change", "a.txt", "message"); + PushOneCommit.Result secondResult = createChange("second change", "b.txt", "other"); + approve(secondResult.getChangeId()); + approve(firstResult.getChangeId()); + gApi.changes().id(secondResult.getChangeId()).current().submit(); + List<ChangeApi> revertChanges = + getChangeApis(gApi.changes().id(firstResult.getChangeId()).revertSubmission()); + Collections.reverse(revertChanges); + String sha1SecondChange = secondResult.getCommit().getName(); + String sha1FirstRevert = revertChanges.get(0).current().commit(false).commit; + assertThat(revertChanges.get(0).current().commit(false).parents.get(0).commit) + .isEqualTo(sha1SecondChange); + assertThat(revertChanges.get(1).current().commit(false).parents.get(0).commit) + .isEqualTo(sha1FirstRevert); + assertThat(revertChanges.get(0).get().revertOf) + .isEqualTo(secondResult.getChange().change().getChangeId()); + assertThat(revertChanges.get(1).get().revertOf) + .isEqualTo(firstResult.getChange().change().getChangeId()); + assertThat(revertChanges.get(0).current().files().get("b.txt").linesDeleted).isEqualTo(1); + assertThat(revertChanges.get(1).current().files().get("a.txt").linesDeleted).isEqualTo(1); + + assertThat(revertChanges).hasSize(2); + assertThat(gApi.changes().id(revertChanges.get(0).id()).current().related().changes).hasSize(2); + } + + @Test + public void revertSubmissionWithDependantChangeWithoutRevertingLastOne() throws Exception { + PushOneCommit.Result firstResult = createChange("first change", "a.txt", "message"); + PushOneCommit.Result secondResult = createChange("second change", "b.txt", "other"); + approve(secondResult.getChangeId()); + approve(firstResult.getChangeId()); + gApi.changes().id(secondResult.getChangeId()).current().submit(); + String unrelated = createChange("other change", "c.txt", "message other").getChangeId(); + approve(unrelated); + gApi.changes().id(unrelated).current().submit(); + List<ChangeApi> revertChanges = + getChangeApis(gApi.changes().id(firstResult.getChangeId()).revertSubmission()); + Collections.reverse(revertChanges); + String sha1SecondChange = secondResult.getCommit().getName(); + String sha1FirstRevert = revertChanges.get(0).current().commit(false).commit; + assertThat(revertChanges.get(0).current().commit(false).parents.get(0).commit) + .isEqualTo(sha1SecondChange); + assertThat(revertChanges.get(1).current().commit(false).parents.get(0).commit) + .isEqualTo(sha1FirstRevert); + assertThat(revertChanges.get(0).get().revertOf) + .isEqualTo(secondResult.getChange().change().getChangeId()); + assertThat(revertChanges.get(1).get().revertOf) + .isEqualTo(firstResult.getChange().change().getChangeId()); + assertThat(revertChanges.get(0).current().files().get("b.txt").linesDeleted).isEqualTo(1); + assertThat(revertChanges.get(1).current().files().get("a.txt").linesDeleted).isEqualTo(1); + + assertThat(revertChanges).hasSize(2); + assertThat(gApi.changes().id(revertChanges.get(0).id()).current().related().changes).hasSize(2); + } + + @Test + @GerritConfig(name = "change.submitWholeTopic", value = "true") + public void revertSubmissionDifferentRepositories() throws Exception { + projectOperations.newProject().name("secondProject").create(); + TestRepository<InMemoryRepository> secondRepo = + cloneProject(Project.nameKey("secondProject"), admin); + String topic = "topic"; + PushOneCommit.Result firstResult = + createChange(testRepo, "master", "first change", "a.txt", "message", topic); + PushOneCommit.Result secondResult = + createChange(secondRepo, "master", "second change", "b.txt", "other", topic); + approve(secondResult.getChangeId()); + approve(firstResult.getChangeId()); + // submit both changes + gApi.changes().id(secondResult.getChangeId()).current().submit(); + List<ChangeApi> revertChanges = + getChangeApis(gApi.changes().id(secondResult.getChangeId()).revertSubmission()); + // has size 2 because of the same topic, and submitWholeTopic is true. + assertThat(gApi.changes().id(revertChanges.get(0).get()._number).submittedTogether()) + .hasSize(2); + String sha1SecondChange = secondResult.getCommit().getName(); + String sha1FirstChange = firstResult.getCommit().getName(); + assertThat(revertChanges.get(0).current().commit(false).parents.get(0).commit) + .isEqualTo(sha1FirstChange); + assertThat(revertChanges.get(1).current().commit(false).parents.get(0).commit) + .isEqualTo(sha1SecondChange); + assertThat(revertChanges.get(0).get().revertOf) + .isEqualTo(firstResult.getChange().change().getChangeId()); + assertThat(revertChanges.get(1).get().revertOf) + .isEqualTo(secondResult.getChange().change().getChangeId()); + assertThat(revertChanges.get(0).current().files().get("a.txt").linesDeleted).isEqualTo(1); + assertThat(revertChanges.get(1).current().files().get("b.txt").linesDeleted).isEqualTo(1); + + assertThat(revertChanges).hasSize(2); + } + + @Test + @GerritConfig(name = "change.submitWholeTopic", value = "true") + public void revertSubmissionMultipleBranches() throws Exception { + List<PushOneCommit.Result> resultCommits = new ArrayList<>(); + String topic = "topic"; + resultCommits.add(createChange(testRepo, "master", "first change", "c.txt", "message", topic)); + testRepo.reset("HEAD~1"); + createBranch(BranchNameKey.create(project, "other")); + resultCommits.add(createChange(testRepo, "other", "second change", "a.txt", "message", topic)); + resultCommits.add( + createChange(testRepo, "other", "third change", "b.txt", "Other message", topic)); + for (PushOneCommit.Result result : resultCommits) { + approve(result.getChangeId()); + } + // submit all changes + gApi.changes().id(resultCommits.get(1).getChangeId()).current().submit(); + List<ChangeApi> revertChanges = + getChangeApis(gApi.changes().id(resultCommits.get(1).getChangeId()).revertSubmission()); + assertThat(revertChanges.get(0).current().files().get("c.txt").linesDeleted).isEqualTo(1); + assertThat(revertChanges.get(1).current().files().get("a.txt").linesDeleted).isEqualTo(1); + assertThat(revertChanges.get(2).current().files().get("b.txt").linesDeleted).isEqualTo(1); + String sha1FirstChange = resultCommits.get(0).getCommit().getName(); + String sha1ThirdChange = resultCommits.get(2).getCommit().getName(); + String sha1SecondRevert = revertChanges.get(2).current().commit(false).commit; + assertThat(revertChanges.get(0).current().commit(false).parents.get(0).commit) + .isEqualTo(sha1FirstChange); + assertThat(revertChanges.get(2).current().commit(false).parents.get(0).commit) + .isEqualTo(sha1ThirdChange); + assertThat(revertChanges.get(1).current().commit(false).parents.get(0).commit) + .isEqualTo(sha1SecondRevert); + + assertThat(revertChanges).hasSize(3); + assertThat(gApi.changes().id(revertChanges.get(1).id()).current().related().changes).hasSize(2); + } + + @Test + @GerritConfig(name = "change.submitWholeTopic", value = "true") + public void revertSubmissionDependantAndUnrelatedWithMerge() throws Exception { + String topic = "topic"; + PushOneCommit.Result firstResult = + createChange(testRepo, "master", "first change", "a.txt", "message", topic); + approve(firstResult.getChangeId()); + PushOneCommit.Result secondResult = + createChange(testRepo, "master", "second change", "b.txt", "message", topic); + approve(secondResult.getChangeId()); + testRepo.reset("HEAD~1"); + PushOneCommit.Result thirdResult = + createChange(testRepo, "master", "third change", "c.txt", "message", topic); + approve(thirdResult.getChangeId()); + + gApi.changes().id(firstResult.getChangeId()).current().submit(); + + // put the head on the merge commit created by submitting the second and third change. + testRepo.git().fetch().setRefSpecs(new RefSpec("refs/heads/master:merge")).call(); + testRepo.reset("merge"); + + // Create another change that should be ignored. The reverts should be rebased on top of the + // merge commit. + PushOneCommit.Result fourthResult = + createChange(testRepo, "master", "fourth change", "d.txt", "message", topic); + approve(fourthResult.getChangeId()); + gApi.changes().id(fourthResult.getChangeId()).current().submit(); + + List<ChangeApi> revertChanges = + getChangeApis(gApi.changes().id(secondResult.getChangeId()).revertSubmission()); + Collections.reverse(revertChanges); + assertThat(revertChanges.get(0).current().files().get("c.txt").linesDeleted).isEqualTo(1); + assertThat(revertChanges.get(1).current().files().get("b.txt").linesDeleted).isEqualTo(1); + assertThat(revertChanges.get(2).current().files().get("a.txt").linesDeleted).isEqualTo(1); + String sha1FirstRevert = revertChanges.get(0).current().commit(false).commit; + String sha1SecondRevert = revertChanges.get(1).current().commit(false).commit; + // parent of the first revert is the merged change of previous changes. + assertThat(revertChanges.get(0).current().commit(false).parents.get(0).subject) + .contains("Merge"); + // Next reverts would stack on top of the previous ones. + assertThat(revertChanges.get(1).current().commit(false).parents.get(0).commit) + .isEqualTo(sha1FirstRevert); + assertThat(revertChanges.get(2).current().commit(false).parents.get(0).commit) + .isEqualTo(sha1SecondRevert); + + assertThat(revertChanges).hasSize(3); + assertThat(gApi.changes().id(revertChanges.get(1).id()).current().related().changes).hasSize(3); + } + + @Test + @GerritConfig(name = "change.submitWholeTopic", value = "true") + public void revertSubmissionUnrelatedWithTwoMergeCommits() throws Exception { + String topic = "topic"; + PushOneCommit.Result firstResult = + createChange(testRepo, "master", "first change", "a.txt", "message", topic); + approve(firstResult.getChangeId()); + testRepo.reset("HEAD~1"); + PushOneCommit.Result secondResult = + createChange(testRepo, "master", "second change", "b.txt", "message", topic); + approve(secondResult.getChangeId()); + testRepo.reset("HEAD~1"); + PushOneCommit.Result thirdResult = + createChange(testRepo, "master", "third change", "c.txt", "message", topic); + approve(thirdResult.getChangeId()); + + gApi.changes().id(firstResult.getChangeId()).current().submit(); + + // put the head on the most recent merge commit. + testRepo.git().fetch().setRefSpecs(new RefSpec("refs/heads/master:merge")).call(); + testRepo.reset("merge"); + + // Create another change that should be ignored. The reverts should be rebased on top of the + // merge commit. + PushOneCommit.Result fourthResult = + createChange(testRepo, "master", "fourth change", "d.txt", "message", topic); + approve(fourthResult.getChangeId()); + gApi.changes().id(fourthResult.getChangeId()).current().submit(); + + List<ChangeApi> revertChanges = + getChangeApis(gApi.changes().id(secondResult.getChangeId()).revertSubmission()); + Collections.reverse(revertChanges); + assertThat(revertChanges.get(0).current().files().get("c.txt").linesDeleted).isEqualTo(1); + assertThat(revertChanges.get(1).current().files().get("b.txt").linesDeleted).isEqualTo(1); + assertThat(revertChanges.get(2).current().files().get("a.txt").linesDeleted).isEqualTo(1); + String sha1FirstRevert = revertChanges.get(0).current().commit(false).commit; + String sha1SecondRevert = revertChanges.get(1).current().commit(false).commit; + // parent of the first revert is the merged change of previous changes. + assertThat(revertChanges.get(0).current().commit(false).parents.get(0).subject) + .contains("Merge \"third change\""); + // Next reverts would stack on top of the previous ones. + assertThat(revertChanges.get(1).current().commit(false).parents.get(0).commit) + .isEqualTo(sha1FirstRevert); + assertThat(revertChanges.get(2).current().commit(false).parents.get(0).commit) + .isEqualTo(sha1SecondRevert); + + assertThat(revertChanges).hasSize(3); + assertThat(gApi.changes().id(revertChanges.get(1).id()).current().related().changes).hasSize(3); } @Override @@ -451,4 +1094,13 @@ .create(); } } + + private List<ChangeApi> getChangeApis(RevertSubmissionInfo revertSubmissionInfo) + throws Exception { + List<ChangeApi> results = new ArrayList<>(); + for (ChangeInfo changeInfo : revertSubmissionInfo.revertChanges) { + results.add(gApi.changes().id(changeInfo._number)); + } + return results; + } }
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java index 07fb577..988580e 100644 --- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java +++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -40,7 +40,6 @@ import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.ExtensionRegistry; import com.google.gerrit.acceptance.ExtensionRegistry.Registration; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.GitUtil; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.ProjectResetter; @@ -48,6 +47,7 @@ import com.google.gerrit.acceptance.Sandboxed; import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.acceptance.UseClockStep; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.acceptance.testsuite.account.AccountOperations; import com.google.gerrit.acceptance.testsuite.group.GroupOperations; import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; @@ -187,6 +187,24 @@ } @Test + public void removeMember_nullInMemberInputDoesNotCauseFailure() throws Exception { + AccountGroup.UUID group = + groupOperations.newGroup().addMember(admin.id()).addMember(user.id()).create(); + gApi.groups().id(group.get()).removeMembers(user.id().toString(), null); + ImmutableSet<Account.Id> members = groupOperations.group(group).get().members(); + assertThat(members).containsExactly(admin.id()); + } + + @Test + public void removeMember_emptyStringInMemberInputDoesNotCauseFailure() throws Exception { + AccountGroup.UUID group = + groupOperations.newGroup().addMember(admin.id()).addMember(user.id()).create(); + gApi.groups().id(group.get()).removeMembers(user.id().toString(), ""); + ImmutableSet<Account.Id> members = groupOperations.group(group).get().members(); + assertThat(members).containsExactly(admin.id()); + } + + @Test public void cachedGroupsForMemberAreUpdatedOnMemberAdditionAndRemoval() throws Exception { String username = name("user"); Account.Id accountId = accountOperations.newAccount().username(username).create(); @@ -1265,7 +1283,7 @@ // Newly created group is not stale GroupInfo groupInfo = gApi.groups().create(name("foo")).get(); AccountGroup.UUID groupUuid = AccountGroup.uuid(groupInfo.id); - assertThat(stalenessChecker.isStale(groupUuid)).isFalse(); + assertThat(stalenessChecker.check(groupUuid).isStale()).isFalse(); // Manual update makes index document stale String groupRef = RefNames.refsGroups(groupUuid); @@ -1406,11 +1424,11 @@ private void assertStaleGroupAndReindex(AccountGroup.UUID groupUuid) throws IOException { // Evict group from cache to be sure that we use the index state for staleness checks. groupCache.evict(groupUuid); - assertThat(stalenessChecker.isStale(groupUuid)).isTrue(); + assertThat(stalenessChecker.check(groupUuid).isStale()).isTrue(); // Reindex fixes staleness groupIndexer.index(groupUuid); - assertThat(stalenessChecker.isStale(groupUuid)).isFalse(); + assertThat(stalenessChecker.check(groupUuid).isStale()).isFalse(); } private void pushToGroupBranchForReviewAndSubmit(
diff --git a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java index a120eac..ff873dd 100644 --- a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java +++ b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
@@ -21,8 +21,8 @@ import com.google.common.collect.ImmutableList; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; import com.google.gerrit.common.RawInputUtil; import com.google.gerrit.extensions.api.plugins.InstallPluginInput;
diff --git a/javatests/com/google/gerrit/acceptance/api/plugin/PluginLoaderIT.java b/javatests/com/google/gerrit/acceptance/api/plugin/PluginLoaderIT.java index 7eb3680..744cc2a 100644 --- a/javatests/com/google/gerrit/acceptance/api/plugin/PluginLoaderIT.java +++ b/javatests/com/google/gerrit/acceptance/api/plugin/PluginLoaderIT.java
@@ -15,8 +15,8 @@ package com.google.gerrit.acceptance.api.plugin; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.server.plugins.MissingMandatoryPluginsException; import org.junit.Test; import org.junit.runner.Description;
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java index eebcc5b..2801b36 100644 --- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java +++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -35,10 +35,10 @@ import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.ExtensionRegistry; import com.google.gerrit.acceptance.ExtensionRegistry.Registration; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.GitUtil; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; +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.common.Nullable; @@ -49,6 +49,7 @@ import com.google.gerrit.extensions.annotations.Exports; import com.google.gerrit.extensions.api.projects.BranchInput; import com.google.gerrit.extensions.api.projects.CommentLinkInfo; +import com.google.gerrit.extensions.api.projects.CommentLinkInput; import com.google.gerrit.extensions.api.projects.ConfigInfo; import com.google.gerrit.extensions.api.projects.ConfigInput; import com.google.gerrit.extensions.api.projects.ConfigValue; @@ -68,11 +69,16 @@ import com.google.gerrit.server.group.SystemGroupBackend; import com.google.gerrit.server.index.IndexExecutor; import com.google.gerrit.server.project.CommentLinkInfoImpl; +import com.google.gerrit.server.project.ProjectConfig; import com.google.inject.AbstractModule; import com.google.inject.Inject; import com.google.inject.Module; import java.util.HashMap; import java.util.Map; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.transport.PushResult; import org.eclipse.jgit.transport.RemoteRefUpdate.Status; @@ -717,6 +723,191 @@ assertCommentLinks(getConfig(), expected); } + @Test + public void projectConfigUsesLocallySetCommentlinks() throws Exception { + ConfigInput input = new ConfigInput(); + addCommentLink(input, BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK); + addCommentLink(input, JIRA, JIRA_MATCH, JIRA_LINK); + ConfigInfo info = setConfig(project, input); + + Map<String, CommentLinkInfo> expected = new HashMap<>(); + expected.put(BUGZILLA, commentLinkInfo(BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK)); + expected.put(JIRA, commentLinkInfo(JIRA, JIRA_MATCH, JIRA_LINK)); + assertCommentLinks(info, expected); + assertCommentLinks(getConfig(), expected); + } + + @Test + @GerritConfig(name = "commentlink.bugzilla.match", value = BUGZILLA_MATCH) + @GerritConfig(name = "commentlink.bugzilla.link", value = BUGZILLA_LINK) + public void projectConfigUsesCommentLinksFromGlobalAndLocal() throws Exception { + Map<String, CommentLinkInfo> expected = new HashMap<>(); + expected.put(BUGZILLA, commentLinkInfo(BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK)); + assertCommentLinks(getConfig(), expected); + + ConfigInput input = new ConfigInput(); + addCommentLink(input, JIRA, JIRA_MATCH, JIRA_LINK); + ConfigInfo info = setConfig(project, input); + expected.put(JIRA, commentLinkInfo(JIRA, JIRA_MATCH, JIRA_LINK)); + + assertCommentLinks(info, expected); + assertCommentLinks(getConfig(), expected); + } + + @Test + @GerritConfig(name = "commentlink.bugzilla.match", value = BUGZILLA_MATCH) + @GerritConfig(name = "commentlink.bugzilla.link", value = BUGZILLA_LINK) + public void localCommentLinkOverridesGlobalConfig() throws Exception { + String otherLink = "https://other.example.com"; + ConfigInput input = new ConfigInput(); + addCommentLink(input, BUGZILLA, BUGZILLA_MATCH, otherLink); + + Map<String, CommentLinkInfo> expected = new HashMap<>(); + expected.put(BUGZILLA, commentLinkInfo(BUGZILLA, BUGZILLA_MATCH, otherLink)); + + ConfigInfo info = setConfig(project, input); + assertCommentLinks(info, expected); + assertCommentLinks(getConfig(), expected); + } + + @Test + public void localCommentLinksAreInheritedFromParent() throws Exception { + ConfigInput input = new ConfigInput(); + addCommentLink(input, BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK); + addCommentLink(input, JIRA, JIRA_MATCH, JIRA_LINK); + ConfigInfo info = setConfig(project, input); + + Map<String, CommentLinkInfo> expected = new HashMap<>(); + expected.put(BUGZILLA, commentLinkInfo(BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK)); + expected.put(JIRA, commentLinkInfo(JIRA, JIRA_MATCH, JIRA_LINK)); + assertCommentLinks(info, expected); + + Project.NameKey child = projectOperations.newProject().parent(project).create(); + assertCommentLinks(getConfig(child), expected); + } + + @Test + public void localCommentLinkOverridesParentCommentLink() throws Exception { + ConfigInput input = new ConfigInput(); + addCommentLink(input, BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK); + addCommentLink(input, JIRA, JIRA_MATCH, JIRA_LINK); + ConfigInfo info = setConfig(project, input); + + Map<String, CommentLinkInfo> expected = new HashMap<>(); + expected.put(BUGZILLA, commentLinkInfo(BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK)); + expected.put(JIRA, commentLinkInfo(JIRA, JIRA_MATCH, JIRA_LINK)); + assertCommentLinks(info, expected); + + Project.NameKey child = projectOperations.newProject().parent(project).create(); + + String otherLink = "https://other.example.com"; + input = new ConfigInput(); + addCommentLink(input, BUGZILLA, BUGZILLA_MATCH, otherLink); + info = setConfig(child, input); + + expected = new HashMap<>(); + expected.put(BUGZILLA, commentLinkInfo(BUGZILLA, BUGZILLA_MATCH, otherLink)); + expected.put(JIRA, commentLinkInfo(JIRA, JIRA_MATCH, JIRA_LINK)); + + assertCommentLinks(getConfig(child), expected); + } + + @Test + public void updateExistingCommentLink() throws Exception { + ConfigInput input = new ConfigInput(); + addCommentLink(input, BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK); + addCommentLink(input, JIRA, JIRA_MATCH, JIRA_LINK); + ConfigInfo info = setConfig(project, input); + + Map<String, CommentLinkInfo> expected = new HashMap<>(); + expected.put(BUGZILLA, commentLinkInfo(BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK)); + expected.put(JIRA, commentLinkInfo(JIRA, JIRA_MATCH, JIRA_LINK)); + assertCommentLinks(info, expected); + + String otherLink = "https://other.example.com"; + input = new ConfigInput(); + addCommentLink(input, BUGZILLA, BUGZILLA_MATCH, otherLink); + info = setConfig(project, input); + + expected = new HashMap<>(); + expected.put(BUGZILLA, commentLinkInfo(BUGZILLA, BUGZILLA_MATCH, otherLink)); + expected.put(JIRA, commentLinkInfo(JIRA, JIRA_MATCH, JIRA_LINK)); + assertCommentLinks(getConfig(project), expected); + } + + @Test + public void removeCommentLink() throws Exception { + ConfigInput input = new ConfigInput(); + addCommentLink(input, BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK); + addCommentLink(input, JIRA, JIRA_MATCH, JIRA_LINK); + ConfigInfo info = setConfig(project, input); + + Map<String, CommentLinkInfo> expected = new HashMap<>(); + expected.put(BUGZILLA, commentLinkInfo(BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK)); + expected.put(JIRA, commentLinkInfo(JIRA, JIRA_MATCH, JIRA_LINK)); + assertCommentLinks(info, expected); + + input = new ConfigInput(); + addCommentLink(input, BUGZILLA, null); + info = setConfig(project, input); + + expected = new HashMap<>(); + expected.put(JIRA, commentLinkInfo(JIRA, JIRA_MATCH, JIRA_LINK)); + assertCommentLinks(getConfig(project), expected); + } + + @Test + public void cannotPushLabelDefinitionWithDuplicateValues() throws Exception { + Config cfg = new Config(); + cfg.fromText(projectOperations.project(allProjects).getConfig().toText()); + cfg.setStringList( + "label", + "Code-Review", + "value", + ImmutableList.of("+1 LGTM", "1 LGTM", "0 No Value", "-1 Looks Bad")); + + TestRepository<InMemoryRepository> repo = cloneProject(allProjects); + GitUtil.fetch(repo, RefNames.REFS_CONFIG + ":" + RefNames.REFS_CONFIG); + repo.reset(RefNames.REFS_CONFIG); + PushOneCommit.Result r = + pushFactory + .create(admin.newIdent(), repo, "Subject", "project.config", cfg.toText()) + .to(RefNames.REFS_CONFIG); + r.assertErrorStatus("invalid project configuration"); + r.assertMessage("project.config: duplicate value \"1 lgtm\" for label \"code-review\""); + } + + @Test + public void getProjectThatHasLabelDefinitionWithDuplicateValues() throws Exception { + // Update the definition of the Code-Review label so that it has the value "+1 LGTM" twice. + // This update bypasses all validation checks so that the duplicate label value doesn't get + // rejected. + Config cfg = new Config(); + cfg.fromText(projectOperations.project(allProjects).getConfig().toText()); + cfg.setStringList( + "label", + "Code-Review", + "value", + ImmutableList.of("+1 LGTM", "1 LGTM", "0 No Value", "-1 Looks Bad")); + + try (TestRepository<Repository> repo = + new TestRepository<>(repoManager.openRepository(allProjects))) { + repo.update( + RefNames.REFS_CONFIG, + repo.commit() + .message("Set label with duplicate value") + .parent(getHead(repo.getRepository(), RefNames.REFS_CONFIG)) + .add(ProjectConfig.PROJECT_CONFIG, cfg.toText())); + } + + // Verify that project info can be retrieved and that the label value "+1 LGTM" appears only + // once. + ProjectInfo projectInfo = gApi.projects().name(allProjects.get()).get(); + assertThat(projectInfo.labels.keySet()).containsExactly("Code-Review"); + assertThat(projectInfo.labels.get("Code-Review").values) + .containsExactly("+1", "LGTM", " 0", "No Value", "-1", "Looks Bad"); + } + private CommentLinkInfo commentLinkInfo(String name, String match, String link) { return new CommentLinkInfoImpl(name, match, link, null /*html*/, null /*enabled*/); } @@ -725,6 +916,21 @@ assertThat(actual.commentlinks).containsExactlyEntriesIn(expected); } + private void addCommentLink(ConfigInput configInput, String name, String match, String link) { + CommentLinkInput commentLinkInput = new CommentLinkInput(); + commentLinkInput.match = match; + commentLinkInput.link = link; + addCommentLink(configInput, name, commentLinkInput); + } + + private void addCommentLink( + ConfigInput configInput, String name, CommentLinkInput commentLinkInput) { + if (configInput.commentLinks == null) { + configInput.commentLinks = new HashMap<>(); + } + configInput.commentLinks.put(name, commentLinkInput); + } + private ConfigInfo setConfig(Project.NameKey name, ConfigInput input) throws Exception { return gApi.projects().name(name.get()).config(input); }
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java index 019df0e..dad09f9 100644 --- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java +++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
@@ -23,6 +23,7 @@ import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; import com.google.gerrit.entities.Project; +import com.google.gerrit.exceptions.StorageException; import com.google.gerrit.index.IndexConfig; import com.google.gerrit.index.QueryOptions; import com.google.gerrit.index.RefState; @@ -83,19 +84,19 @@ @Test public void stalenessChecker_currentProject_notStale() throws Exception { - assertThat(stalenessChecker.isStale(project)).isFalse(); + assertThat(stalenessChecker.check(project).isStale()).isFalse(); } @Test public void stalenessChecker_currentProjectUpdates_isStale() throws Exception { updateProjectConfigWithoutIndexUpdate(project); - assertThat(stalenessChecker.isStale(project)).isTrue(); + assertThat(stalenessChecker.check(project).isStale()).isTrue(); } @Test public void stalenessChecker_parentProjectUpdates_isStale() throws Exception { updateProjectConfigWithoutIndexUpdate(allProjects); - assertThat(stalenessChecker.isStale(project)).isTrue(); + assertThat(stalenessChecker.check(project).isStale()).isTrue(); } @Test @@ -106,10 +107,10 @@ u.getConfig().getProject().setParentName(p1); u.save(); } - assertThat(stalenessChecker.isStale(project)).isFalse(); + assertThat(stalenessChecker.check(project).isStale()).isFalse(); updateProjectConfigWithoutIndexUpdate(p1, c -> c.getProject().setParentName(p2)); - assertThat(stalenessChecker.isStale(project)).isTrue(); + assertThat(stalenessChecker.check(project).isStale()).isTrue(); } private void updateProjectConfigWithoutIndexUpdate(Project.NameKey project) throws Exception { @@ -119,15 +120,17 @@ private void updateProjectConfigWithoutIndexUpdate( Project.NameKey project, Consumer<ProjectConfig> update) throws Exception { - assertThrows( - UnsupportedOperationException.class, - () -> { - try (AutoCloseable ignored = disableProjectIndex()) { - try (ProjectConfigUpdate u = updateProject(project)) { - update.accept(u.getConfig()); - u.save(); - } - } - }); + StorageException storageException = + assertThrows( + StorageException.class, + () -> { + try (AutoCloseable ignored = disableProjectIndex()) { + try (ProjectConfigUpdate u = updateProject(project)) { + update.accept(u.getConfig()); + u.save(); + } + } + }); + assertThat(storageException.getCause()).isInstanceOf(UnsupportedOperationException.class); } }
diff --git a/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java b/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java index cf7aab4..1539334 100644 --- a/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java +++ b/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
@@ -19,8 +19,8 @@ import static com.google.gerrit.testing.GerritJUnit.assertThrows; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.NoHttpd; +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.common.data.Permission;
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java index 24ac209..d4a4c45 100644 --- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java +++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -17,6 +17,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.TruthJUnit.assume; import static com.google.gerrit.entities.Patch.COMMIT_MSG; +import static com.google.gerrit.entities.Patch.MERGE_LIST; import static com.google.gerrit.extensions.common.testing.DiffInfoSubject.assertThat; import static com.google.gerrit.extensions.common.testing.FileInfoSubject.assertThat; import static com.google.gerrit.git.ObjectIds.abbreviateName; @@ -41,6 +42,7 @@ import com.google.gerrit.extensions.common.DiffInfo; import com.google.gerrit.extensions.common.FileInfo; import com.google.gerrit.extensions.restapi.BinaryResult; +import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.testing.ConfigSuite; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; @@ -401,6 +403,24 @@ } @Test + public void diffBetweenPatchSetsOfMergeCommitCanBeRetrievedForCommitMessageAndMergeList() + throws Exception { + PushOneCommit.Result result = createMergeCommitChange("refs/for/master", "my_file.txt"); + String changeId = result.getChangeId(); + String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; + addModifiedPatchSet(changeId, "my_file.txt", content -> content.concat("Line I\nLine II\n")); + + // Call both of them in succession to ensure that they don't share the same cache keys. + DiffInfo commitMessageDiffInfo = + getDiffRequest(changeId, CURRENT, COMMIT_MSG).withBase(previousPatchSetId).get(); + DiffInfo mergeListDiffInfo = + getDiffRequest(changeId, CURRENT, MERGE_LIST).withBase(previousPatchSetId).get(); + + assertThat(commitMessageDiffInfo).content().hasSize(3); + assertThat(mergeListDiffInfo).content().hasSize(1); + } + + @Test public void diffOfUnmodifiedFileMarksAllLinesAsCommon() throws Exception { String filePath = "a_new_file.txt"; String fileContent = "Line 1\nLine 2\nLine 3\n"; @@ -578,6 +598,109 @@ } @Test + public void addedNewlineAtEndOfFileIsMarkedInDiffWhenOtherwiseOnlyEditsDueToRebaseExist() + throws Exception { + addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101")); + String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; + String newBaseFileContent = FILE_CONTENT.replace("Line 70\n", "Line seventy\n"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, newBaseFileContent); + rebaseChangeOn(changeId, commit2); + addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\n")); + + DiffInfo diffInfo = + getDiffRequest(changeId, CURRENT, FILE_NAME) + .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE) + .withBase(previousPatchSetId) + .get(); + assertThat(diffInfo).content().element(0).commonLines().isNotEmpty(); + assertThat(diffInfo).content().element(1).isNotNull(); // Line 70 modification + assertThat(diffInfo).content().element(2).commonLines().isNotEmpty(); + assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line 101"); + assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line 101", ""); + + assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101); + assertThat(diffInfo).metaB().totalLineCount().isEqualTo(102); + } + + @Test + // TODO: Fix this issue. This test documents the current behavior and ensures that we at least + // don't run into an internal server error. + public void addedNewlineAtEndOfFileIsNotIdentifiedAsDueToRebaseEvenThoughItShould() + throws Exception { + String baseFileContent = FILE_CONTENT.concat("Line 101"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, baseFileContent); + rebaseChangeOn(changeId, commit2); + // Add a comment so that file contents are not 'skipped'. To be able to add a comment, touch + // (= modify) the file in the change. + addModifiedPatchSet( + changeId, FILE_NAME, fileContent -> fileContent.replace("Line 2\n", "Line two\n")); + CommentInput comment = createCommentInput(3, 0, 4, 0, "Comment to not skip file content."); + addCommentTo(changeId, CURRENT, FILE_NAME, comment); + String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; + String newBaseFileContent = baseFileContent.concat("\n"); + ObjectId commit3 = addCommit(commit2, FILE_NAME, newBaseFileContent); + rebaseChangeOn(changeId, commit3); + + DiffInfo diffInfo = + getDiffRequest(changeId, CURRENT, FILE_NAME) + .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_ALL) + .withBase(previousPatchSetId) + .get(); + assertThat(diffInfo).content().element(0).commonLines().isNotEmpty(); + assertThat(diffInfo).content().element(1).linesOfA().isNull(); + assertThat(diffInfo).content().element(1).linesOfB().containsExactly(""); + // This should actually be isDueToRebase(). + assertThat(diffInfo).content().element(1).isNotDueToRebase(); + } + + @Test + public void + addedNewlineAtEndOfFileIsMarkedWhenEditDueToRebaseIncreasedLineCountAndWhitespaceConsidered() + throws Exception { + addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101")); + String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; + String newBaseFileContent = FILE_CONTENT.replace("Line 70\n", "Line 70\nLine 70.5\n"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, newBaseFileContent); + rebaseChangeOn(changeId, commit2); + addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\n")); + + DiffInfo diffInfo = + getDiffRequest(changeId, CURRENT, FILE_NAME) + .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE) + .withBase(previousPatchSetId) + .get(); + assertThat(diffInfo).content().element(0).commonLines().isNotEmpty(); + assertThat(diffInfo).content().element(1).isNotNull(); // Line 70.5 insertion + assertThat(diffInfo).content().element(2).commonLines().isNotEmpty(); + assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line 101"); + assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line 101", ""); + + assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101); + assertThat(diffInfo).metaB().totalLineCount().isEqualTo(103); + } + + @Test + // TODO: Fix this issue. This test documents the current behavior and ensures that we at least + // don't run into an internal server error. + public void + addedNewlineAtEndOfFileIsNotMarkedWhenEditDueToRebaseIncreasedLineCountAndWhitespaceIgnoredEvenThoughItShould() + throws Exception { + addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101")); + String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; + String newBaseFileContent = FILE_CONTENT.replace("Line 70\n", "Line 70\nLine 70.5\n"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, newBaseFileContent); + rebaseChangeOn(changeId, commit2); + addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\n")); + + DiffInfo diffInfo = + getDiffRequest(changeId, CURRENT, FILE_NAME) + .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_ALL) + .withBase(previousPatchSetId) + .get(); + assertThat(diffInfo).content().element(0).numberOfSkippedLines().isGreaterThan(0); + } + + @Test public void addedLastLineWithoutNewlineBeforeAndAfterwardsIsMarkedInDiff() throws Exception { addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101")); String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; @@ -2374,9 +2497,7 @@ addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n")); String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; CommentInput comment = createCommentInput(2, 0, 3, 0, "Should be 'Line 2'."); - ReviewInput reviewInput = new ReviewInput(); - reviewInput.comments = ImmutableMap.of(FILE_NAME, ImmutableList.of(comment)); - gApi.changes().id(changeId).revision(previousPatchSetId).review(reviewInput); + addCommentTo(changeId, previousPatchSetId, FILE_NAME, comment); addModifiedPatchSet( changeId, FILE_NAME2, content -> content.replace("2nd line\n", "Second line\n")); @@ -2395,6 +2516,112 @@ } @Test + public void + diffOfFileWithOnlyRebaseHunksAndWithCommentAndConsideringWhitespaceReturnsFileContents() + throws Exception { + addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n")); + String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; + String newBaseFileContent = FILE_CONTENT.replace("Line 70\n", "Line seventy\n"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, newBaseFileContent); + rebaseChangeOn(changeId, commit2); + CommentInput comment = createCommentInput(2, 0, 3, 0, "Should be 'Line 2'."); + addCommentTo(changeId, previousPatchSetId, FILE_NAME, comment); + + DiffInfo diffInfo = + getDiffRequest(changeId, CURRENT, FILE_NAME) + .withBase(previousPatchSetId) + .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE) + .get(); + // We don't list the full file contents here as that is not the focus of this test. + assertThat(diffInfo) + .content() + .element(0) + .commonLines() + .containsAtLeast("Line 1", "Line two", "Line 3", "Line 4", "Line 5") + .inOrder(); + } + + @Test + public void diffOfFileWithOnlyRebaseHunksAndWithCommentAndIgnoringWhitespaceReturnsFileContents() + throws Exception { + addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n")); + String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; + String newBaseFileContent = FILE_CONTENT.replace("Line 70\n", "Line seventy\n"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, newBaseFileContent); + rebaseChangeOn(changeId, commit2); + CommentInput comment = createCommentInput(2, 0, 3, 0, "Should be 'Line 2'."); + addCommentTo(changeId, previousPatchSetId, FILE_NAME, comment); + + DiffInfo diffInfo = + getDiffRequest(changeId, CURRENT, FILE_NAME) + .withBase(previousPatchSetId) + .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_ALL) + .get(); + // We don't list the full file contents here as that is not the focus of this test. + assertThat(diffInfo) + .content() + .element(0) + .commonLines() + .containsAtLeast("Line 1", "Line two", "Line 3", "Line 4", "Line 5") + .inOrder(); + } + + @Test + public void + diffOfFileWithMultilineRebaseHunkAddingNewlineAtEndOfFileAndWithCommentReturnsFileContents() + throws Exception { + String baseFileContent = FILE_CONTENT.concat("Line 101"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, baseFileContent); + rebaseChangeOn(changeId, commit2); + addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n")); + String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; + String newBaseFileContent = baseFileContent.concat("\nLine 102\nLine 103\n"); + ObjectId commit3 = addCommit(commit2, FILE_NAME, newBaseFileContent); + rebaseChangeOn(changeId, commit3); + CommentInput comment = createCommentInput(2, 0, 3, 0, "Should be 'Line 2'."); + addCommentTo(changeId, previousPatchSetId, FILE_NAME, comment); + + DiffInfo diffInfo = + getDiffRequest(changeId, CURRENT, FILE_NAME) + .withBase(previousPatchSetId) + .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE) + .get(); + // We don't list the full file contents here as that is not the focus of this test. + assertThat(diffInfo) + .content() + .element(0) + .commonLines() + .containsAtLeast("Line 1", "Line two", "Line 3", "Line 4", "Line 5") + .inOrder(); + } + + @Test + public void + diffOfFileWithMultilineRebaseHunkRemovingNewlineAtEndOfFileAndWithCommentReturnsFileContents() + throws Exception { + addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n")); + String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; + String newBaseFileContent = FILE_CONTENT.concat("Line 101\nLine 103\nLine 104"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, newBaseFileContent); + rebaseChangeOn(changeId, commit2); + CommentInput comment = createCommentInput(2, 0, 3, 0, "Should be 'Line 2'."); + addCommentTo(changeId, previousPatchSetId, FILE_NAME, comment); + + DiffInfo diffInfo = + getDiffRequest(changeId, CURRENT, FILE_NAME) + .withBase(previousPatchSetId) + .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE) + .get(); + // We don't list the full file contents here as that is not the focus of this test. + assertThat(diffInfo) + .content() + .element(0) + .commonLines() + .containsAtLeast("Line 1", "Line two", "Line 3", "Line 4", "Line 5") + .inOrder(); + } + + @Test public void diffOfNonExistentFileIsAnEmptyDiffResult() throws Exception { addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n")); @@ -2432,9 +2659,7 @@ changeId, FILE_NAME, content -> content.replace("Line 20\n", "Line twenty\n")); String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; CommentInput comment = createCommentInput(20, 0, 21, 0, "Should be 'Line 20'."); - ReviewInput reviewInput = new ReviewInput(); - reviewInput.comments = ImmutableMap.of(FILE_NAME, ImmutableList.of(comment)); - gApi.changes().id(changeId).revision(previousPatchSetId).review(reviewInput); + addCommentTo(changeId, previousPatchSetId, FILE_NAME, comment); addModifiedPatchSet( changeId, FILE_NAME2, content -> content.replace("2nd line\n", "Second line\n")); @@ -2476,9 +2701,7 @@ addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n")); String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; CommentInput comment = createCommentInput(2, 0, 3, 0, "Should be 'Line 2'."); - ReviewInput reviewInput = new ReviewInput(); - reviewInput.comments = ImmutableMap.of(FILE_NAME, ImmutableList.of(comment)); - gApi.changes().id(changeId).revision(previousPatchSetId).review(reviewInput); + addCommentTo(changeId, previousPatchSetId, FILE_NAME, comment); String newFilePath = "a_new_file.txt"; gApi.changes().id(changeId).edit().renameFile(FILE_NAME, newFilePath); gApi.changes().id(changeId).edit().publish(); @@ -2507,6 +2730,14 @@ return comment; } + private void addCommentTo( + String changeId, String previousPatchSetId, String fileName, CommentInput comment) + throws RestApiException { + ReviewInput reviewInput = new ReviewInput(); + reviewInput.comments = ImmutableMap.of(fileName, ImmutableList.of(comment)); + gApi.changes().id(changeId).revision(previousPatchSetId).review(reviewInput); + } + private void assertDiffForNewFile( PushOneCommit.Result pushResult, String path, String expectedContentSideB) throws Exception { DiffInfo diff =
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java index 32941ff..61d0fd5 100644 --- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java +++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -46,6 +46,7 @@ import com.google.gerrit.acceptance.RestResponse; import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.acceptance.TestProjectInput; +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.common.data.Permission; @@ -1062,6 +1063,7 @@ } @Test + @GerritConfig(name = "index.change.indexMergeable", value = "true") public void mergeable() throws Exception { ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId(); @@ -1328,6 +1330,23 @@ } @Test + public void cannotGetContentOfDirectory() throws Exception { + Map<String, String> files = ImmutableMap.of("dir/file1.txt", "content 1"); + PushOneCommit.Result result = + pushFactory.create(admin.newIdent(), testRepo, SUBJECT, files).to("refs/for/master"); + result.assertOkStatus(); + + assertThrows( + BadRequestException.class, + () -> + gApi.changes() + .id(result.getChangeId()) + .revision(result.getCommit().name()) + .file("dir") + .content()); + } + + @Test public void contentType() throws Exception { PushOneCommit.Result r = createChange();
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java index 62a7037..abf0279 100644 --- a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java +++ b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
@@ -16,6 +16,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT; +import static com.google.gerrit.extensions.common.testing.DiffInfoSubject.assertThat; import static com.google.gerrit.extensions.common.testing.EditInfoSubject.assertThat; import static com.google.gerrit.extensions.common.testing.RobotCommentInfoSubject.assertThatList; import static com.google.gerrit.testing.GerritJUnit.assertThrows; @@ -25,12 +26,15 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput; import com.google.gerrit.extensions.client.Comment; import com.google.gerrit.extensions.common.ChangeInfo; +import com.google.gerrit.extensions.common.ChangeType; +import com.google.gerrit.extensions.common.DiffInfo; +import com.google.gerrit.extensions.common.DiffInfo.IntraLineStatus; import com.google.gerrit.extensions.common.EditInfo; import com.google.gerrit.extensions.common.FixReplacementInfo; import com.google.gerrit.extensions.common.FixSuggestionInfo; @@ -49,9 +53,12 @@ import java.util.Objects; import java.util.Optional; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; public class RobotCommentsIT extends AbstractDaemonTest { + private static final String PLAIN_TEXT_CONTENT_TYPE = "text/plain"; + private static final String FILE_NAME = "file_to_fix.txt"; private static final String FILE_NAME2 = "another_file_to_fix.txt"; private static final String FILE_CONTENT = @@ -60,6 +67,7 @@ private static final String FILE_CONTENT2 = "1st line\n2nd line\n3rd line\n"; private String changeId; + private String commitId; private FixReplacementInfo fixReplacementInfo; private FixSuggestionInfo fixSuggestionInfo; private RobotCommentInput withFixRobotCommentInput; @@ -74,6 +82,7 @@ ImmutableMap.of(FILE_NAME, FILE_CONTENT, FILE_NAME2, FILE_CONTENT2)); PushOneCommit.Result changeResult = push.to("refs/for/master"); changeId = changeResult.getChangeId(); + commitId = changeResult.getCommit().getName(); fixReplacementInfo = createFixReplacementInfo(); fixSuggestionInfo = createFixSuggestionInfo(fixReplacementInfo); @@ -960,6 +969,142 @@ } } + @Test + public void getFixPreviewWithNonexistingFixId() throws Exception { + addRobotComment(changeId, withFixRobotCommentInput); + + assertThrows( + ResourceNotFoundException.class, + () -> gApi.changes().id(changeId).current().getFixPreview("Non existing fixId")); + } + + @Test + @Ignore + public void getFixPreviewForNonExistingFile() throws Exception { + // Not implemented yet. + fixReplacementInfo.path = "a_non_existent_file.txt"; + fixReplacementInfo.range = createRange(1, 0, 2, 0); + fixReplacementInfo.replacement = "Modified content\n"; + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + List<String> fixIds = getFixIds(robotCommentInfos); + String fixId = Iterables.getOnlyElement(fixIds); + + assertThrows( + BadRequestException.class, + () -> gApi.changes().id(changeId).current().getFixPreview(fixId)); + } + + @Test + public void getFixPreview() throws Exception { + FixReplacementInfo fixReplacementInfoFile1 = new FixReplacementInfo(); + fixReplacementInfoFile1.path = FILE_NAME; + fixReplacementInfoFile1.replacement = "some replacement code"; + fixReplacementInfoFile1.range = createRange(3, 9, 8, 4); + + FixReplacementInfo fixReplacementInfoFile2 = new FixReplacementInfo(); + fixReplacementInfoFile2.path = FILE_NAME2; + fixReplacementInfoFile2.replacement = "New line\n"; + fixReplacementInfoFile2.range = createRange(2, 0, 2, 0); + + fixSuggestionInfo = createFixSuggestionInfo(fixReplacementInfoFile1, fixReplacementInfoFile2); + + withFixRobotCommentInput = createRobotCommentInput(fixSuggestionInfo); + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + + List<String> fixIds = getFixIds(robotCommentInfos); + String fixId = Iterables.getOnlyElement(fixIds); + + Map<String, DiffInfo> fixPreview = gApi.changes().id(changeId).current().getFixPreview(fixId); + assertThat(fixPreview).hasSize(2); + assertThat(fixPreview).containsKey(FILE_NAME); + assertThat(fixPreview).containsKey(FILE_NAME2); + + DiffInfo diff = fixPreview.get(FILE_NAME); + assertThat(diff).intralineStatus().isEqualTo(IntraLineStatus.OK); + assertThat(diff).webLinks().isNull(); + assertThat(diff).binary().isNull(); + assertThat(diff).diffHeader().isNull(); + assertThat(diff).changeType().isEqualTo(ChangeType.MODIFIED); + assertThat(diff).metaA().totalLineCount().isEqualTo(11); + assertThat(diff).metaA().name().isEqualTo(FILE_NAME); + assertThat(diff).metaA().commitId().isEqualTo(commitId); + assertThat(diff).metaA().contentType().isEqualTo(PLAIN_TEXT_CONTENT_TYPE); + assertThat(diff).metaA().webLinks().isNull(); + assertThat(diff).metaB().totalLineCount().isEqualTo(6); + assertThat(diff).metaB().name().isEqualTo(FILE_NAME); + assertThat(diff).metaB().commitId().isNull(); + assertThat(diff).metaB().contentType().isEqualTo(PLAIN_TEXT_CONTENT_TYPE); + assertThat(diff).metaB().webLinks().isNull(); + + assertThat(diff).content().hasSize(3); + assertThat(diff) + .content() + .element(0) + .commonLines() + .containsExactly("First line", "Second line"); + assertThat(diff).content().element(0).linesOfA().isNull(); + assertThat(diff).content().element(0).linesOfB().isNull(); + + assertThat(diff).content().element(1).commonLines().isNull(); + assertThat(diff) + .content() + .element(1) + .linesOfA() + .containsExactly( + "Third line", "Fourth line", "Fifth line", "Sixth line", "Seventh line", "Eighth line"); + assertThat(diff) + .content() + .element(1) + .linesOfB() + .containsExactly("Third linsome replacement codeth line"); + + assertThat(diff) + .content() + .element(2) + .commonLines() + .containsExactly("Ninth line", "Tenth line", ""); + assertThat(diff).content().element(2).linesOfA().isNull(); + assertThat(diff).content().element(2).linesOfB().isNull(); + + DiffInfo diff2 = fixPreview.get(FILE_NAME2); + assertThat(diff2).intralineStatus().isEqualTo(IntraLineStatus.OK); + assertThat(diff2).webLinks().isNull(); + assertThat(diff2).binary().isNull(); + assertThat(diff2).diffHeader().isNull(); + assertThat(diff2).changeType().isEqualTo(ChangeType.MODIFIED); + assertThat(diff2).metaA().totalLineCount().isEqualTo(4); + assertThat(diff2).metaA().name().isEqualTo(FILE_NAME2); + assertThat(diff2).metaA().commitId().isEqualTo(commitId); + assertThat(diff2).metaA().contentType().isEqualTo(PLAIN_TEXT_CONTENT_TYPE); + assertThat(diff2).metaA().webLinks().isNull(); + assertThat(diff2).metaB().totalLineCount().isEqualTo(5); + assertThat(diff2).metaB().name().isEqualTo(FILE_NAME2); + assertThat(diff2).metaB().commitId().isNull(); + assertThat(diff2).metaA().contentType().isEqualTo(PLAIN_TEXT_CONTENT_TYPE); + assertThat(diff2).metaB().webLinks().isNull(); + + assertThat(diff2).content().hasSize(3); + assertThat(diff2).content().element(0).commonLines().containsExactly("1st line"); + assertThat(diff2).content().element(0).linesOfA().isNull(); + assertThat(diff2).content().element(0).linesOfB().isNull(); + + assertThat(diff2).content().element(1).commonLines().isNull(); + assertThat(diff2).content().element(1).linesOfA().isNull(); + assertThat(diff2).content().element(1).linesOfB().containsExactly("New line"); + + assertThat(diff2) + .content() + .element(2) + .commonLines() + .containsExactly("2nd line", "3rd line", ""); + assertThat(diff2).content().element(2).linesOfA().isNull(); + assertThat(diff2).content().element(2).linesOfB().isNull(); + } + private static RobotCommentInput createRobotCommentInputWithMandatoryFields() { RobotCommentInput in = new RobotCommentInput(); in.robotId = "happyRobot";
diff --git a/javatests/com/google/gerrit/acceptance/config/BUILD b/javatests/com/google/gerrit/acceptance/config/BUILD new file mode 100644 index 0000000..350f2e6 --- /dev/null +++ b/javatests/com/google/gerrit/acceptance/config/BUILD
@@ -0,0 +1,7 @@ +load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests") + +acceptance_tests( + srcs = glob(["*.java"]), + group = "config", + labels = ["config"], +)
diff --git a/javatests/com/google/gerrit/acceptance/annotation/UseGerritConfigAnnotationTest.java b/javatests/com/google/gerrit/acceptance/config/UseGerritConfigAnnotationTest.java similarity index 96% rename from javatests/com/google/gerrit/acceptance/annotation/UseGerritConfigAnnotationTest.java rename to javatests/com/google/gerrit/acceptance/config/UseGerritConfigAnnotationTest.java index d5ac2f7..f72aa74 100644 --- a/javatests/com/google/gerrit/acceptance/annotation/UseGerritConfigAnnotationTest.java +++ b/javatests/com/google/gerrit/acceptance/config/UseGerritConfigAnnotationTest.java
@@ -12,12 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.acceptance.annotation; +package com.google.gerrit.acceptance.config; import static com.google.common.truth.Truth.assertThat; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; import org.junit.Test; public class UseGerritConfigAnnotationTest extends AbstractDaemonTest {
diff --git a/javatests/com/google/gerrit/acceptance/annotation/UseGlobalPluginConfigAnnotationTest.java b/javatests/com/google/gerrit/acceptance/config/UseGlobalPluginConfigAnnotationTest.java similarity index 96% rename from javatests/com/google/gerrit/acceptance/annotation/UseGlobalPluginConfigAnnotationTest.java rename to javatests/com/google/gerrit/acceptance/config/UseGlobalPluginConfigAnnotationTest.java index 44d9e46..cfad6f2 100644 --- a/javatests/com/google/gerrit/acceptance/annotation/UseGlobalPluginConfigAnnotationTest.java +++ b/javatests/com/google/gerrit/acceptance/config/UseGlobalPluginConfigAnnotationTest.java
@@ -12,12 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.acceptance.annotation; +package com.google.gerrit.acceptance.config; import static com.google.common.truth.Truth.assertThat; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GlobalPluginConfig; import com.google.gerrit.acceptance.UseLocalDisk; import org.eclipse.jgit.lib.Config; import org.junit.Test;
diff --git a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java index cc8d233..bf93d58 100644 --- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java +++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -45,6 +45,7 @@ import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.Project; import com.google.gerrit.extensions.api.changes.AddReviewerInput; +import com.google.gerrit.extensions.api.changes.FileContentInput; import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.api.changes.PublishChangeEditInput; import com.google.gerrit.extensions.api.changes.ReviewInput; @@ -64,7 +65,6 @@ import com.google.gerrit.server.project.testing.TestLabels; import com.google.gerrit.server.restapi.change.ChangeEdits.EditMessage; import com.google.gerrit.server.restapi.change.ChangeEdits.Post; -import com.google.gerrit.server.restapi.change.ChangeEdits.Put; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.inject.Inject; @@ -501,7 +501,7 @@ @Test public void createAndChangeEditInOneRequestRest() throws Exception { - Put.Input in = new Put.Input(); + FileContentInput in = new FileContentInput(); in.content = RawInputUtil.create(CONTENT_NEW); adminRestSession.putRaw(urlEditFile(changeId, FILE_NAME), in.content).assertNoContent(); ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_NEW); @@ -513,7 +513,7 @@ @Test public void changeEditRest() throws Exception { createEmptyEditFor(changeId); - Put.Input in = new Put.Input(); + FileContentInput in = new FileContentInput(); in.content = RawInputUtil.create(CONTENT_NEW); adminRestSession.putRaw(urlEditFile(changeId, FILE_NAME), in.content).assertNoContent(); ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_NEW); @@ -534,7 +534,7 @@ @Test public void getFileContentRest() throws Exception { - Put.Input in = new Put.Input(); + FileContentInput in = new FileContentInput(); in.content = RawInputUtil.create(CONTENT_NEW); adminRestSession.putRaw(urlEditFile(changeId, FILE_NAME), in.content).assertNoContent(); gApi.changes().id(changeId).edit().modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW2));
diff --git a/javatests/com/google/gerrit/acceptance/filter/BUILD b/javatests/com/google/gerrit/acceptance/filter/BUILD new file mode 100644 index 0000000..22aead3 --- /dev/null +++ b/javatests/com/google/gerrit/acceptance/filter/BUILD
@@ -0,0 +1,22 @@ +load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests") + +acceptance_tests( + srcs = glob([ + "*IT.java", + ]), + group = "filter", + labels = ["filter"], + deps = [ + ":util", + ], +) + +java_library( + name = "util", + testonly = True, + srcs = [ + "FakeMustInitParamsFilter.java", + "FakeNoInitParamsFilter.java", + ], + deps = ["//java/com/google/gerrit/acceptance:lib"], +)
diff --git a/javatests/com/google/gerrit/acceptance/filter/FakeMustInitParamsFilter.java b/javatests/com/google/gerrit/acceptance/filter/FakeMustInitParamsFilter.java new file mode 100644 index 0000000..89d268e --- /dev/null +++ b/javatests/com/google/gerrit/acceptance/filter/FakeMustInitParamsFilter.java
@@ -0,0 +1,56 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.filter; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +public class FakeMustInitParamsFilter implements Filter { + + // `PARAM_X` and `PARAM_Y` are init param keys + private static final String INIT_PARAM_1 = "PARAM-1"; + private static final String INIT_PARAM_2 = "PARAM-2"; + // the map is used for testing + private static final Map<String, String> initParams = new HashMap<>(); + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + initParams.put(INIT_PARAM_1, filterConfig.getInitParameter(INIT_PARAM_1)); + initParams.put(INIT_PARAM_2, filterConfig.getInitParameter(INIT_PARAM_2)); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + chain.doFilter(request, response); + } + + @Override + public void destroy() { + // do nothing. + } + + // the function is used for testing + Map<String, String> getInitParams() { + return initParams; + } +}
diff --git a/javatests/com/google/gerrit/acceptance/filter/FakeNoInitParamsFilter.java b/javatests/com/google/gerrit/acceptance/filter/FakeNoInitParamsFilter.java new file mode 100644 index 0000000..6fd6366 --- /dev/null +++ b/javatests/com/google/gerrit/acceptance/filter/FakeNoInitParamsFilter.java
@@ -0,0 +1,42 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.filter; + +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +public class FakeNoInitParamsFilter implements Filter { + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // no init params in this filter. + // do nothing. + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + chain.doFilter(request, response); + } + + @Override + public void destroy() { + // do nothing. + } +}
diff --git a/javatests/com/google/gerrit/acceptance/filter/FilterClassIT.java b/javatests/com/google/gerrit/acceptance/filter/FilterClassIT.java new file mode 100644 index 0000000..a23c5ce --- /dev/null +++ b/javatests/com/google/gerrit/acceptance/filter/FilterClassIT.java
@@ -0,0 +1,57 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.filter; + +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.testing.ConfigSuite; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.Config; +import org.junit.Assert; +import org.junit.Test; + +public class FilterClassIT extends AbstractDaemonTest { + @ConfigSuite.Default + public static Config enableFilter() throws ConfigInvalidException { + Config cfg = new Config(); + cfg.fromText( + "" + + "[httpd]\n" + + " filterClass = com.google.gerrit.acceptance.filter.FakeNoInitParamsFilter\n" + + " filterClass = com.google.gerrit.acceptance.filter.FakeMustInitParamsFilter\n" + + "[filterClass \"com.google.gerrit.acceptance.filter.FakeMustInitParamsFilter\"]\n" + + " PARAM-1 = hello\n" + + " PARAM-2 = world\n"); + return cfg; + } + + @Test + public void filterLoad() { + FakeNoInitParamsFilter fakeNoInitParamsFilter = + server.getTestInjector().getBinding(FakeNoInitParamsFilter.class).getProvider().get(); + Assert.assertNotNull(fakeNoInitParamsFilter); + FakeMustInitParamsFilter fakeMustInitParamsFilter = + server.getTestInjector().getBinding(FakeMustInitParamsFilter.class).getProvider().get(); + Assert.assertNotNull(fakeMustInitParamsFilter); + } + + @Test + public void filterInitParams() { + FakeMustInitParamsFilter fakeMustInitParamsFilter = + server.getTestInjector().getBinding(FakeMustInitParamsFilter.class).getProvider().get(); + Assert.assertEquals(2, fakeMustInitParamsFilter.getInitParams().size()); + Assert.assertEquals("hello", fakeMustInitParamsFilter.getInitParams().get("PARAM-1")); + Assert.assertEquals("world", fakeMustInitParamsFilter.getInitParams().get("PARAM-2")); + } +}
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java index cab12b3..4258009 100644 --- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java +++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -52,13 +52,13 @@ import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.ExtensionRegistry; import com.google.gerrit.acceptance.ExtensionRegistry.Registration; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.GitUtil; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.SkipProjectClone; import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.acceptance.TestProjectInput; import com.google.gerrit.acceptance.UseClockStep; +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.common.data.GlobalCapability; @@ -485,15 +485,9 @@ @Test public void pushForMasterWithTopic() throws Exception { - // specify topic in ref String topic = "my/topic"; - PushOneCommit.Result r = pushTo("refs/for/master/" + topic); - r.assertOkStatus(); - r.assertChange(Change.Status.NEW, topic); - r.assertMessage("deprecated topic syntax"); - // specify topic as option - r = pushTo("refs/for/master%topic=" + topic); + PushOneCommit.Result r = pushTo("refs/for/master%topic=" + topic); r.assertOkStatus(); r.assertChange(Change.Status.NEW, topic); } @@ -514,14 +508,7 @@ } @Test - public void pushForMasterWithTopicInRefExceedLimitFails() throws Exception { - String topic = Stream.generate(() -> "t").limit(2049).collect(joining()); - PushOneCommit.Result r = pushTo("refs/for/master/" + topic); - r.assertErrorStatus("topic length exceeds the limit (2048)"); - } - - @Test - public void pushForMasterWithTopicAsOptionExceedLimitFails() throws Exception { + public void pushForMasterWithTopicExceedLimitFails() throws Exception { String topic = Stream.generate(() -> "t").limit(2049).collect(joining()); PushOneCommit.Result r = pushTo("refs/for/master%topic=" + topic); r.assertErrorStatus("topic length exceeds the limit (2048)"); @@ -605,16 +592,16 @@ public void pushForMasterWithCc() throws Exception { // cc one user String topic = "my/topic"; - PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%cc=" + user.email()); + PushOneCommit.Result r = pushTo("refs/for/master%topic=" + topic + ",cc=" + user.email()); r.assertOkStatus(); r.assertChange(Change.Status.NEW, topic, ImmutableList.of(), ImmutableList.of(user)); // cc several users r = pushTo( - "refs/for/master/" + "refs/for/master%topic=" + topic - + "%cc=" + + ",cc=" + admin.email() + ",cc=" + user.email() @@ -632,9 +619,9 @@ String nonExistingEmail = "non.existing@example.com"; r = pushTo( - "refs/for/master/" + "refs/for/master%topic=" + topic - + "%cc=" + + ",cc=" + admin.email() + ",cc=" + nonExistingEmail @@ -684,7 +671,7 @@ public void pushForMasterWithReviewer() throws Exception { // add one reviewer String topic = "my/topic"; - PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%r=" + user.email()); + PushOneCommit.Result r = pushTo("refs/for/master%topic=" + topic + ",r=" + user.email()); r.assertOkStatus(); r.assertChange(Change.Status.NEW, topic, user); @@ -693,9 +680,9 @@ accountCreator.create("another-user", "another.user@example.com", "Another User"); r = pushTo( - "refs/for/master/" + "refs/for/master%topic=" + topic - + "%r=" + + ",r=" + admin.email() + ",r=" + user.email() @@ -709,9 +696,9 @@ String nonExistingEmail = "non.existing@example.com"; r = pushTo( - "refs/for/master/" + "refs/for/master%topic=" + topic - + "%r=" + + ",r=" + admin.email() + ",r=" + nonExistingEmail @@ -942,7 +929,7 @@ @Test public void pushForMasterWithMessage() throws Exception { - PushOneCommit.Result r = pushTo("refs/for/master/%m=my_test_message"); + PushOneCommit.Result r = pushTo("refs/for/master%m=my_test_message"); r.assertOkStatus(); r.assertChange(Change.Status.NEW, null); ChangeInfo ci = get(r.getChangeId(), MESSAGES, ALL_REVISIONS); @@ -966,7 +953,7 @@ pushFactory.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content"); // %2C is comma; the value below tests that percent decoding happens after splitting. // All three ways of representing space ("%20", "+", and "_" are also exercised. - PushOneCommit.Result r = push.to("refs/for/master/%m=my_test%20+_message%2Cm="); + PushOneCommit.Result r = push.to("refs/for/master%m=my_test%20+_message%2Cm="); r.assertOkStatus(); push = @@ -977,7 +964,7 @@ "b.txt", "anotherContent", r.getChangeId()); - r = push.to("refs/for/master/%m=new_test_message"); + r = push.to("refs/for/master%m=new_test_message"); r.assertOkStatus(); ChangeInfo ci = get(r.getChangeId(), ALL_REVISIONS); @@ -997,7 +984,7 @@ // Exercise percent-encoding of UTF-8, underscores, and patterns reserved by git-rev-parse. PushOneCommit.Result r = pushTo( - "refs/for/master/%m=" + "refs/for/master%m=" + "Punctu%2E%2e%2Eation%7E%2D%40%7Bu%7D%20%7C%20%28%E2%95%AF%C2%B0%E2%96%A1%C2%B0" + "%EF%BC%89%E2%95%AF%EF%B8%B5%20%E2%94%BB%E2%94%81%E2%94%BB%20%5E%5F%5E"); r.assertOkStatus(); @@ -1018,7 +1005,7 @@ @Test public void pushForMasterWithInvalidPercentEncodedMessage() throws Exception { - PushOneCommit.Result r = pushTo("refs/for/master/%m=not_percent_decodable_%%oops%20"); + PushOneCommit.Result r = pushTo("refs/for/master%m=not_percent_decodable_%%oops%20"); r.assertOkStatus(); r.assertChange(Change.Status.NEW, null); ChangeInfo ci = get(r.getChangeId(), MESSAGES, ALL_REVISIONS); @@ -1036,7 +1023,7 @@ @Test public void pushForMasterWithApprovals() throws Exception { - PushOneCommit.Result r = pushTo("refs/for/master/%l=Code-Review"); + PushOneCommit.Result r = pushTo("refs/for/master%l=Code-Review"); r.assertOkStatus(); ChangeInfo ci = get(r.getChangeId(), DETAILED_LABELS, MESSAGES, DETAILED_ACCOUNTS); LabelInfo cr = ci.labels.get("Code-Review"); @@ -1054,7 +1041,7 @@ "b.txt", "anotherContent", r.getChangeId()); - r = push.to("refs/for/master/%l=Code-Review+2"); + r = push.to("refs/for/master%l=Code-Review+2"); ci = get(r.getChangeId(), DETAILED_LABELS, MESSAGES, DETAILED_ACCOUNTS); cr = ci.labels.get("Code-Review"); @@ -1075,7 +1062,7 @@ "c.txt", "moreContent", r.getChangeId()); - r = push.to("refs/for/master/%l=Code-Review+2"); + r = push.to("refs/for/master%l=Code-Review+2"); ci = get(r.getChangeId(), MESSAGES); assertThat(Iterables.getLast(ci.messages).message).isEqualTo("Uploaded patch set 3."); } @@ -1093,7 +1080,7 @@ "b.txt", "anotherContent", r.getChangeId()); - r = push.to("refs/for/master/%l=Code-Review+2"); + r = push.to("refs/for/master%l=Code-Review+2"); ChangeInfo ci = get(r.getChangeId(), DETAILED_LABELS, MESSAGES, DETAILED_ACCOUNTS); LabelInfo cr = ci.labels.get("Code-Review"); @@ -1180,7 +1167,7 @@ .create(); // Push this commit as "Administrator" (requires Forge Committer Identity) - pushHead(testRepo, "refs/for/master/%l=Code-Review+1", false); + pushHead(testRepo, "refs/for/master%l=Code-Review+1", false); // Expected Code-Review votes: // 1. 0 from User (committer): @@ -1228,7 +1215,7 @@ .message(PushOneCommit.SUBJECT) .create(); - pushHead(testRepo, "refs/for/master/%l=Code-Review+1,l=Custom-Label-1", false); + pushHead(testRepo, "refs/for/master%l=Code-Review+1,l=Custom-Label-1", false); ChangeInfo ci = get(GitUtil.getChangeId(testRepo, c).get(), DETAILED_LABELS, DETAILED_ACCOUNTS); LabelInfo cr = ci.labels.get("Code-Review"); @@ -1258,13 +1245,13 @@ @Test public void pushForMasterWithApprovals_MissingLabel() throws Exception { - PushOneCommit.Result r = pushTo("refs/for/master/%l=Verify"); + PushOneCommit.Result r = pushTo("refs/for/master%l=Verify"); r.assertErrorStatus("label \"Verify\" is not a configured label"); } @Test public void pushForMasterWithApprovals_ValueOutOfRange() throws Exception { - PushOneCommit.Result r = pushTo("refs/for/master/%l=Code-Review-3"); + PushOneCommit.Result r = pushTo("refs/for/master%l=Code-Review-3"); r.assertErrorStatus("label \"Code-Review\": -3 is not a valid value"); } @@ -1297,7 +1284,7 @@ "b.txt", "anotherContent", r.getChangeId()); - r = push.to("refs/for/master/%hashtag=" + hashtag2); + r = push.to("refs/for/master%hashtag=" + hashtag2); r.assertOkStatus(); expected = ImmutableSet.of(hashtag1, hashtag2); hashtags = gApi.changes().id(r.getChangeId()).getHashtags();
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java index 01323a0..a0725c3 100644 --- a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java +++ b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
@@ -139,7 +139,7 @@ String pushedRef = ref; if (!topic.isEmpty()) { - pushedRef += "/" + name(topic); + pushedRef += "%topic=" + name(topic); } String refspec = "HEAD:" + pushedRef;
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java index 0ac40a0..9e44753 100644 --- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java +++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -14,6 +14,7 @@ package com.google.gerrit.acceptance.git; +import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static com.google.common.truth.TruthJUnit.assume; @@ -24,15 +25,13 @@ import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey; import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toMap; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; 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.common.Nullable; @@ -59,12 +58,10 @@ import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.testing.ConfigSuite; import com.google.inject.Inject; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; -import java.util.Map; -import java.util.function.Function; import java.util.function.Predicate; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.junit.TestRepository; @@ -1365,15 +1362,18 @@ expectedAllRefs.addAll(expectedMetaRefs); try (Repository repo = repoManager.openRepository(allUsers)) { - Map<String, Ref> all = getAllRefs(repo); - PermissionBackend.ForProject forProject = newFilter(allUsers, admin); - assertThat(forProject.filter(all, repo, RefFilterOptions.defaults()).keySet()) + assertThat( + names( + forProject.filter( + repo.getRefDatabase().getRefs(), repo, RefFilterOptions.defaults()))) .containsExactlyElementsIn(expectedAllRefs); assertThat( - forProject - .filter(all, repo, RefFilterOptions.builder().setFilterMeta(true).build()) - .keySet()) + names( + forProject.filter( + repo.getRefDatabase().getRefs(), + repo, + RefFilterOptions.builder().setFilterMeta(true).build()))) .containsExactlyElementsIn(expectedNonMetaRefs); } } @@ -1384,8 +1384,8 @@ String patchSetRef = change.getPatchSetId().toRefName(); try (AutoCloseable ignored = disableChangeIndex(); Repository repo = repoManager.openRepository(project)) { - Map<String, Ref> singleRef = ImmutableMap.of(patchSetRef, repo.exactRef(patchSetRef)); - Map<String, Ref> filteredRefs = + Collection<Ref> singleRef = ImmutableList.of(repo.exactRef(patchSetRef)); + Collection<Ref> filteredRefs = permissionBackend .user(user(admin)) .project(project) @@ -1446,7 +1446,7 @@ private TestRefAdvertiser.Result getReceivePackRefs() throws Exception { try (Repository repo = repoManager.openRepository(project)) { AdvertiseRefsHook adv = - ReceiveCommitsAdvertiseRefsHookChain.createForTest(queryProvider, project); + ReceiveCommitsAdvertiseRefsHookChain.createForTest(queryProvider, project, admin.id()); ReceivePack rp = new ReceivePack(repo); rp.setAdvertiseRefsHook(adv); TestRefAdvertiser advertiser = new TestRefAdvertiser(repo); @@ -1482,8 +1482,7 @@ return AccountGroup.uuid(gApi.groups().create(groupInput).get().id); } - private static Map<String, Ref> getAllRefs(Repository repo) throws IOException { - return repo.getRefDatabase().getRefs().stream() - .collect(toMap(Ref::getName, Function.identity())); + private static Collection<String> names(Collection<Ref> refs) { + return refs.stream().map(Ref::getName).collect(toImmutableList()); } }
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java index 09da628..0efc4f9 100644 --- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java +++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
@@ -18,10 +18,10 @@ import static com.google.common.truth.TruthJUnit.assume; import com.google.common.collect.Iterables; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.UseClockStep; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; import com.google.gerrit.entities.Project; import com.google.gerrit.testing.ConfigSuite; @@ -565,7 +565,7 @@ // Create change as user. PushOneCommit push = pushFactory.create(user.newIdent(), repo2, "Change 2", "b.txt", "other content"); - PushOneCommit.Result pushResult2 = push.to("refs/for/master/" + name(topic)); + PushOneCommit.Result pushResult2 = push.to("refs/for/master%topic=" + name(topic)); approve(pushResult2.getChangeId()); // Submit the topic, 2 changes with the different author.
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java index 283c95f..0715b7e 100644 --- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java +++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -109,7 +109,7 @@ .git() .push() .setRemote("origin") - .setRefSpecs(new RefSpec("HEAD:refs/for/master/" + name("topic-foo"))) + .setRefSpecs(new RefSpec("HEAD:refs/for/master%topic=" + name("topic-foo"))) .call(); subRepo.reset(c.getId()); @@ -134,7 +134,7 @@ .git() .push() .setRemote("origin") - .setRefSpecs(new RefSpec("HEAD:refs/for/master/" + name("topic-foo"))) + .setRefSpecs(new RefSpec("HEAD:refs/for/master%topic=" + name("topic-foo"))) .call(); String id1 = getChangeId(subRepo, c1).get(); @@ -212,7 +212,7 @@ .git() .push() .setRemote("origin") - .setRefSpecs(new RefSpec("HEAD:refs/for/master/" + name("topic-foo"))) + .setRefSpecs(new RefSpec("HEAD:refs/for/master%topic=" + name("topic-foo"))) .call(); subRepo.reset(c.getId()); @@ -237,7 +237,7 @@ .git() .push() .setRemote("origin") - .setRefSpecs(new RefSpec("HEAD:refs/for/master/" + name("topic-foo"))) + .setRefSpecs(new RefSpec("HEAD:refs/for/master%topic=" + name("topic-foo"))) .call(); RevCommit c4 = @@ -252,7 +252,7 @@ .git() .push() .setRemote("origin") - .setRefSpecs(new RefSpec("HEAD:refs/for/master/" + name("topic-foo"))) + .setRefSpecs(new RefSpec("HEAD:refs/for/master%topic=" + name("topic-foo"))) .call(); String id1 = getChangeId(subRepo, c1).get();
diff --git a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java index 52de5ad..09680fb 100644 --- a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java +++ b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
@@ -28,9 +28,9 @@ import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.ExtensionRegistry; import com.google.gerrit.acceptance.ExtensionRegistry.Registration; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.RestResponse; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.common.data.SubmitRecord; import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.events.ChangeIndexedListener; @@ -743,7 +743,7 @@ .add( new ExceptionHook() { @Override - public boolean shouldRetry(Throwable t) { + public boolean shouldRetry(String actionType, String actionName, Throwable t) { return true; } })) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java index faaba06..a3c0295 100644 --- a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java +++ b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -29,11 +29,11 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; 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.config.GerritConfig; import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; import com.google.gerrit.common.data.GlobalCapability;
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java index 8e5eaa4..eb125a0 100644 --- a/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java +++ b/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java
@@ -21,8 +21,8 @@ import com.google.common.collect.ImmutableList; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.UseSsh; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.acceptance.rest.util.RestCall; import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; import com.google.gerrit.extensions.common.ChangeInput;
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java index 8a284d9..83bc3eb 100644 --- a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java +++ b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
@@ -84,6 +84,7 @@ RestCall.post("/changes/%s/rebase"), RestCall.post("/changes/%s/restore"), RestCall.post("/changes/%s/revert"), + RestCall.post("/changes/%s/revert_submission"), RestCall.get("/changes/%s/pure_revert"), RestCall.post("/changes/%s/submit"), RestCall.get("/changes/%s/submitted_together"),
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/PluginsRemoteAdminRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/PluginsRemoteAdminRestApiBindingsIT.java index d60148e..dd6eb7a 100644 --- a/javatests/com/google/gerrit/acceptance/rest/binding/PluginsRemoteAdminRestApiBindingsIT.java +++ b/javatests/com/google/gerrit/acceptance/rest/binding/PluginsRemoteAdminRestApiBindingsIT.java
@@ -18,7 +18,7 @@ import com.google.common.collect.ImmutableList; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.acceptance.rest.util.RestApiCallHelper; import com.google.gerrit.acceptance.rest.util.RestCall; import com.google.gerrit.common.RawInputUtil;
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java index f48a603..55eeaf4 100644 --- a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java +++ b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
@@ -29,6 +29,7 @@ import com.google.gerrit.acceptance.rest.util.RestApiCallHelper; import com.google.gerrit.acceptance.rest.util.RestCall; import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; +import com.google.gerrit.common.data.LabelFunction; import com.google.gerrit.common.data.Permission; import com.google.gerrit.entities.Project; import com.google.gerrit.extensions.api.changes.ReviewInput; @@ -73,6 +74,7 @@ RestCall.get("/projects/%s/branches"), RestCall.post("/projects/%s/branches:delete"), RestCall.put("/projects/%s/branches/new-branch"), + RestCall.get("/projects/%s/labels"), RestCall.get("/projects/%s/tags"), RestCall.post("/projects/%s/tags:delete"), RestCall.put("/projects/%s/tags/new-tag"), @@ -80,7 +82,9 @@ // GET /projects/<project>/branches/<branch>/commits is not implemented .expectedResponseCode(SC_NOT_FOUND) .build(), - RestCall.get("/projects/%s/dashboards")); + RestCall.get("/projects/%s/dashboards"), + RestCall.put("/projects/%s/labels/new-label"), + RestCall.post("/projects/%s/labels/")); /** * Child project REST endpoints to be tested, each URL contains placeholders for the parent @@ -158,6 +162,18 @@ private static final ImmutableList<RestCall> COMMIT_FILE_ENDPOINTS = ImmutableList.of(RestCall.get("/projects/%s/commits/%s/files/%s/content")); + /** + * Label REST endpoints to be tested, each URL contains placeholders for the project identifier + * and the label name. + */ + private static final ImmutableList<RestCall> LABEL_ENDPOINTS = + ImmutableList.of( + RestCall.get("/projects/%s/labels/%s"), + RestCall.put("/projects/%s/labels/%s"), + + // Label deletion must be tested last + RestCall.delete("/projects/%s/labels/%s")); + private static final String FILENAME = "test.txt"; @Inject private ProjectOperations projectOperations; @@ -212,6 +228,13 @@ adminRestSession, COMMIT_FILE_ENDPOINTS, project.get(), commit, FILENAME); } + @Test + public void labelEndpoints() throws Exception { + String label = "Foo-Review"; + configLabel(label, LabelFunction.NO_OP); + RestApiCallHelper.execute(adminRestSession, LABEL_ENDPOINTS, project.get(), label); + } + private String createAndSubmitChange(String filename) throws Exception { RevCommit c = testRepo
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/RootCollectionsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/RootCollectionsRestApiBindingsIT.java index 6d140c6..5573ad7 100644 --- a/javatests/com/google/gerrit/acceptance/rest/binding/RootCollectionsRestApiBindingsIT.java +++ b/javatests/com/google/gerrit/acceptance/rest/binding/RootCollectionsRestApiBindingsIT.java
@@ -20,7 +20,7 @@ import com.google.common.collect.ImmutableList; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.acceptance.rest.util.RestCall; import org.junit.Test;
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java index 12fe263..0c0a8ed 100644 --- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java +++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -44,7 +44,6 @@ import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.ExtensionRegistry; import com.google.gerrit.acceptance.ExtensionRegistry.Registration; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.GitUtil; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; @@ -52,6 +51,7 @@ import com.google.gerrit.acceptance.TestProjectInput; import com.google.gerrit.acceptance.UseClockStep; import com.google.gerrit.acceptance.UseTimezone; +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.common.Nullable; @@ -614,11 +614,11 @@ @Test public void submitWithHiddenBranchInSameTopic() throws Throwable { assume().that(isSubmitWholeTopicEnabled()).isTrue(); - PushOneCommit.Result visible = createChange("refs/for/master/" + name("topic")); + PushOneCommit.Result visible = createChange("refs/for/master%topic=" + name("topic")); Change.Id num = visible.getChange().getId(); createBranch(BranchNameKey.create(project, "hidden")); - PushOneCommit.Result hidden = createChange("refs/for/hidden/" + name("topic")); + PushOneCommit.Result hidden = createChange("refs/for/hidden%topic=" + name("topic")); approve(hidden.getChangeId()); projectOperations .project(project) @@ -789,8 +789,8 @@ // create and submit 2 changes with the same topic String topic = name("topic"); - PushOneCommit.Result change1 = createChange("refs/for/master/" + topic); - PushOneCommit.Result change2 = createChange("refs/for/master/" + topic); + PushOneCommit.Result change1 = createChange("refs/for/master%topic=" + topic); + PushOneCommit.Result change2 = createChange("refs/for/master%topic=" + topic); approve(change1.getChangeId()); submit(change2.getChangeId()); assertMerged(change1.getChangeId()); @@ -938,7 +938,7 @@ testRepo .git() .push() - .setRefSpecs(new RefSpec("refs/heads/stable:refs/for/stable/" + name("topic"))) + .setRefSpecs(new RefSpec("refs/heads/stable:refs/for/stable%topic=" + name("topic"))) .call(); // Merge the fix into master. @@ -955,7 +955,7 @@ testRepo .git() .push() - .setRefSpecs(new RefSpec("refs/heads/master:refs/for/master/" + name("topic"))) + .setRefSpecs(new RefSpec("refs/heads/master:refs/for/master%topic=" + name("topic"))) .call(); // Submit together. @@ -1205,7 +1205,7 @@ } @Test - @GerritConfig(name = "index.reindexAfterRefUpdate", value = "true") + @GerritConfig(name = "index.change.indexMergeable", value = "true") public void submitSchedulesOpenChangesOfSameBranchForReindexing() throws Throwable { // Create a merged change. PushOneCommit push = @@ -1475,6 +1475,6 @@ protected PushOneCommit.Result createChange( String subject, String fileName, String content, String topic) throws Throwable { PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content); - return push.to("refs/for/master/" + name(topic)); + return push.to("refs/for/master%topic=" + name(topic)); } }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java index a4fa84b..f77552d 100644 --- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java +++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
@@ -102,11 +102,11 @@ PushOneCommit.Result change1 = pushFactory .create(admin.newIdent(), testRepo, "Change 1", "a", "a") - .to("refs/for/master/" + name("topic")); + .to("refs/for/master%topic=" + name("topic")); PushOneCommit push2 = pushFactory.create(admin.newIdent(), testRepo, "Change 2", "b", "b"); push2.noParents(); - PushOneCommit.Result change2 = push2.to("refs/for/master/" + name("topic")); + PushOneCommit.Result change2 = push2.to("refs/for/master%topic=" + name("topic")); change2.assertOkStatus(); approve(change1.getChangeId());
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java index dda7bbd..911a04d 100644 --- a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java +++ b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -63,11 +63,40 @@ return gApi.changes().id(id).revision(1).actions(); } + protected Map<String, ActionInfo> getChangeActions(String id) throws Exception { + return gApi.changes().id(id).get().actions; + } + protected String getETag(String id) throws Exception { return gApi.changes().id(id).current().etag(); } @Test + public void changeActionOneMergedChangeHasOnlyNormalRevert() throws Exception { + String changeId = createChangeWithTopic().getChangeId(); + gApi.changes().id(changeId).current().review(ReviewInput.approve()); + gApi.changes().id(changeId).current().submit(); + Map<String, ActionInfo> actions = getChangeActions(changeId); + assertThat(actions).containsKey("revert"); + assertThat(actions).doesNotContainKey("revert_submission"); + } + + @Test + public void changeActionTwoMergedChangesHaveReverts() throws Exception { + String changeId1 = createChangeWithTopic().getChangeId(); + String changeId2 = createChangeWithTopic().getChangeId(); + gApi.changes().id(changeId1).current().review(ReviewInput.approve()); + gApi.changes().id(changeId2).current().review(ReviewInput.approve()); + gApi.changes().id(changeId2).current().submit(); + Map<String, ActionInfo> actions1 = getChangeActions(changeId1); + assertThat(actions1).containsKey("revert"); + assertThat(actions1).containsKey("revert_submission"); + Map<String, ActionInfo> actions2 = getChangeActions(changeId2); + assertThat(actions2).containsKey("revert"); + assertThat(actions2).containsKey("revert_submission"); + } + + @Test public void revisionActionsOneChangePerTopicUnapproved() throws Exception { String changeId = createChangeWithTopic().getChangeId(); Map<String, ActionInfo> actions = getActions(changeId);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java index a1167ed..10bae39 100644 --- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java +++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -39,6 +39,7 @@ import com.google.gerrit.extensions.api.changes.CherryPickInput; import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.api.changes.ReviewInput; +import com.google.gerrit.extensions.api.projects.BranchInput; import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.client.GeneralPreferencesInfo; import com.google.gerrit.extensions.common.ChangeInfo; @@ -606,6 +607,67 @@ } } + @Test + public void createChangeWithSubmittedMergeSource() throws Exception { + // Provide coverage for a performance optimization in CommitsCollection#canRead. + BranchInput branchInput = new BranchInput(); + String mergeTarget = "refs/heads/new-branch"; + RevCommit startCommit = projectOperations.project(project).getHead("master"); + + branchInput.revision = startCommit.name(); + branchInput.ref = mergeTarget; + + gApi.projects().name(project.get()).branch(mergeTarget).create(branchInput); + + PushOneCommit.Result result1 = + pushFactory + .create( + admin.newIdent(), testRepo, "subject1", ImmutableMap.of("file1.txt", "content 1")) + .to("refs/for/master"); + result1.assertOkStatus(); + + testRepo.branch("HEAD").update(startCommit); + PushOneCommit.Result result2 = + pushFactory + .create( + admin.newIdent(), testRepo, "subject2", ImmutableMap.of("file2.txt", "content 2")) + .to("refs/for/master"); + result2.assertOkStatus(); + + ReviewInput reviewInput = ReviewInput.approve().label("Code-Review", 2); + + gApi.changes().id(result1.getChangeId()).revision("current").review(reviewInput); + gApi.changes().id(result1.getChangeId()).revision("current").submit(); + + gApi.changes().id(result2.getChangeId()).revision("current").review(reviewInput); + gApi.changes().id(result2.getChangeId()).revision("current").submit(); + + String mergeRev = gApi.projects().name(project.get()).branch("master").get().revision; + RevCommit mergeCommit = projectOperations.project(project).getHead("master"); + assertThat(mergeCommit.getParents().length).isEqualTo(2); + + testRepo.git().fetch().call(); + testRepo.branch("HEAD").update(mergeCommit); + PushOneCommit.Result result3 = + pushFactory + .create( + admin.newIdent(), testRepo, "subject3", ImmutableMap.of("file1.txt", "content 3")) + .to("refs/for/master"); + result2.assertOkStatus(); + gApi.changes().id(result3.getChangeId()).revision("current").review(reviewInput); + gApi.changes().id(result3.getChangeId()).revision("current").submit(); + + // Now master doesn't point directly to mergeRev + ChangeInput in = new ChangeInput(); + in.branch = mergeTarget; + in.merge = new MergeInput(); + in.project = project.get(); + in.merge.source = mergeRev; + in.subject = "propagate merge"; + + gApi.changes().create(in); + } + private ChangeInput newChangeInput(ChangeStatus status) { ChangeInput in = new ChangeInput(); in.project = project.get();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java index 3030b02..ea3a6a0 100644 --- a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java +++ b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
@@ -22,6 +22,7 @@ import com.google.gerrit.acceptance.AbstractDaemonTest; 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.group.GroupOperations; import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; @@ -58,6 +59,7 @@ } @Test + @GerritConfig(name = "index.change.indexMergeable", value = "true") public void indexChangeAfterOwnerLosesVisibility() throws Exception { // Create a test group with 2 users as members TestAccount user2 = accountCreator.user2();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java index 063f1a0..37b1713 100644 --- a/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java +++ b/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
@@ -22,10 +22,10 @@ import static com.google.gerrit.testing.GerritJUnit.assertThrows; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.GitUtil; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; +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.common.data.LabelFunction;
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java b/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java index 1a3c10f..f5cca1b 100644 --- a/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java +++ b/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
@@ -18,8 +18,8 @@ import static com.google.gerrit.testing.GerritJUnit.assertThrows; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; import com.google.gerrit.entities.Project; import com.google.gerrit.extensions.api.projects.ConfigInput;
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java index 73f10e5..a63d60a 100644 --- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java +++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
@@ -217,7 +217,7 @@ "new.txt", "Conflicting line #2", ImmutableList.of(f.getCommit()), - "refs/for/master/" + name("topic1")); + "refs/for/master%topic=" + name("topic1")); PushOneCommit.Result h = createChange(project2, "H"); PushOneCommit.Result i = @@ -231,7 +231,7 @@ "new.txt", "Sadly conflicting topic-wise", ImmutableList.of(i.getCommit(), j.getCommit()), - "refs/for/master/" + name("topic1")); + "refs/for/master%topic=" + name("topic1")); approve(h.getChangeId()); approve(i.getChangeId()); @@ -253,7 +253,7 @@ "new.txt", "Resolving conflicts again", ImmutableList.of(c.getCommit(), g.getCommit()), - "refs/for/master/" + name("topic1")); + "refs/for/master%topic=" + name("topic1")); approve(l.getChangeId()); assertChangeSetMergeable(l.getChange(), true);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java index 8e0042c..568c63b 100644 --- a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java +++ b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -27,8 +27,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.TestAccount; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.acceptance.testsuite.account.AccountOperations; import com.google.gerrit.acceptance.testsuite.group.GroupOperations; import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ListCachesIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ListCachesIT.java index ae17be0..8baeffc 100644 --- a/javatests/com/google/gerrit/acceptance/rest/config/ListCachesIT.java +++ b/javatests/com/google/gerrit/acceptance/rest/config/ListCachesIT.java
@@ -18,6 +18,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.collect.Ordering; +import com.google.common.io.BaseEncoding; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.RestResponse; import com.google.gerrit.server.restapi.config.ListCaches.CacheInfo; @@ -26,7 +27,6 @@ import java.util.Arrays; import java.util.List; import java.util.Map; -import org.eclipse.jgit.util.Base64; import org.junit.Test; public class ListCachesIT extends AbstractDaemonTest { @@ -78,7 +78,7 @@ public void listCacheNamesTextList() throws Exception { RestResponse r = adminRestSession.get("/config/server/caches/?format=TEXT_LIST"); r.assertOK(); - String result = new String(Base64.decode(r.getEntityContent()), UTF_8.name()); + String result = new String(BaseEncoding.base64().decode(r.getEntityContent()), UTF_8); List<String> list = Arrays.asList(result.split("\n")); assertThat(list).contains("accounts"); assertThat(list).contains("projects");
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java index 1d87ca1..9573eb0 100644 --- a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java +++ b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -18,9 +18,9 @@ import static java.nio.charset.StandardCharsets.UTF_8; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.UseSsh; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.common.RawInputUtil; import com.google.gerrit.extensions.api.plugins.InstallPluginInput; import com.google.gerrit.extensions.client.AccountFieldName; @@ -180,6 +180,9 @@ assertThat(i.gerrit.allUsers).isEqualTo(AllUsersNameProvider.DEFAULT); assertThat(i.gerrit.reportBugUrl).isNull(); + // index + assertThat(i.index.change.indexMergeable).isNull(); // false in all tests + // plugin assertThat(i.plugin.jsResourcePaths).isEmpty(); @@ -206,4 +209,11 @@ ServerInfo i = gApi.config().server().getInfo(); assertThat(i.change.excludeMergeableInChangeInfo).isTrue(); } + + @Test + @GerritConfig(name = "index.change.indexMergeable", value = "true") + public void indexMergeableIsTrueWhenTrueInConfig() throws Exception { + ServerInfo i = gApi.config().server().getInfo(); + assertThat(i.index.change.indexMergeable).isTrue(); + } }
diff --git a/javatests/com/google/gerrit/acceptance/rest/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/rest/group/GroupsIT.java deleted file mode 100644 index e153e561..0000000 --- a/javatests/com/google/gerrit/acceptance/rest/group/GroupsIT.java +++ /dev/null
@@ -1,31 +0,0 @@ -// 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.acceptance.rest.group; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.RestResponse; -import org.junit.Test; - -public class GroupsIT extends AbstractDaemonTest { - @Test - public void invalidQueryOptions() throws Exception { - RestResponse r = adminRestSession.put("/groups/?query=foo&query2=bar"); - r.assertBadRequest(); - assertThat(r.getEntityContent()) - .isEqualTo("\"query\" and \"query2\" options are mutually exclusive"); - } -}
diff --git a/javatests/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java b/javatests/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java new file mode 100644 index 0000000..d8132b7 --- /dev/null +++ b/javatests/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
@@ -0,0 +1,37 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.rest.group; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.RestResponse; +import com.google.gerrit.extensions.common.GroupInfo; +import com.google.gson.reflect.TypeToken; +import java.util.Map; +import org.junit.Test; + +public class ListGroupsIT extends AbstractDaemonTest { + @Test + public void listAllGroups() throws Exception { + RestResponse response = adminRestSession.get("/groups/"); + response.assertOK(); + + Map<String, GroupInfo> groupMap = + newGson() + .fromJson(response.getReader(), new TypeToken<Map<String, GroupInfo>>() {}.getType()); + assertThat(groupMap.keySet()).containsExactly("Administrators", "Non-Interactive Users"); + } +}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java index 91a10ca..3e9b1f6 100644 --- a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java +++ b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -604,7 +604,7 @@ } @Test - public void syncCreateGroupPermission() throws Exception { + public void syncCreateGroupPermission_addAndRemoveCreateGroupCapability() throws Exception { // Grant CREATE_GROUP to Registered Users ProjectAccessInput accessInput = newProjectAccessInput(); AccessSectionInfo accessSection = newAccessSectionInfo(); @@ -642,6 +642,44 @@ } @Test + public void syncCreateGroupPermission_addCreateGroupCapabilityToMultipleGroups() + throws Exception { + PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false); + + // Grant CREATE_GROUP to Registered Users + ProjectAccessInput accessInput = newProjectAccessInput(); + AccessSectionInfo accessSection = newAccessSectionInfo(); + PermissionInfo createGroup = newPermissionInfo(); + createGroup.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri); + accessSection.permissions.put(GlobalCapability.CREATE_GROUP, createGroup); + accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSection); + gApi.projects().name(allProjects.get()).access(accessInput); + + // Grant CREATE_GROUP to Administrators + accessInput = newProjectAccessInput(); + accessSection = newAccessSectionInfo(); + createGroup = newPermissionInfo(); + createGroup.rules.put(adminGroupUuid().get(), pri); + accessSection.permissions.put(GlobalCapability.CREATE_GROUP, createGroup); + accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSection); + gApi.projects().name(allProjects.get()).access(accessInput); + + // Assert that the permissions were synced from All-Projects (global) to All-Users (ref) + Map<String, AccessSectionInfo> local = gApi.projects().name("All-Users").access().local; + assertThat(local).isNotNull(); + assertThat(local).containsKey(RefNames.REFS_GROUPS + "*"); + Map<String, PermissionInfo> permissions = local.get(RefNames.REFS_GROUPS + "*").permissions; + assertThat(permissions).hasSize(2); + // READ is the default permission and should be preserved by the syncer + assertThat(permissions.keySet()).containsExactly(Permission.READ, Permission.CREATE); + Map<String, PermissionRuleInfo> rules = permissions.get(Permission.CREATE).rules; + assertThat(rules.keySet()) + .containsExactly(SystemGroupBackend.REGISTERED_USERS.get(), adminGroupUuid().get()); + assertThat(rules.get(SystemGroupBackend.REGISTERED_USERS.get())).isEqualTo(pri); + assertThat(rules.get(adminGroupUuid().get())).isEqualTo(pri); + } + + @Test public void addAccessSectionForInvalidRef() throws Exception { ProjectAccessInput accessInput = newProjectAccessInput(); AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/BUILD b/javatests/com/google/gerrit/acceptance/rest/project/BUILD index e082559..b50a12b 100644 --- a/javatests/com/google/gerrit/acceptance/rest/project/BUILD +++ b/javatests/com/google/gerrit/acceptance/rest/project/BUILD
@@ -6,6 +6,7 @@ group = f[:f.index(".")], labels = ["rest"], deps = [ + ":labelassert", ":project", ":push_tag_util", ":refassert", @@ -14,6 +15,19 @@ ) for f in glob(["*IT.java"])] java_library( + name = "labelassert", + srcs = [ + "LabelAssert.java", + ], + deps = [ + "//java/com/google/gerrit/common:server", + "//java/com/google/gerrit/extensions:api", + "//java/com/google/gerrit/server", + "//lib/truth", + ], +) + +java_library( name = "refassert", srcs = [ "RefAssert.java",
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java new file mode 100644 index 0000000..57a1e56 --- /dev/null +++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java
@@ -0,0 +1,611 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.rest.project; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; +import static com.google.gerrit.testing.GerritJUnit.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; +import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; +import com.google.gerrit.common.data.LabelFunction; +import com.google.gerrit.common.data.Permission; +import com.google.gerrit.entities.RefNames; +import com.google.gerrit.extensions.common.LabelDefinitionInfo; +import com.google.gerrit.extensions.common.LabelDefinitionInput; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.inject.Inject; +import org.junit.Test; + +@NoHttpd +public class CreateLabelIT extends AbstractDaemonTest { + @Inject private RequestScopeOperations requestScopeOperations; + @Inject private ProjectOperations projectOperations; + + @Test + public void anonymous() throws Exception { + requestScopeOperations.setApiUserAnonymous(); + AuthException thrown = + assertThrows( + AuthException.class, + () -> + gApi.projects() + .name(project.get()) + .label("Foo-Review") + .create(new LabelDefinitionInput())); + assertThat(thrown).hasMessageThat().contains("Authentication required"); + } + + @Test + public void notAllowed() throws Exception { + projectOperations + .project(project) + .forUpdate() + .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS)) + .update(); + + requestScopeOperations.setApiUser(user.id()); + AuthException thrown = + assertThrows( + AuthException.class, + () -> + gApi.projects() + .name(project.get()) + .label("Foo-Review") + .create(new LabelDefinitionInput())); + assertThat(thrown).hasMessageThat().contains("write refs/meta/config not permitted"); + } + + @Test + public void cannotCreateLabelIfNameDoesntMatch() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.name = "Foo"; + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(project.get()).label("Bar").create(input)); + assertThat(thrown).hasMessageThat().contains("name in input must match name in URL"); + } + + @Test + public void cannotCreateLabelWithNameThatIsAlreadyInUse() throws Exception { + ResourceConflictException thrown = + assertThrows( + ResourceConflictException.class, + () -> + gApi.projects() + .name(allProjects.get()) + .label("Code-Review") + .create(new LabelDefinitionInput())); + assertThat(thrown).hasMessageThat().contains("label Code-Review already exists"); + } + + @Test + public void cannotCreateLabelWithNameThatConflicts() throws Exception { + ResourceConflictException thrown = + assertThrows( + ResourceConflictException.class, + () -> + gApi.projects() + .name(allProjects.get()) + .label("code-review") + .create(new LabelDefinitionInput())); + assertThat(thrown) + .hasMessageThat() + .contains("label code-review conflicts with existing label Code-Review"); + } + + @Test + public void cannotCreateLabelWithInvalidName() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", "0", "Don't Know", "-1", "Looks Bad"); + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(project.get()).label("INVALID_NAME").create(input)); + assertThat(thrown).hasMessageThat().contains("invalid name: INVALID_NAME"); + } + + @Test + public void cannotCreateLabelWithoutValues() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(project.get()).label("Foo").create(input)); + assertThat(thrown).hasMessageThat().contains("values are required"); + + input.values = ImmutableMap.of(); + thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(project.get()).label("Foo").create(input)); + assertThat(thrown).hasMessageThat().contains("values are required"); + } + + @Test + public void cannotCreateLabelWithInvalidValues() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("invalidValue", "description"); + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(project.get()).label("Foo").create(input)); + assertThat(thrown).hasMessageThat().contains("invalid value: invalidValue"); + } + + @Test + public void cannotCreateLabelWithValuesThatHaveEmptyDescription() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", ""); + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(project.get()).label("Foo").create(input)); + assertThat(thrown).hasMessageThat().contains("description for value '+1' cannot be empty"); + } + + @Test + public void cannotCreateLabelWithDuplicateValues() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + // Positive values can be specified as '<value>' or '+<value>'. + input.values = + ImmutableMap.of( + "+1", "Looks Good", "1", "Looks Good", "0", "Don't Know", "-1", "Looks Bad"); + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(allProjects.get()).label("Foo").create(input)); + assertThat(thrown).hasMessageThat().contains("duplicate value: 1"); + } + + @Test + public void cannotCreateLabelWithInvalidDefaultValue() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", "0", "Don't Know", "-1", "Looks Bad"); + input.defaultValue = 5; + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(project.get()).label("Foo").create(input)); + assertThat(thrown).hasMessageThat().contains("invalid default value: " + input.defaultValue); + } + + @Test + public void cannotCreateLabelWithUnknownFunction() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", "0", "Don't Know", "-1", "Looks Bad"); + input.function = "UnknownFuction"; + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(project.get()).label("Foo").create(input)); + assertThat(thrown).hasMessageThat().contains("unknown function: " + input.function); + } + + @Test + public void cannotCreateLabelWithInvalidBranch() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", "0", "Don't Know", "-1", "Looks Bad"); + input.branches = ImmutableList.of("refs heads master"); + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(project.get()).label("Foo").create(input)); + assertThat(thrown).hasMessageThat().contains("invalid branch: refs heads master"); + } + + @Test + public void createWithNameAndValuesOnly() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("Foo").create(input).get(); + + assertThat(createdLabel.name).isEqualTo("Foo"); + assertThat(createdLabel.projectName).isEqualTo(project.get()); + assertThat(createdLabel.function).isEqualTo(LabelFunction.MAX_WITH_BLOCK.getFunctionName()); + assertThat(createdLabel.values).containsExactlyEntriesIn(input.values); + assertThat(createdLabel.defaultValue).isEqualTo(0); + assertThat(createdLabel.branches).isNull(); + assertThat(createdLabel.canOverride).isTrue(); + assertThat(createdLabel.copyAnyScore).isNull(); + assertThat(createdLabel.copyMinScore).isNull(); + assertThat(createdLabel.copyMaxScore).isNull(); + assertThat(createdLabel.copyAllScoresIfNoChange).isTrue(); + assertThat(createdLabel.copyAllScoresIfNoCodeChange).isNull(); + assertThat(createdLabel.copyAllScoresOnTrivialRebase).isNull(); + assertThat(createdLabel.copyAllScoresOnMergeFirstParentUpdate).isNull(); + assertThat(createdLabel.allowPostSubmit).isTrue(); + assertThat(createdLabel.ignoreSelfApproval).isNull(); + } + + @Test + public void createWithFunction() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.function = LabelFunction.NO_OP.getFunctionName(); + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("Foo").create(input).get(); + + assertThat(createdLabel.function).isEqualTo(LabelFunction.NO_OP.getFunctionName()); + } + + @Test + public void functionEmptyAfterTrim() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.function = " "; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("Foo").create(input).get(); + + assertThat(createdLabel.function).isEqualTo(LabelFunction.MAX_WITH_BLOCK.getFunctionName()); + } + + @Test + public void valuesAndDescriptionsAreTrimmed() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + // Positive values can be specified as '<value>' or '+<value>'. + input.values = + ImmutableMap.of( + " 2 ", + " Looks Very Good ", + " +1 ", + " Looks Good ", + " 0 ", + " Don't Know ", + " -1 ", + " Looks Bad ", + " -2 ", + " Looks Very Bad "); + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("Foo").create(input).get(); + assertThat(createdLabel.values) + .containsExactly( + "+2", "Looks Very Good", + "+1", "Looks Good", + " 0", "Don't Know", + "-1", "Looks Bad", + "-2", "Looks Very Bad"); + } + + @Test + public void createWithDefaultValue() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.defaultValue = 1; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("Foo").create(input).get(); + + assertThat(createdLabel.defaultValue).isEqualTo(input.defaultValue); + } + + @Test + public void createWithBranches() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + // Branches can be full ref, ref pattern or regular expression. + input.branches = + ImmutableList.of("refs/heads/master", "refs/heads/foo/*", "^refs/heads/stable-.*"); + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("Foo").create(input).get(); + assertThat(createdLabel.branches).containsExactlyElementsIn(input.branches); + } + + @Test + public void branchesAreTrimmed() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.branches = + ImmutableList.of(" refs/heads/master ", " refs/heads/foo/* ", " ^refs/heads/stable-.* "); + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("Foo").create(input).get(); + assertThat(createdLabel.branches) + .containsExactly("refs/heads/master", "refs/heads/foo/*", "^refs/heads/stable-.*"); + } + + @Test + public void emptyBranchesAreIgnored() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.branches = ImmutableList.of("refs/heads/master", "", " "); + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("Foo").create(input).get(); + assertThat(createdLabel.branches).containsExactly("refs/heads/master"); + } + + @Test + public void branchesAreAutomaticallyPrefixedWithRefsHeads() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.branches = ImmutableList.of("master", "refs/meta/config"); + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("Foo").create(input).get(); + assertThat(createdLabel.branches).containsExactly("refs/heads/master", "refs/meta/config"); + } + + @Test + public void createWithCanOverride() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.canOverride = true; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.canOverride).isTrue(); + } + + @Test + public void createWithoutCanOverride() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.canOverride = false; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.canOverride).isNull(); + } + + @Test + public void createWithCopyAnyScore() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyAnyScore = true; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyAnyScore).isTrue(); + } + + @Test + public void createWithoutCopyAnyScore() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyAnyScore = false; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyAnyScore).isNull(); + } + + @Test + public void createWithCopyMinScore() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyMinScore = true; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyMinScore).isTrue(); + } + + @Test + public void createWithoutCopyMinScore() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyMinScore = false; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyMinScore).isNull(); + } + + @Test + public void createWithCopyMaxScore() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyMaxScore = true; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyMaxScore).isTrue(); + } + + @Test + public void createWithoutCopyMaxScore() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyMaxScore = false; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyMaxScore).isNull(); + } + + @Test + public void createWithCopyAllScoresIfNoChange() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyAllScoresIfNoChange = true; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyAllScoresIfNoChange).isTrue(); + } + + @Test + public void createWithoutCopyAllScoresIfNoChange() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyAllScoresIfNoChange = false; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyAllScoresIfNoChange).isNull(); + } + + @Test + public void createWithCopyAllScoresIfNoCodeChange() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyAllScoresIfNoCodeChange = true; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyAllScoresIfNoCodeChange).isTrue(); + } + + @Test + public void createWithoutCopyAllScoresIfNoCodeChange() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyAllScoresIfNoCodeChange = false; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyAllScoresIfNoCodeChange).isNull(); + } + + @Test + public void createWithCopyAllScoresOnTrivialRebase() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyAllScoresOnTrivialRebase = true; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyAllScoresOnTrivialRebase).isTrue(); + } + + @Test + public void createWithoutCopyAllScoresOnTrivialRebase() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyAllScoresOnTrivialRebase = false; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyAllScoresOnTrivialRebase).isNull(); + } + + @Test + public void createWithCopyAllScoresOnMergeFirstParentUpdate() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyAllScoresOnMergeFirstParentUpdate = true; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyAllScoresOnMergeFirstParentUpdate).isTrue(); + } + + @Test + public void createWithoutCopyAllScoresOnMergeFirstParentUpdate() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyAllScoresOnMergeFirstParentUpdate = false; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyAllScoresOnMergeFirstParentUpdate).isNull(); + } + + @Test + public void createWithAllowPostSubmit() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.allowPostSubmit = true; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.allowPostSubmit).isTrue(); + } + + @Test + public void createWithoutAllowPostSubmit() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.allowPostSubmit = false; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.allowPostSubmit).isNull(); + } + + @Test + public void createWithIgnoreSelfApproval() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.ignoreSelfApproval = true; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.ignoreSelfApproval).isTrue(); + } + + @Test + public void createWithoutIgnoreSelfApproval() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.ignoreSelfApproval = false; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.ignoreSelfApproval).isNull(); + } + + @Test + public void defaultCommitMessage() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + gApi.projects().name(project.get()).label("Foo").create(input); + assertThat(projectOperations.project(project).getHead(RefNames.REFS_CONFIG).getShortMessage()) + .isEqualTo("Update label"); + } + + @Test + public void withCommitMessage() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.commitMessage = "Add Foo Label"; + gApi.projects().name(project.get()).label("Foo").create(input); + assertThat(projectOperations.project(project).getHead(RefNames.REFS_CONFIG).getShortMessage()) + .isEqualTo(input.commitMessage); + } + + @Test + public void commitMessageIsTrimmed() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.commitMessage = " Add Foo Label "; + gApi.projects().name(project.get()).label("Foo").create(input); + assertThat(projectOperations.project(project).getHead(RefNames.REFS_CONFIG).getShortMessage()) + .isEqualTo("Add Foo Label"); + } +}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java index 6b2baa7..0ba4f16 100644 --- a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java +++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -30,9 +30,9 @@ import com.google.common.collect.Sets; import com.google.common.net.HttpHeaders; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.RestResponse; 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; import com.google.gerrit.common.data.GlobalCapability;
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteLabelIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteLabelIT.java new file mode 100644 index 0000000..c916285 --- /dev/null +++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteLabelIT.java
@@ -0,0 +1,108 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.rest.project; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; +import static com.google.gerrit.testing.GerritJUnit.assertThrows; + +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; +import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; +import com.google.gerrit.common.data.Permission; +import com.google.gerrit.entities.RefNames; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.inject.Inject; +import org.junit.Test; + +public class DeleteLabelIT extends AbstractDaemonTest { + @Inject private RequestScopeOperations requestScopeOperations; + @Inject private ProjectOperations projectOperations; + + @Test + public void anonymous() throws Exception { + requestScopeOperations.setApiUserAnonymous(); + AuthException thrown = + assertThrows( + AuthException.class, + () -> gApi.projects().name(allProjects.get()).label("Code-Review").delete()); + assertThat(thrown).hasMessageThat().contains("Authentication required"); + } + + @Test + public void notAllowed() throws Exception { + projectOperations + .project(allProjects) + .forUpdate() + .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS)) + .update(); + + requestScopeOperations.setApiUser(user.id()); + AuthException thrown = + assertThrows( + AuthException.class, + () -> gApi.projects().name(allProjects.get()).label("Code-Review").delete()); + assertThat(thrown).hasMessageThat().contains("write refs/meta/config not permitted"); + } + + @Test + public void nonExisting() throws Exception { + ResourceNotFoundException thrown = + assertThrows( + ResourceNotFoundException.class, + () -> gApi.projects().name(allProjects.get()).label("Non-Existing-Review").delete()); + assertThat(thrown).hasMessageThat().contains("Not found: Non-Existing-Review"); + } + + @Test + public void delete() throws Exception { + gApi.projects().name(allProjects.get()).label("Code-Review").delete(); + + ResourceNotFoundException thrown = + assertThrows( + ResourceNotFoundException.class, + () -> gApi.projects().name(project.get()).label("Code-Review").get()); + assertThat(thrown).hasMessageThat().contains("Not found: Code-Review"); + } + + @Test + public void defaultCommitMessage() throws Exception { + gApi.projects().name(allProjects.get()).label("Code-Review").delete(); + assertThat( + projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage()) + .isEqualTo("Delete label"); + } + + @Test + public void withCommitMessage() throws Exception { + gApi.projects().name(allProjects.get()).label("Code-Review").delete("Delete Code-Review label"); + assertThat( + projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage()) + .isEqualTo("Delete Code-Review label"); + } + + @Test + public void commitMessageIsTrimmed() throws Exception { + gApi.projects() + .name(allProjects.get()) + .label("Code-Review") + .delete(" Delete Code-Review label "); + assertThat( + projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage()) + .isEqualTo("Delete Code-Review label"); + } +}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/GetLabelIT.java b/javatests/com/google/gerrit/acceptance/rest/project/GetLabelIT.java new file mode 100644 index 0000000..9f98490 --- /dev/null +++ b/javatests/com/google/gerrit/acceptance/rest/project/GetLabelIT.java
@@ -0,0 +1,154 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.rest.project; + +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.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; +import com.google.gerrit.common.data.LabelFunction; +import com.google.gerrit.common.data.LabelType; +import com.google.gerrit.extensions.common.LabelDefinitionInfo; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.inject.Inject; +import org.junit.Test; + +@NoHttpd +public class GetLabelIT extends AbstractDaemonTest { + @Inject private RequestScopeOperations requestScopeOperations; + + @Test + public void anonymous() throws Exception { + requestScopeOperations.setApiUserAnonymous(); + AuthException thrown = + assertThrows( + AuthException.class, + () -> gApi.projects().name(allProjects.get()).label("Code-Review").get()); + assertThat(thrown).hasMessageThat().contains("Authentication required"); + } + + @Test + public void notAllowed() throws Exception { + requestScopeOperations.setApiUser(user.id()); + AuthException thrown = + assertThrows( + AuthException.class, + () -> gApi.projects().name(allProjects.get()).label("Code-Review").get()); + assertThat(thrown).hasMessageThat().contains("read refs/meta/config not permitted"); + } + + @Test + public void notFound() throws Exception { + ResourceNotFoundException thrown = + assertThrows( + ResourceNotFoundException.class, + () -> gApi.projects().name(project.get()).label("Foo-Review").get()); + assertThat(thrown).hasMessageThat().contains("Not found: Foo-Review"); + } + + @Test + public void allProjectsCodeReviewLabel() throws Exception { + LabelDefinitionInfo codeReviewLabel = + gApi.projects().name(allProjects.get()).label("Code-Review").get(); + LabelAssert.assertCodeReviewLabel(codeReviewLabel); + } + + @Test + public void labelWithDefaultValue() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + + // set default value + try (ProjectConfigUpdate u = updateProject(project)) { + LabelType labelType = u.getConfig().getLabelSections().get("foo"); + labelType.setDefaultValue((short) 1); + u.getConfig().getLabelSections().put(labelType.getName(), labelType); + u.save(); + } + + LabelDefinitionInfo fooLabel = gApi.projects().name(project.get()).label("foo").get(); + assertThat(fooLabel.defaultValue).isEqualTo(1); + } + + @Test + public void labelLimitedToBranches() throws Exception { + configLabel( + "foo", LabelFunction.NO_OP, ImmutableList.of("refs/heads/master", "^refs/heads/stable-.*")); + + LabelDefinitionInfo fooLabel = gApi.projects().name(project.get()).label("foo").get(); + assertThat(fooLabel.branches).containsExactly("refs/heads/master", "^refs/heads/stable-.*"); + } + + @Test + public void labelWithoutRules() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + + // unset rules which are enabled by default + try (ProjectConfigUpdate u = updateProject(project)) { + LabelType labelType = u.getConfig().getLabelSections().get("foo"); + labelType.setCanOverride(false); + labelType.setCopyAllScoresIfNoChange(false); + labelType.setAllowPostSubmit(false); + u.getConfig().getLabelSections().put(labelType.getName(), labelType); + u.save(); + } + + LabelDefinitionInfo fooLabel = gApi.projects().name(project.get()).label("foo").get(); + assertThat(fooLabel.canOverride).isNull(); + assertThat(fooLabel.copyAnyScore).isNull(); + assertThat(fooLabel.copyMinScore).isNull(); + assertThat(fooLabel.copyMaxScore).isNull(); + assertThat(fooLabel.copyAllScoresIfNoChange).isNull(); + assertThat(fooLabel.copyAllScoresIfNoCodeChange).isNull(); + assertThat(fooLabel.copyAllScoresOnTrivialRebase).isNull(); + assertThat(fooLabel.copyAllScoresOnMergeFirstParentUpdate).isNull(); + assertThat(fooLabel.allowPostSubmit).isNull(); + assertThat(fooLabel.ignoreSelfApproval).isNull(); + } + + @Test + public void labelWithAllRules() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + + // set rules which are not enabled by default + try (ProjectConfigUpdate u = updateProject(project)) { + LabelType labelType = u.getConfig().getLabelSections().get("foo"); + labelType.setCopyAnyScore(true); + labelType.setCopyMinScore(true); + labelType.setCopyMaxScore(true); + labelType.setCopyAllScoresIfNoCodeChange(true); + labelType.setCopyAllScoresOnTrivialRebase(true); + labelType.setCopyAllScoresOnMergeFirstParentUpdate(true); + labelType.setIgnoreSelfApproval(true); + u.getConfig().getLabelSections().put(labelType.getName(), labelType); + u.save(); + } + + LabelDefinitionInfo fooLabel = gApi.projects().name(project.get()).label("foo").get(); + assertThat(fooLabel.canOverride).isTrue(); + assertThat(fooLabel.copyAnyScore).isTrue(); + assertThat(fooLabel.copyMinScore).isTrue(); + assertThat(fooLabel.copyMaxScore).isTrue(); + assertThat(fooLabel.copyAllScoresIfNoChange).isTrue(); + assertThat(fooLabel.copyAllScoresIfNoCodeChange).isTrue(); + assertThat(fooLabel.copyAllScoresOnTrivialRebase).isTrue(); + assertThat(fooLabel.copyAllScoresOnMergeFirstParentUpdate).isTrue(); + assertThat(fooLabel.allowPostSubmit).isTrue(); + assertThat(fooLabel.ignoreSelfApproval).isTrue(); + } +}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/LabelAssert.java b/javatests/com/google/gerrit/acceptance/rest/project/LabelAssert.java new file mode 100644 index 0000000..7998ecb --- /dev/null +++ b/javatests/com/google/gerrit/acceptance/rest/project/LabelAssert.java
@@ -0,0 +1,55 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.rest.project; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gerrit.common.data.LabelFunction; +import com.google.gerrit.extensions.common.LabelDefinitionInfo; +import com.google.gerrit.server.config.AllProjectsNameProvider; + +public class LabelAssert { + public static void assertCodeReviewLabel(LabelDefinitionInfo codeReviewLabel) { + assertThat(codeReviewLabel.name).isEqualTo("Code-Review"); + assertThat(codeReviewLabel.projectName).isEqualTo(AllProjectsNameProvider.DEFAULT); + assertThat(codeReviewLabel.function).isEqualTo(LabelFunction.MAX_WITH_BLOCK.getFunctionName()); + assertThat(codeReviewLabel.values) + .containsExactly( + "+2", + "Looks good to me, approved", + "+1", + "Looks good to me, but someone else must approve", + " 0", + "No score", + "-1", + "I would prefer this is not merged as is", + "-2", + "This shall not be merged"); + assertThat(codeReviewLabel.defaultValue).isEqualTo(0); + assertThat(codeReviewLabel.branches).isNull(); + assertThat(codeReviewLabel.canOverride).isTrue(); + assertThat(codeReviewLabel.copyAnyScore).isNull(); + assertThat(codeReviewLabel.copyMinScore).isTrue(); + assertThat(codeReviewLabel.copyMaxScore).isNull(); + assertThat(codeReviewLabel.copyAllScoresIfNoChange).isTrue(); + assertThat(codeReviewLabel.copyAllScoresIfNoCodeChange).isNull(); + assertThat(codeReviewLabel.copyAllScoresOnTrivialRebase).isTrue(); + assertThat(codeReviewLabel.copyAllScoresOnMergeFirstParentUpdate).isNull(); + assertThat(codeReviewLabel.allowPostSubmit).isTrue(); + assertThat(codeReviewLabel.ignoreSelfApproval).isNull(); + } + + private LabelAssert() {} +}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListLabelsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListLabelsIT.java new file mode 100644 index 0000000..d2539e5 --- /dev/null +++ b/javatests/com/google/gerrit/acceptance/rest/project/ListLabelsIT.java
@@ -0,0 +1,268 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.rest.project; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; +import static com.google.gerrit.testing.GerritJUnit.assertThrows; +import static java.util.stream.Collectors.toList; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; +import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; +import com.google.gerrit.common.data.LabelFunction; +import com.google.gerrit.common.data.LabelType; +import com.google.gerrit.common.data.Permission; +import com.google.gerrit.entities.Project; +import com.google.gerrit.entities.RefNames; +import com.google.gerrit.extensions.common.LabelDefinitionInfo; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.inject.Inject; +import java.util.List; +import org.junit.Test; + +@NoHttpd +public class ListLabelsIT extends AbstractDaemonTest { + @Inject private RequestScopeOperations requestScopeOperations; + @Inject private ProjectOperations projectOperations; + + @Test + public void anonymous() throws Exception { + requestScopeOperations.setApiUserAnonymous(); + AuthException thrown = + assertThrows(AuthException.class, () -> gApi.projects().name(project.get()).labels().get()); + assertThat(thrown).hasMessageThat().contains("Authentication required"); + } + + @Test + public void notAllowed() throws Exception { + requestScopeOperations.setApiUser(user.id()); + AuthException thrown = + assertThrows(AuthException.class, () -> gApi.projects().name(project.get()).labels().get()); + assertThat(thrown).hasMessageThat().contains("read refs/meta/config not permitted"); + } + + @Test + public void noLabels() throws Exception { + assertThat(gApi.projects().name(project.get()).labels().get()).isEmpty(); + } + + @Test + public void allProjectsLabels() throws Exception { + List<LabelDefinitionInfo> labels = gApi.projects().name(allProjects.get()).labels().get(); + assertThat(labelNames(labels)).containsExactly("Code-Review"); + + LabelDefinitionInfo codeReviewLabel = Iterables.getOnlyElement(labels); + LabelAssert.assertCodeReviewLabel(codeReviewLabel); + } + + @Test + public void labelsAreSortedByName() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + configLabel("bar", LabelFunction.NO_OP); + configLabel("baz", LabelFunction.NO_OP); + + List<LabelDefinitionInfo> labels = gApi.projects().name(project.get()).labels().get(); + assertThat(labelNames(labels)).containsExactly("bar", "baz", "foo").inOrder(); + } + + @Test + public void labelWithDefaultValue() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + + // set default value + try (ProjectConfigUpdate u = updateProject(project)) { + LabelType labelType = u.getConfig().getLabelSections().get("foo"); + labelType.setDefaultValue((short) 1); + u.getConfig().getLabelSections().put(labelType.getName(), labelType); + u.save(); + } + + List<LabelDefinitionInfo> labels = gApi.projects().name(project.get()).labels().get(); + assertThat(labelNames(labels)).containsExactly("foo"); + + LabelDefinitionInfo fooLabel = Iterables.getOnlyElement(labels); + assertThat(fooLabel.defaultValue).isEqualTo(1); + } + + @Test + public void labelLimitedToBranches() throws Exception { + configLabel( + "foo", LabelFunction.NO_OP, ImmutableList.of("refs/heads/master", "^refs/heads/stable-.*")); + + List<LabelDefinitionInfo> labels = gApi.projects().name(project.get()).labels().get(); + assertThat(labelNames(labels)).containsExactly("foo"); + + LabelDefinitionInfo fooLabel = Iterables.getOnlyElement(labels); + assertThat(fooLabel.branches).containsExactly("refs/heads/master", "^refs/heads/stable-.*"); + } + + @Test + public void labelWithoutRules() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + + // unset rules which are enabled by default + try (ProjectConfigUpdate u = updateProject(project)) { + LabelType labelType = u.getConfig().getLabelSections().get("foo"); + labelType.setCanOverride(false); + labelType.setCopyAllScoresIfNoChange(false); + labelType.setAllowPostSubmit(false); + u.getConfig().getLabelSections().put(labelType.getName(), labelType); + u.save(); + } + + List<LabelDefinitionInfo> labels = gApi.projects().name(project.get()).labels().get(); + assertThat(labelNames(labels)).containsExactly("foo"); + + LabelDefinitionInfo fooLabel = Iterables.getOnlyElement(labels); + assertThat(fooLabel.canOverride).isNull(); + assertThat(fooLabel.copyAnyScore).isNull(); + assertThat(fooLabel.copyMinScore).isNull(); + assertThat(fooLabel.copyMaxScore).isNull(); + assertThat(fooLabel.copyAllScoresIfNoChange).isNull(); + assertThat(fooLabel.copyAllScoresIfNoCodeChange).isNull(); + assertThat(fooLabel.copyAllScoresOnTrivialRebase).isNull(); + assertThat(fooLabel.copyAllScoresOnMergeFirstParentUpdate).isNull(); + assertThat(fooLabel.allowPostSubmit).isNull(); + assertThat(fooLabel.ignoreSelfApproval).isNull(); + } + + @Test + public void labelWithAllRules() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + + // set rules which are not enabled by default + try (ProjectConfigUpdate u = updateProject(project)) { + LabelType labelType = u.getConfig().getLabelSections().get("foo"); + labelType.setCopyAnyScore(true); + labelType.setCopyMinScore(true); + labelType.setCopyMaxScore(true); + labelType.setCopyAllScoresIfNoCodeChange(true); + labelType.setCopyAllScoresOnTrivialRebase(true); + labelType.setCopyAllScoresOnMergeFirstParentUpdate(true); + labelType.setIgnoreSelfApproval(true); + u.getConfig().getLabelSections().put(labelType.getName(), labelType); + u.save(); + } + + List<LabelDefinitionInfo> labels = gApi.projects().name(project.get()).labels().get(); + assertThat(labelNames(labels)).containsExactly("foo"); + + LabelDefinitionInfo fooLabel = Iterables.getOnlyElement(labels); + assertThat(fooLabel.canOverride).isTrue(); + assertThat(fooLabel.copyAnyScore).isTrue(); + assertThat(fooLabel.copyMinScore).isTrue(); + assertThat(fooLabel.copyMaxScore).isTrue(); + assertThat(fooLabel.copyAllScoresIfNoChange).isTrue(); + assertThat(fooLabel.copyAllScoresIfNoCodeChange).isTrue(); + assertThat(fooLabel.copyAllScoresOnTrivialRebase).isTrue(); + assertThat(fooLabel.copyAllScoresOnMergeFirstParentUpdate).isTrue(); + assertThat(fooLabel.allowPostSubmit).isTrue(); + assertThat(fooLabel.ignoreSelfApproval).isTrue(); + } + + @Test + public void withInheritedLabelsNotAllowed() throws Exception { + projectOperations + .project(project) + .forUpdate() + .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS)) + .update(); + + requestScopeOperations.setApiUser(user.id()); + + // can list labels without inheritance + gApi.projects().name(project.get()).labels().get(); + + // cannot list labels with inheritance + AuthException thrown = + assertThrows( + AuthException.class, + () -> gApi.projects().name(project.get()).labels().withInherited(true).get()); + assertThat(thrown) + .hasMessageThat() + .contains("All-Projects: read refs/meta/config not permitted"); + } + + @Test + public void inheritedLabelsOnly() throws Exception { + List<LabelDefinitionInfo> labels = + gApi.projects().name(project.get()).labels().withInherited(true).get(); + assertThat(labelNames(labels)).containsExactly("Code-Review"); + + LabelDefinitionInfo codeReviewLabel = Iterables.getOnlyElement(labels); + LabelAssert.assertCodeReviewLabel(codeReviewLabel); + } + + @Test + public void withInheritedLabels() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + configLabel("bar", LabelFunction.NO_OP); + configLabel("baz", LabelFunction.NO_OP); + + List<LabelDefinitionInfo> labels = + gApi.projects().name(project.get()).labels().withInherited(true).get(); + assertThat(labelNames(labels)).containsExactly("Code-Review", "bar", "baz", "foo").inOrder(); + + LabelAssert.assertCodeReviewLabel(labels.get(0)); + assertThat(labels.get(1).name).isEqualTo("bar"); + assertThat(labels.get(1).projectName).isEqualTo(project.get()); + assertThat(labels.get(2).name).isEqualTo("baz"); + assertThat(labels.get(2).projectName).isEqualTo(project.get()); + assertThat(labels.get(3).name).isEqualTo("foo"); + assertThat(labels.get(3).projectName).isEqualTo(project.get()); + } + + @Test + public void withInheritedLabelsAndOverriddenLabel() throws Exception { + configLabel("Code-Review", LabelFunction.NO_OP); + + List<LabelDefinitionInfo> labels = + gApi.projects().name(project.get()).labels().withInherited(true).get(); + assertThat(labelNames(labels)).containsExactly("Code-Review", "Code-Review"); + + LabelAssert.assertCodeReviewLabel(labels.get(0)); + assertThat(labels.get(1).name).isEqualTo("Code-Review"); + assertThat(labels.get(1).projectName).isEqualTo(project.get()); + assertThat(labels.get(1).function).isEqualTo(LabelFunction.NO_OP.getFunctionName()); + } + + @Test + public void withInheritedLabelsFromMultipleParents() throws Exception { + configLabel(project, "foo", LabelFunction.NO_OP); + + Project.NameKey childProject = + projectOperations.newProject().name("child").parent(project).create(); + configLabel(childProject, "bar", LabelFunction.NO_OP); + + List<LabelDefinitionInfo> labels = + gApi.projects().name(childProject.get()).labels().withInherited(true).get(); + assertThat(labelNames(labels)).containsExactly("Code-Review", "foo", "bar").inOrder(); + + LabelAssert.assertCodeReviewLabel(labels.get(0)); + assertThat(labels.get(1).name).isEqualTo("foo"); + assertThat(labels.get(1).projectName).isEqualTo(project.get()); + assertThat(labels.get(2).name).isEqualTo("bar"); + assertThat(labels.get(2).projectName).isEqualTo(childProject.get()); + } + + private static List<String> labelNames(List<LabelDefinitionInfo> labels) { + return labels.stream().map(l -> l.name).collect(toList()); + } +}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java index 29d3eb2..bb08267 100644 --- a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java +++ b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -26,10 +26,10 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.Sandboxed; import com.google.gerrit.acceptance.TestProjectInput; +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.common.data.Permission;
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/PostLabelsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/PostLabelsIT.java new file mode 100644 index 0000000..9e6b051 --- /dev/null +++ b/javatests/com/google/gerrit/acceptance/rest/project/PostLabelsIT.java
@@ -0,0 +1,456 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.rest.project; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; +import static com.google.gerrit.testing.GerritJUnit.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; +import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; +import com.google.gerrit.common.data.LabelFunction; +import com.google.gerrit.common.data.Permission; +import com.google.gerrit.entities.RefNames; +import com.google.gerrit.extensions.common.BatchLabelInput; +import com.google.gerrit.extensions.common.LabelDefinitionInfo; +import com.google.gerrit.extensions.common.LabelDefinitionInput; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.UnprocessableEntityException; +import com.google.gerrit.server.restapi.project.PostLabels; +import com.google.inject.Inject; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Test; + +/** Tests for the {@link PostLabels} REST endpoint. */ +public class PostLabelsIT extends AbstractDaemonTest { + @Inject private RequestScopeOperations requestScopeOperations; + @Inject private ProjectOperations projectOperations; + + @Test + public void anonymous() throws Exception { + requestScopeOperations.setApiUserAnonymous(); + AuthException thrown = + assertThrows( + AuthException.class, + () -> gApi.projects().name(allProjects.get()).labels(new BatchLabelInput())); + assertThat(thrown).hasMessageThat().contains("Authentication required"); + } + + @Test + public void notAllowed() throws Exception { + projectOperations + .project(allProjects) + .forUpdate() + .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS)) + .update(); + + requestScopeOperations.setApiUser(user.id()); + AuthException thrown = + assertThrows( + AuthException.class, + () -> gApi.projects().name(allProjects.get()).labels(new BatchLabelInput())); + assertThat(thrown).hasMessageThat().contains("write refs/meta/config not permitted"); + } + + @Test + public void deleteNonExistingLabel() throws Exception { + BatchLabelInput input = new BatchLabelInput(); + input.delete = ImmutableList.of("Foo"); + + UnprocessableEntityException thrown = + assertThrows( + UnprocessableEntityException.class, + () -> gApi.projects().name(allProjects.get()).labels(input)); + assertThat(thrown).hasMessageThat().contains("label Foo not found"); + } + + @Test + public void deleteLabels() throws Exception { + configLabel("Foo", LabelFunction.NO_OP); + configLabel("Bar", LabelFunction.NO_OP); + assertThat(gApi.projects().name(project.get()).labels().get()).isNotEmpty(); + + BatchLabelInput input = new BatchLabelInput(); + input.delete = ImmutableList.of("Foo", "Bar"); + gApi.projects().name(project.get()).labels(input); + assertThat(gApi.projects().name(project.get()).labels().get()).isEmpty(); + } + + @Test + public void deleteLabels_labelNamesAreTrimmed() throws Exception { + configLabel("Foo", LabelFunction.NO_OP); + configLabel("Bar", LabelFunction.NO_OP); + assertThat(gApi.projects().name(project.get()).labels().get()).isNotEmpty(); + + BatchLabelInput input = new BatchLabelInput(); + input.delete = ImmutableList.of(" Foo ", " Bar "); + gApi.projects().name(project.get()).labels(input); + assertThat(gApi.projects().name(project.get()).labels().get()).isEmpty(); + } + + @Test + public void cannotDeleteTheSameLabelTwice() throws Exception { + configLabel("Foo", LabelFunction.NO_OP); + + BatchLabelInput input = new BatchLabelInput(); + input.delete = ImmutableList.of("Foo", "Foo"); + + UnprocessableEntityException thrown = + assertThrows( + UnprocessableEntityException.class, + () -> gApi.projects().name(project.get()).labels(input)); + assertThat(thrown).hasMessageThat().contains("label Foo not found"); + } + + @Test + public void cannotCreateLabelWithNameThatIsAlreadyInUse() throws Exception { + LabelDefinitionInput labelInput = new LabelDefinitionInput(); + labelInput.name = "Code-Review"; + BatchLabelInput input = new BatchLabelInput(); + input.create = ImmutableList.of(labelInput); + + ResourceConflictException thrown = + assertThrows( + ResourceConflictException.class, + () -> gApi.projects().name(allProjects.get()).labels(input)); + assertThat(thrown).hasMessageThat().contains("label Code-Review already exists"); + } + + @Test + public void cannotCreateTwoLabelsWithTheSameName() throws Exception { + LabelDefinitionInput fooInput = new LabelDefinitionInput(); + fooInput.name = "Foo"; + fooInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + BatchLabelInput input = new BatchLabelInput(); + input.create = ImmutableList.of(fooInput, fooInput); + + ResourceConflictException thrown = + assertThrows( + ResourceConflictException.class, + () -> gApi.projects().name(project.get()).labels(input)); + assertThat(thrown).hasMessageThat().contains("label Foo already exists"); + } + + @Test + public void cannotCreateTwoLabelsWithNamesThatAreTheSameAfterTrim() throws Exception { + LabelDefinitionInput foo1Input = new LabelDefinitionInput(); + foo1Input.name = "Foo"; + foo1Input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + LabelDefinitionInput foo2Input = new LabelDefinitionInput(); + foo2Input.name = " Foo "; + foo2Input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + BatchLabelInput input = new BatchLabelInput(); + input.create = ImmutableList.of(foo1Input, foo2Input); + + ResourceConflictException thrown = + assertThrows( + ResourceConflictException.class, + () -> gApi.projects().name(project.get()).labels(input)); + assertThat(thrown).hasMessageThat().contains("label Foo already exists"); + } + + @Test + public void cannotCreateTwoLabelsWithConflictingNames() throws Exception { + LabelDefinitionInput foo1Input = new LabelDefinitionInput(); + foo1Input.name = "Foo"; + foo1Input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + LabelDefinitionInput foo2Input = new LabelDefinitionInput(); + foo2Input.name = "foo"; + foo2Input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + BatchLabelInput input = new BatchLabelInput(); + input.create = ImmutableList.of(foo1Input, foo2Input); + + ResourceConflictException thrown = + assertThrows( + ResourceConflictException.class, + () -> gApi.projects().name(project.get()).labels(input)); + assertThat(thrown).hasMessageThat().contains("label foo conflicts with existing label Foo"); + } + + @Test + public void createLabels() throws Exception { + LabelDefinitionInput fooInput = new LabelDefinitionInput(); + fooInput.name = "Foo"; + fooInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + LabelDefinitionInput barInput = new LabelDefinitionInput(); + barInput.name = "Bar"; + barInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + BatchLabelInput input = new BatchLabelInput(); + input.create = ImmutableList.of(fooInput, barInput); + + gApi.projects().name(allProjects.get()).labels(input); + assertThat(gApi.projects().name(allProjects.get()).label("Foo").get()).isNotNull(); + assertThat(gApi.projects().name(allProjects.get()).label("Bar").get()).isNotNull(); + } + + @Test + public void createLabels_labelNamesAreTrimmed() throws Exception { + LabelDefinitionInput fooInput = new LabelDefinitionInput(); + fooInput.name = " Foo "; + fooInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + LabelDefinitionInput barInput = new LabelDefinitionInput(); + barInput.name = " Bar "; + barInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + BatchLabelInput input = new BatchLabelInput(); + input.create = ImmutableList.of(fooInput, barInput); + + gApi.projects().name(allProjects.get()).labels(input); + assertThat(gApi.projects().name(allProjects.get()).label("Foo").get()).isNotNull(); + assertThat(gApi.projects().name(allProjects.get()).label("Bar").get()).isNotNull(); + } + + @Test + public void cannotCreateLabelWithoutName() throws Exception { + BatchLabelInput input = new BatchLabelInput(); + input.create = ImmutableList.of(new LabelDefinitionInput()); + + BadRequestException thrown = + assertThrows( + BadRequestException.class, () -> gApi.projects().name(allProjects.get()).labels(input)); + assertThat(thrown).hasMessageThat().contains("label name is required for new label"); + } + + @Test + public void cannotSetCommitMessageOnLabelDefinitionInputForCreate() throws Exception { + LabelDefinitionInput labelInput = new LabelDefinitionInput(); + labelInput.name = "Foo"; + labelInput.commitMessage = "Create Label Foo"; + + BatchLabelInput input = new BatchLabelInput(); + input.create = ImmutableList.of(labelInput); + + BadRequestException thrown = + assertThrows( + BadRequestException.class, () -> gApi.projects().name(allProjects.get()).labels(input)); + assertThat(thrown) + .hasMessageThat() + .contains("commit message on label definition input not supported"); + } + + @Test + public void updateNonExistingLabel() throws Exception { + BatchLabelInput input = new BatchLabelInput(); + input.update = ImmutableMap.of("Foo", new LabelDefinitionInput()); + + UnprocessableEntityException thrown = + assertThrows( + UnprocessableEntityException.class, + () -> gApi.projects().name(allProjects.get()).labels(input)); + assertThat(thrown).hasMessageThat().contains("label Foo not found"); + } + + @Test + public void updateLabels() throws Exception { + configLabel("Foo", LabelFunction.NO_OP); + configLabel("Bar", LabelFunction.NO_OP); + + LabelDefinitionInput fooUpdate = new LabelDefinitionInput(); + fooUpdate.function = LabelFunction.MAX_WITH_BLOCK.getFunctionName(); + LabelDefinitionInput barUpdate = new LabelDefinitionInput(); + barUpdate.name = "Baz"; + + BatchLabelInput input = new BatchLabelInput(); + input.update = ImmutableMap.of("Foo", fooUpdate, "Bar", barUpdate); + + gApi.projects().name(project.get()).labels(input); + + assertThat(gApi.projects().name(project.get()).label("Foo").get().function) + .isEqualTo(fooUpdate.function); + assertThat(gApi.projects().name(project.get()).label("Baz").get()).isNotNull(); + assertThrows( + ResourceNotFoundException.class, + () -> gApi.projects().name(project.get()).label("Bar").get()); + } + + @Test + public void updateLabels_labelNamesAreTrimmed() throws Exception { + configLabel("Foo", LabelFunction.NO_OP); + configLabel("Bar", LabelFunction.NO_OP); + + LabelDefinitionInput fooUpdate = new LabelDefinitionInput(); + fooUpdate.function = LabelFunction.MAX_WITH_BLOCK.getFunctionName(); + LabelDefinitionInput barUpdate = new LabelDefinitionInput(); + barUpdate.name = "Baz"; + + BatchLabelInput input = new BatchLabelInput(); + input.update = ImmutableMap.of(" Foo ", fooUpdate, " Bar ", barUpdate); + + gApi.projects().name(project.get()).labels(input); + + assertThat(gApi.projects().name(project.get()).label("Foo").get().function) + .isEqualTo(fooUpdate.function); + assertThat(gApi.projects().name(project.get()).label("Baz").get()).isNotNull(); + assertThrows( + ResourceNotFoundException.class, + () -> gApi.projects().name(project.get()).label("Bar").get()); + } + + @Test + public void cannotSetCommitMessageOnLabelDefinitionInputForUpdate() throws Exception { + LabelDefinitionInput labelInput = new LabelDefinitionInput(); + labelInput.commitMessage = "Update label"; + + BatchLabelInput input = new BatchLabelInput(); + input.update = ImmutableMap.of("Code-Review", labelInput); + + BadRequestException thrown = + assertThrows( + BadRequestException.class, () -> gApi.projects().name(allProjects.get()).labels(input)); + assertThat(thrown) + .hasMessageThat() + .contains("commit message on label definition input not supported"); + } + + @Test + public void deleteAndRecreateLabel() throws Exception { + configLabel("Foo", LabelFunction.NO_OP); + + LabelDefinitionInput fooInput = new LabelDefinitionInput(); + fooInput.name = "Foo"; + fooInput.function = LabelFunction.MAX_NO_BLOCK.getFunctionName(); + fooInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + BatchLabelInput input = new BatchLabelInput(); + input.delete = ImmutableList.of("Foo"); + input.create = ImmutableList.of(fooInput); + + gApi.projects().name(project.get()).labels(input); + + LabelDefinitionInfo fooLabel = gApi.projects().name(project.get()).label("Foo").get(); + assertThat(fooLabel.function).isEqualTo(fooInput.function); + } + + @Test + public void deleteRecreateAndUpdateLabel() throws Exception { + configLabel("Foo", LabelFunction.NO_OP); + + LabelDefinitionInput fooCreateInput = new LabelDefinitionInput(); + fooCreateInput.name = "Foo"; + fooCreateInput.function = LabelFunction.MAX_NO_BLOCK.getFunctionName(); + fooCreateInput.values = + ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + LabelDefinitionInput fooUpdateInput = new LabelDefinitionInput(); + fooUpdateInput.function = LabelFunction.ANY_WITH_BLOCK.getFunctionName(); + + BatchLabelInput input = new BatchLabelInput(); + input.delete = ImmutableList.of("Foo"); + input.create = ImmutableList.of(fooCreateInput); + input.update = ImmutableMap.of("Foo", fooUpdateInput); + + gApi.projects().name(project.get()).labels(input); + + LabelDefinitionInfo fooLabel = gApi.projects().name(project.get()).label("Foo").get(); + assertThat(fooLabel.function).isEqualTo(fooUpdateInput.function); + } + + @Test + public void cannotDeleteAndUpdateLabel() throws Exception { + configLabel("Foo", LabelFunction.NO_OP); + + LabelDefinitionInput fooInput = new LabelDefinitionInput(); + fooInput.function = LabelFunction.MAX_NO_BLOCK.getFunctionName(); + + BatchLabelInput input = new BatchLabelInput(); + input.delete = ImmutableList.of("Foo"); + input.update = ImmutableMap.of("Foo", fooInput); + + UnprocessableEntityException thrown = + assertThrows( + UnprocessableEntityException.class, + () -> gApi.projects().name(project.get()).labels(input)); + assertThat(thrown).hasMessageThat().contains("label Foo not found"); + } + + @Test + public void createAndUpdateLabel() throws Exception { + LabelDefinitionInput fooCreateInput = new LabelDefinitionInput(); + fooCreateInput.name = "Foo"; + fooCreateInput.function = LabelFunction.MAX_NO_BLOCK.getFunctionName(); + fooCreateInput.values = + ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + LabelDefinitionInput fooUpdateInput = new LabelDefinitionInput(); + fooUpdateInput.function = LabelFunction.ANY_WITH_BLOCK.getFunctionName(); + + BatchLabelInput input = new BatchLabelInput(); + input.create = ImmutableList.of(fooCreateInput); + input.update = ImmutableMap.of("Foo", fooUpdateInput); + + gApi.projects().name(project.get()).labels(input); + + LabelDefinitionInfo fooLabel = gApi.projects().name(project.get()).label("Foo").get(); + assertThat(fooLabel.function).isEqualTo(fooUpdateInput.function); + } + + @Test + public void noOpUpdate() throws Exception { + RevCommit refsMetaConfigHead = + projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG); + + gApi.projects().name(allProjects.get()).labels(new BatchLabelInput()); + + assertThat(projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG)) + .isEqualTo(refsMetaConfigHead); + } + + @Test + public void defaultCommitMessage() throws Exception { + BatchLabelInput input = new BatchLabelInput(); + input.delete = ImmutableList.of("Code-Review"); + gApi.projects().name(allProjects.get()).labels(input); + assertThat( + projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage()) + .isEqualTo("Update labels"); + } + + @Test + public void withCommitMessage() throws Exception { + BatchLabelInput input = new BatchLabelInput(); + input.commitMessage = "Batch Update Labels"; + input.delete = ImmutableList.of("Code-Review"); + gApi.projects().name(allProjects.get()).labels(input); + assertThat( + projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage()) + .isEqualTo(input.commitMessage); + } + + @Test + public void commitMessageIsTrimmed() throws Exception { + BatchLabelInput input = new BatchLabelInput(); + input.commitMessage = " Batch Update Labels "; + input.delete = ImmutableList.of("Code-Review"); + gApi.projects().name(allProjects.get()).labels(input); + assertThat( + projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage()) + .isEqualTo("Batch Update Labels"); + } +}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java b/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java new file mode 100644 index 0000000..97b795f --- /dev/null +++ b/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java
@@ -0,0 +1,895 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.rest.project; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; +import static com.google.gerrit.testing.GerritJUnit.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; +import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; +import com.google.gerrit.common.data.LabelFunction; +import com.google.gerrit.common.data.LabelType; +import com.google.gerrit.common.data.Permission; +import com.google.gerrit.entities.RefNames; +import com.google.gerrit.extensions.common.LabelDefinitionInfo; +import com.google.gerrit.extensions.common.LabelDefinitionInput; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.inject.Inject; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Test; + +@NoHttpd +public class SetLabelIT extends AbstractDaemonTest { + @Inject private RequestScopeOperations requestScopeOperations; + @Inject private ProjectOperations projectOperations; + + @Test + public void anonymous() throws Exception { + requestScopeOperations.setApiUserAnonymous(); + AuthException thrown = + assertThrows( + AuthException.class, + () -> + gApi.projects() + .name(allProjects.get()) + .label("Code-Review") + .update(new LabelDefinitionInput())); + assertThat(thrown).hasMessageThat().contains("Authentication required"); + } + + @Test + public void notAllowed() throws Exception { + projectOperations + .project(allProjects) + .forUpdate() + .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS)) + .update(); + + requestScopeOperations.setApiUser(user.id()); + AuthException thrown = + assertThrows( + AuthException.class, + () -> + gApi.projects() + .name(allProjects.get()) + .label("Code-Review") + .update(new LabelDefinitionInput())); + assertThat(thrown).hasMessageThat().contains("write refs/meta/config not permitted"); + } + + @Test + public void updateName() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.name = "Foo-Review"; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(allProjects.get()).label("Code-Review").update(input); + assertThat(updatedLabel.name).isEqualTo(input.name); + + assertThat(gApi.projects().name(allProjects.get()).label("Foo-Review").get()).isNotNull(); + assertThrows( + ResourceNotFoundException.class, + () -> gApi.projects().name(allProjects.get()).label("Code-Review").get()); + } + + @Test + public void nameIsTrimmed() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.name = " Foo-Review "; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(allProjects.get()).label("Code-Review").update(input); + assertThat(updatedLabel.name).isEqualTo("Foo-Review"); + + assertThat(gApi.projects().name(allProjects.get()).label("Foo-Review").get()).isNotNull(); + assertThrows( + ResourceNotFoundException.class, + () -> gApi.projects().name(allProjects.get()).label("Code-Review").get()); + } + + @Test + public void cannotSetEmptyName() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.name = ""; + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(allProjects.get()).label("Code-Review").update(input)); + assertThat(thrown).hasMessageThat().contains("name cannot be empty"); + } + + @Test + public void cannotSetInvalidName() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.name = "INVALID_NAME"; + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(allProjects.get()).label("Code-Review").update(input)); + assertThat(thrown).hasMessageThat().contains("invalid name: " + input.name); + } + + @Test + public void cannotSetNameIfNameClashes() throws Exception { + configLabel("Foo-Review", LabelFunction.NO_OP); + configLabel("Bar-Review", LabelFunction.NO_OP); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.name = "Bar-Review"; + + ResourceConflictException thrown = + assertThrows( + ResourceConflictException.class, + () -> gApi.projects().name(project.get()).label("Foo-Review").update(input)); + assertThat(thrown).hasMessageThat().contains("name " + input.name + " already in use"); + } + + @Test + public void cannotSetNameIfNameConflicts() throws Exception { + configLabel("Foo-Review", LabelFunction.NO_OP); + configLabel("Bar-Review", LabelFunction.NO_OP); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.name = "bar-review"; + + ResourceConflictException thrown = + assertThrows( + ResourceConflictException.class, + () -> gApi.projects().name(project.get()).label("Foo-Review").update(input)); + assertThat(thrown) + .hasMessageThat() + .contains("name bar-review conflicts with existing label Bar-Review"); + } + + @Test + public void updateFunction() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.function = LabelFunction.NO_OP.getFunctionName(); + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(allProjects.get()).label("Code-Review").update(input); + assertThat(updatedLabel.function).isEqualTo(input.function); + + assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().function) + .isEqualTo(input.function); + } + + @Test + public void functionIsTrimmed() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.function = " " + LabelFunction.NO_OP.getFunctionName() + " "; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(allProjects.get()).label("Code-Review").update(input); + assertThat(updatedLabel.function).isEqualTo(LabelFunction.NO_OP.getFunctionName()); + + assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().function) + .isEqualTo(LabelFunction.NO_OP.getFunctionName()); + } + + @Test + public void cannotSetEmptyFunction() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.function = ""; + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(allProjects.get()).label("Code-Review").update(input)); + assertThat(thrown).hasMessageThat().contains("function cannot be empty"); + } + + @Test + public void cannotSetUnknownFunction() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.function = "UnknownFunction"; + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(allProjects.get()).label("Code-Review").update(input)); + assertThat(thrown).hasMessageThat().contains("unknown function: " + input.function); + } + + @Test + public void cannotSetEmptyValues() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of(); + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(allProjects.get()).label("Code-Review").update(input)); + assertThat(thrown).hasMessageThat().contains("values cannot be empty"); + } + + @Test + public void updateValues() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + // Positive values can be specified as '<value>' or '+<value>'. + input.values = + ImmutableMap.of( + "2", + "Looks Very Good", + "+1", + "Looks Good", + "0", + "Don't Know", + "-1", + "Looks Bad", + "-2", + "Looks Very Bad"); + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(allProjects.get()).label("Code-Review").update(input); + assertThat(updatedLabel.values) + .containsExactly( + "+2", "Looks Very Good", + "+1", "Looks Good", + " 0", "Don't Know", + "-1", "Looks Bad", + "-2", "Looks Very Bad"); + + assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().values) + .containsExactly( + "+2", "Looks Very Good", + "+1", "Looks Good", + " 0", "Don't Know", + "-1", "Looks Bad", + "-2", "Looks Very Bad"); + } + + @Test + public void valuesAndDescriptionsAreTrimmed() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + // Positive values can be specified as '<value>' or '+<value>'. + input.values = + ImmutableMap.of( + " 2 ", + " Looks Very Good ", + " +1 ", + " Looks Good ", + " 0 ", + " Don't Know ", + " -1 ", + " Looks Bad ", + " -2 ", + " Looks Very Bad "); + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(allProjects.get()).label("Code-Review").update(input); + assertThat(updatedLabel.values) + .containsExactly( + "+2", "Looks Very Good", + "+1", "Looks Good", + " 0", "Don't Know", + "-1", "Looks Bad", + "-2", "Looks Very Bad"); + + assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().values) + .containsExactly( + "+2", "Looks Very Good", + "+1", "Looks Good", + " 0", "Don't Know", + "-1", "Looks Bad", + "-2", "Looks Very Bad"); + } + + @Test + public void cannotSetInvalidValues() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("invalidValue", "description"); + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(allProjects.get()).label("Code-Review").update(input)); + assertThat(thrown).hasMessageThat().contains("invalid value: invalidValue"); + } + + @Test + public void cannotSetValueWithEmptyDescription() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", ""); + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(allProjects.get()).label("Code-Review").update(input)); + assertThat(thrown).hasMessageThat().contains("description for value '+1' cannot be empty"); + } + + @Test + public void cannotSetDuplicateValues() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + // Positive values can be specified as '<value>' or '+<value>'. + input.values = + ImmutableMap.of( + "+1", "Looks Good", "1", "Looks Good", "0", "Don't Know", "-1", "Looks Bad"); + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(allProjects.get()).label("Code-Review").update(input)); + assertThat(thrown).hasMessageThat().contains("duplicate value: 1"); + } + + @Test + public void updateDefaultValue() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.defaultValue = 1; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(allProjects.get()).label("Code-Review").update(input); + assertThat(updatedLabel.defaultValue).isEqualTo(input.defaultValue); + + assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().defaultValue) + .isEqualTo(input.defaultValue); + } + + @Test + public void cannotSetInvalidDefaultValue() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.defaultValue = 5; + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(allProjects.get()).label("Code-Review").update(input)); + assertThat(thrown).hasMessageThat().contains("invalid default value: " + input.defaultValue); + } + + @Test + public void updateBranches() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + // Branches can be full ref, ref pattern or regular expression. + input.branches = + ImmutableList.of("refs/heads/master", "refs/heads/foo/*", "^refs/heads/stable-.*"); + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(allProjects.get()).label("Code-Review").update(input); + assertThat(updatedLabel.branches).containsExactlyElementsIn(input.branches); + + assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().branches) + .containsExactlyElementsIn(input.branches); + } + + @Test + public void branchesAreTrimmed() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.branches = + ImmutableList.of(" refs/heads/master ", " refs/heads/foo/* ", " ^refs/heads/stable-.* "); + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(allProjects.get()).label("Code-Review").update(input); + assertThat(updatedLabel.branches) + .containsExactly("refs/heads/master", "refs/heads/foo/*", "^refs/heads/stable-.*"); + + assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().branches) + .containsExactly("refs/heads/master", "refs/heads/foo/*", "^refs/heads/stable-.*"); + } + + @Test + public void emptyBranchesAreIgnored() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.branches = ImmutableList.of("refs/heads/master", "", " "); + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(allProjects.get()).label("Code-Review").update(input); + assertThat(updatedLabel.branches).containsExactly("refs/heads/master"); + + assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().branches) + .containsExactly("refs/heads/master"); + } + + @Test + public void branchesCanBeUnset() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.branches = ImmutableList.of("refs/heads/master"); + gApi.projects().name(allProjects.get()).label("Code-Review").update(input); + assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().branches) + .isNotNull(); + + input.branches = ImmutableList.of(); + LabelDefinitionInfo updatedLabel = + gApi.projects().name(allProjects.get()).label("Code-Review").update(input); + assertThat(updatedLabel.branches).isNull(); + assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().branches) + .isNull(); + } + + @Test + public void cannotSetInvalidBranch() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.branches = ImmutableList.of("refs heads master"); + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(allProjects.get()).label("Code-Review").update(input)); + assertThat(thrown).hasMessageThat().contains("invalid branch: refs heads master"); + } + + @Test + public void branchesAreAutomaticallyPrefixedWithRefsHeads() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.branches = ImmutableList.of("master", "refs/meta/config"); + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(allProjects.get()).label("Code-Review").update(input); + assertThat(updatedLabel.branches).containsExactly("refs/heads/master", "refs/meta/config"); + + assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().branches) + .containsExactly("refs/heads/master", "refs/meta/config"); + } + + @Test + public void setCanOverride() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + try (ProjectConfigUpdate u = updateProject(project)) { + LabelType labelType = u.getConfig().getLabelSections().get("foo"); + labelType.setCanOverride(false); + u.getConfig().getLabelSections().put(labelType.getName(), labelType); + u.save(); + } + assertThat(gApi.projects().name(project.get()).label("foo").get().canOverride).isNull(); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.canOverride = true; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(project.get()).label("foo").update(input); + assertThat(updatedLabel.canOverride).isTrue(); + + assertThat(gApi.projects().name(project.get()).label("foo").get().canOverride).isTrue(); + } + + @Test + public void unsetCanOverride() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + assertThat(gApi.projects().name(project.get()).label("foo").get().canOverride).isTrue(); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.canOverride = false; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(project.get()).label("foo").update(input); + assertThat(updatedLabel.canOverride).isNull(); + + assertThat(gApi.projects().name(project.get()).label("foo").get().canOverride).isNull(); + } + + @Test + public void setCopyAnyScore() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + assertThat(gApi.projects().name(project.get()).label("foo").get().copyAnyScore).isNull(); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.copyAnyScore = true; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(project.get()).label("foo").update(input); + assertThat(updatedLabel.copyAnyScore).isTrue(); + + assertThat(gApi.projects().name(project.get()).label("foo").get().copyAnyScore).isTrue(); + } + + @Test + public void unsetCopyAnyScore() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + try (ProjectConfigUpdate u = updateProject(project)) { + LabelType labelType = u.getConfig().getLabelSections().get("foo"); + labelType.setCopyAnyScore(true); + u.getConfig().getLabelSections().put(labelType.getName(), labelType); + u.save(); + } + assertThat(gApi.projects().name(project.get()).label("foo").get().copyAnyScore).isTrue(); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.copyAnyScore = false; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(project.get()).label("foo").update(input); + assertThat(updatedLabel.copyAnyScore).isNull(); + + assertThat(gApi.projects().name(project.get()).label("foo").get().copyAnyScore).isNull(); + } + + @Test + public void setCopyMinScore() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + assertThat(gApi.projects().name(project.get()).label("foo").get().copyMinScore).isNull(); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.copyMinScore = true; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(project.get()).label("foo").update(input); + assertThat(updatedLabel.copyMinScore).isTrue(); + + assertThat(gApi.projects().name(project.get()).label("foo").get().copyMinScore).isTrue(); + } + + @Test + public void unsetCopyMinScore() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + try (ProjectConfigUpdate u = updateProject(project)) { + LabelType labelType = u.getConfig().getLabelSections().get("foo"); + labelType.setCopyMinScore(true); + u.getConfig().getLabelSections().put(labelType.getName(), labelType); + u.save(); + } + assertThat(gApi.projects().name(project.get()).label("foo").get().copyMinScore).isTrue(); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.copyMinScore = false; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(project.get()).label("foo").update(input); + assertThat(updatedLabel.copyMinScore).isNull(); + + assertThat(gApi.projects().name(project.get()).label("foo").get().copyMinScore).isNull(); + } + + @Test + public void setCopyMaxScore() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + assertThat(gApi.projects().name(project.get()).label("foo").get().copyMaxScore).isNull(); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.copyMaxScore = true; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(project.get()).label("foo").update(input); + assertThat(updatedLabel.copyMaxScore).isTrue(); + + assertThat(gApi.projects().name(project.get()).label("foo").get().copyMaxScore).isTrue(); + } + + @Test + public void unsetCopyMaxScore() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + try (ProjectConfigUpdate u = updateProject(project)) { + LabelType labelType = u.getConfig().getLabelSections().get("foo"); + labelType.setCopyMaxScore(true); + u.getConfig().getLabelSections().put(labelType.getName(), labelType); + u.save(); + } + assertThat(gApi.projects().name(project.get()).label("foo").get().copyMaxScore).isTrue(); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.copyMaxScore = false; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(project.get()).label("foo").update(input); + assertThat(updatedLabel.copyMaxScore).isNull(); + + assertThat(gApi.projects().name(project.get()).label("foo").get().copyMaxScore).isNull(); + } + + @Test + public void setCopyAllScoresIfNoChange() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + try (ProjectConfigUpdate u = updateProject(project)) { + LabelType labelType = u.getConfig().getLabelSections().get("foo"); + labelType.setCopyAllScoresIfNoChange(false); + u.getConfig().getLabelSections().put(labelType.getName(), labelType); + u.save(); + } + assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresIfNoChange) + .isNull(); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.copyAllScoresIfNoChange = true; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(project.get()).label("foo").update(input); + assertThat(updatedLabel.copyAllScoresIfNoChange).isTrue(); + + assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresIfNoChange) + .isTrue(); + } + + @Test + public void unsetCopyAllScoresIfNoChange() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresIfNoChange) + .isTrue(); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.copyAllScoresIfNoChange = false; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(project.get()).label("foo").update(input); + assertThat(updatedLabel.copyAllScoresIfNoChange).isNull(); + + assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresIfNoChange) + .isNull(); + } + + @Test + public void setCopyAllScoresIfNoCodeChange() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresIfNoCodeChange) + .isNull(); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.copyAllScoresIfNoCodeChange = true; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(project.get()).label("foo").update(input); + assertThat(updatedLabel.copyAllScoresIfNoCodeChange).isTrue(); + + assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresIfNoCodeChange) + .isTrue(); + } + + @Test + public void unsetCopyAllScoresIfNoCodeChange() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + try (ProjectConfigUpdate u = updateProject(project)) { + LabelType labelType = u.getConfig().getLabelSections().get("foo"); + labelType.setCopyAllScoresIfNoCodeChange(true); + u.getConfig().getLabelSections().put(labelType.getName(), labelType); + u.save(); + } + assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresIfNoCodeChange) + .isTrue(); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.copyAllScoresIfNoCodeChange = false; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(project.get()).label("foo").update(input); + assertThat(updatedLabel.copyAllScoresIfNoCodeChange).isNull(); + + assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresIfNoCodeChange) + .isNull(); + } + + @Test + public void setCopyAllScoresOnTrivialRebase() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresOnTrivialRebase) + .isNull(); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.copyAllScoresOnTrivialRebase = true; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(project.get()).label("foo").update(input); + assertThat(updatedLabel.copyAllScoresOnTrivialRebase).isTrue(); + + assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresOnTrivialRebase) + .isTrue(); + } + + @Test + public void unsetCopyAllScoresOnTrivialRebase() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + try (ProjectConfigUpdate u = updateProject(project)) { + LabelType labelType = u.getConfig().getLabelSections().get("foo"); + labelType.setCopyAllScoresOnTrivialRebase(true); + u.getConfig().getLabelSections().put(labelType.getName(), labelType); + u.save(); + } + assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresOnTrivialRebase) + .isTrue(); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.copyAllScoresOnTrivialRebase = false; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(project.get()).label("foo").update(input); + assertThat(updatedLabel.copyAllScoresOnTrivialRebase).isNull(); + + assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresOnTrivialRebase) + .isNull(); + } + + @Test + public void setCopyAllScoresOnMergeFirstParentUpdate() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + assertThat( + gApi.projects() + .name(project.get()) + .label("foo") + .get() + .copyAllScoresOnMergeFirstParentUpdate) + .isNull(); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.copyAllScoresOnMergeFirstParentUpdate = true; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(project.get()).label("foo").update(input); + assertThat(updatedLabel.copyAllScoresOnMergeFirstParentUpdate).isTrue(); + + assertThat( + gApi.projects() + .name(project.get()) + .label("foo") + .get() + .copyAllScoresOnMergeFirstParentUpdate) + .isTrue(); + } + + @Test + public void unsetCopyAllScoresOnMergeFirstParentUpdate() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + try (ProjectConfigUpdate u = updateProject(project)) { + LabelType labelType = u.getConfig().getLabelSections().get("foo"); + labelType.setCopyAllScoresOnMergeFirstParentUpdate(true); + u.getConfig().getLabelSections().put(labelType.getName(), labelType); + u.save(); + } + assertThat( + gApi.projects() + .name(project.get()) + .label("foo") + .get() + .copyAllScoresOnMergeFirstParentUpdate) + .isTrue(); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.copyAllScoresOnMergeFirstParentUpdate = false; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(project.get()).label("foo").update(input); + assertThat(updatedLabel.copyAllScoresOnMergeFirstParentUpdate).isNull(); + + assertThat( + gApi.projects() + .name(project.get()) + .label("foo") + .get() + .copyAllScoresOnMergeFirstParentUpdate) + .isNull(); + } + + @Test + public void setAllowPostSubmit() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + try (ProjectConfigUpdate u = updateProject(project)) { + LabelType labelType = u.getConfig().getLabelSections().get("foo"); + labelType.setAllowPostSubmit(false); + u.getConfig().getLabelSections().put(labelType.getName(), labelType); + u.save(); + } + assertThat(gApi.projects().name(project.get()).label("foo").get().allowPostSubmit).isNull(); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.allowPostSubmit = true; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(project.get()).label("foo").update(input); + assertThat(updatedLabel.allowPostSubmit).isTrue(); + + assertThat(gApi.projects().name(project.get()).label("foo").get().allowPostSubmit).isTrue(); + } + + @Test + public void unsetAllowPostSubmit() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + assertThat(gApi.projects().name(project.get()).label("foo").get().allowPostSubmit).isTrue(); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.allowPostSubmit = false; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(project.get()).label("foo").update(input); + assertThat(updatedLabel.allowPostSubmit).isNull(); + + assertThat(gApi.projects().name(project.get()).label("foo").get().allowPostSubmit).isNull(); + } + + @Test + public void setIgnoreSelfApproval() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + assertThat(gApi.projects().name(project.get()).label("foo").get().ignoreSelfApproval).isNull(); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.ignoreSelfApproval = true; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(project.get()).label("foo").update(input); + assertThat(updatedLabel.ignoreSelfApproval).isTrue(); + + assertThat(gApi.projects().name(project.get()).label("foo").get().ignoreSelfApproval).isTrue(); + } + + @Test + public void unsetIgnoreSelfApproval() throws Exception { + configLabel("foo", LabelFunction.NO_OP); + try (ProjectConfigUpdate u = updateProject(project)) { + LabelType labelType = u.getConfig().getLabelSections().get("foo"); + labelType.setIgnoreSelfApproval(true); + u.getConfig().getLabelSections().put(labelType.getName(), labelType); + u.save(); + } + assertThat(gApi.projects().name(project.get()).label("foo").get().ignoreSelfApproval).isTrue(); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.ignoreSelfApproval = false; + + LabelDefinitionInfo updatedLabel = + gApi.projects().name(project.get()).label("foo").update(input); + assertThat(updatedLabel.ignoreSelfApproval).isNull(); + + assertThat(gApi.projects().name(project.get()).label("foo").get().ignoreSelfApproval).isNull(); + } + + @Test + public void noOpUpdate() throws Exception { + RevCommit refsMetaConfigHead = + projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG); + + LabelDefinitionInfo updatedLabel = + gApi.projects() + .name(allProjects.get()) + .label("Code-Review") + .update(new LabelDefinitionInput()); + LabelAssert.assertCodeReviewLabel(updatedLabel); + + LabelAssert.assertCodeReviewLabel( + gApi.projects().name(allProjects.get()).label("Code-Review").get()); + + assertThat(projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG)) + .isEqualTo(refsMetaConfigHead); + } + + @Test + public void defaultCommitMessage() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.function = LabelFunction.NO_OP.getFunctionName(); + gApi.projects().name(allProjects.get()).label("Code-Review").update(input); + assertThat( + projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage()) + .isEqualTo("Update label"); + } + + @Test + public void withCommitMessage() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.function = LabelFunction.NO_OP.getFunctionName(); + input.commitMessage = "Set NoOp function"; + gApi.projects().name(allProjects.get()).label("Code-Review").update(input); + assertThat( + projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage()) + .isEqualTo(input.commitMessage); + } + + @Test + public void commitMessageIsTrimmed() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.function = LabelFunction.NO_OP.getFunctionName(); + input.commitMessage = " Set NoOp function "; + gApi.projects().name(allProjects.get()).label("Code-Review").update(input); + assertThat( + projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage()) + .isEqualTo("Set NoOp function"); + } +}
diff --git a/javatests/com/google/gerrit/acceptance/rest/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/rest/revision/RevisionIT.java index 220254b..df6a264 100644 --- a/javatests/com/google/gerrit/acceptance/rest/revision/RevisionIT.java +++ b/javatests/com/google/gerrit/acceptance/rest/revision/RevisionIT.java
@@ -19,13 +19,13 @@ import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME; import static java.nio.charset.StandardCharsets.UTF_8; +import com.google.common.io.BaseEncoding; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.RestResponse; import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.common.ChangeInfo; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.util.Base64; import org.junit.Test; public class RevisionIT extends AbstractDaemonTest { @@ -50,7 +50,7 @@ + FILE_NAME + "/content?parent=1"); response.assertOK(); - assertThat(new String(Base64.decode(response.getEntityContent()), UTF_8)) + assertThat(new String(BaseEncoding.base64().decode(response.getEntityContent()), UTF_8)) .isEqualTo(parentContent); }
diff --git a/javatests/com/google/gerrit/acceptance/server/change/BUILD b/javatests/com/google/gerrit/acceptance/server/change/BUILD index 4d1634d..500ab06 100644 --- a/javatests/com/google/gerrit/acceptance/server/change/BUILD +++ b/javatests/com/google/gerrit/acceptance/server/change/BUILD
@@ -4,5 +4,8 @@ srcs = glob(["*IT.java"]), group = "server_change", labels = ["server"], - deps = ["//java/com/google/gerrit/server/util/time"], + deps = [ + "//java/com/google/gerrit/server/logging", + "//java/com/google/gerrit/server/util/time", + ], )
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java index b1194b1..ba41d7e 100644 --- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java +++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -29,6 +29,7 @@ import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; import com.google.gerrit.entities.Change; import com.google.gerrit.entities.Patch; @@ -83,6 +84,7 @@ @Inject private Provider<ChangesCollection> changes; @Inject private Provider<PostReview> postReview; @Inject private RequestScopeOperations requestScopeOperations; + @Inject ProjectOperations projectOperations; private final Integer[] lines = {0, 1}; @@ -485,7 +487,7 @@ ChangeResource changeRsrc = changes.get().parse(TopLevelResource.INSTANCE, IdString.fromDecoded(changeId)); RevisionResource revRsrc = revisions.parse(changeRsrc, IdString.fromDecoded(revId)); - postReview.get().apply(batchUpdateFactory, revRsrc, input, timestamp); + postReview.get().apply(revRsrc, input, timestamp); Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId); assertThat(result).isNotEmpty(); CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
diff --git a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java index 1e2d1ba..ee5f117 100644 --- a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java +++ b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -29,6 +29,7 @@ import com.google.gerrit.entities.Change; import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.RefNames; +import com.google.gerrit.entities.SubmissionId; import com.google.gerrit.extensions.api.changes.FixInput; import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.common.ChangeInfo; @@ -313,7 +314,8 @@ @Override public boolean updateChange(ChangeContext ctx) { ctx.getChange().setStatus(Change.Status.MERGED); - ctx.getUpdate(ctx.getChange().currentPatchSetId()).fixStatus(Change.Status.MERGED); + ctx.getUpdate(ctx.getChange().currentPatchSetId()) + .fixStatusToMerged(new SubmissionId(ctx.getChange())); return true; } }); @@ -862,7 +864,8 @@ @Override public boolean updateChange(ChangeContext ctx) { ctx.getChange().setStatus(Change.Status.MERGED); - ctx.getUpdate(ctx.getChange().currentPatchSetId()).fixStatus(Change.Status.MERGED); + ctx.getUpdate(ctx.getChange().currentPatchSetId()) + .fixStatusToMerged(new SubmissionId(ctx.getChange())); return true; } });
diff --git a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java index e369d1b..b544f6e 100644 --- a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java +++ b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -25,11 +25,11 @@ import com.google.common.collect.Iterables; import com.google.common.truth.Correspondence; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.UseClockStep; import com.google.gerrit.acceptance.UseTimezone; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.acceptance.testsuite.account.AccountOperations; import com.google.gerrit.acceptance.testsuite.group.GroupOperations; import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
diff --git a/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java b/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java index 445f787..a97fb49 100644 --- a/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java +++ b/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
@@ -117,12 +117,12 @@ // Create two independent commits and push. RevCommit c1_1 = commitBuilder().add("a.txt", "1").message("subject: 1").create(); String id1 = getChangeId(c1_1); - pushHead(testRepo, "refs/for/master/" + name("connectingTopic"), false); + pushHead(testRepo, "refs/for/master%topic=" + name("connectingTopic"), false); testRepo.reset(initialHead); RevCommit c2_1 = commitBuilder().add("b.txt", "2").message("subject: 2").create(); String id2 = getChangeId(c2_1); - pushHead(testRepo, "refs/for/master/" + name("connectingTopic"), false); + pushHead(testRepo, "refs/for/master%topic=" + name("connectingTopic"), false); if (isSubmitWholeTopicEnabled()) { assertSubmittedTogether(id1, id2, id1); @@ -137,12 +137,12 @@ public void anonymousWholeTopic() throws Exception { RevCommit initialHead = projectOperations.project(project).getHead("master"); RevCommit a = commitBuilder().add("a", "1").message("change 1").create(); - pushHead(testRepo, "refs/for/master/" + name("topic"), false); + pushHead(testRepo, "refs/for/master%topic=" + name("topic"), false); String id1 = getChangeId(a); testRepo.reset(initialHead); RevCommit b = commitBuilder().add("b", "1").message("change 2").create(); - pushHead(testRepo, "refs/for/master/" + name("topic"), false); + pushHead(testRepo, "refs/for/master%topic=" + name("topic"), false); String id2 = getChangeId(b); requestScopeOperations.setApiUserAnonymous(); @@ -161,16 +161,16 @@ RevCommit c1_1 = commitBuilder().add("a.txt", "1").message("subject: 1").create(); String id1 = getChangeId(c1_1); - pushHead(testRepo, "refs/for/master/" + name("connectingTopic"), false); + pushHead(testRepo, "refs/for/master%topic=" + name("connectingTopic"), false); testRepo.reset(initialHead); RevCommit c2_1 = commitBuilder().add("b.txt", "2").message("subject: 2").create(); String id2 = getChangeId(c2_1); - pushHead(testRepo, "refs/for/master/" + name("connectingTopic"), false); + pushHead(testRepo, "refs/for/master%topic=" + name("connectingTopic"), false); RevCommit c3_1 = commitBuilder().add("b.txt", "3").message("subject: 3").create(); String id3 = getChangeId(c3_1); - pushHead(testRepo, "refs/for/master/" + name("unrelated-topic"), false); + pushHead(testRepo, "refs/for/master%topic=" + name("unrelated-topic"), false); if (isSubmitWholeTopicEnabled()) { assertSubmittedTogether(id1, id2, id1); @@ -189,16 +189,16 @@ RevCommit c1_1 = commitBuilder().add("a.txt", "1").message("subject: 1").create(); String id1 = getChangeId(c1_1); - pushHead(testRepo, "refs/for/master/" + name("connectingTopic"), false); + pushHead(testRepo, "refs/for/master%topic=" + name("connectingTopic"), false); testRepo.reset(initialHead); RevCommit c2_1 = commitBuilder().add("b.txt", "2").message("subject: 2").create(); String id2 = getChangeId(c2_1); - pushHead(testRepo, "refs/for/master/" + name("otherConnectingTopic"), false); + pushHead(testRepo, "refs/for/master%topic=" + name("otherConnectingTopic"), false); RevCommit c3_1 = commitBuilder().add("b.txt", "3").message("subject: 3").create(); String id3 = getChangeId(c3_1); - pushHead(testRepo, "refs/for/master/" + name("connectingTopic"), false); + pushHead(testRepo, "refs/for/master%topic=" + name("connectingTopic"), false); RevCommit c4_1 = commitBuilder().add("b.txt", "4").message("subject: 4").create(); String id4 = getChangeId(c4_1); @@ -211,7 +211,7 @@ RevCommit c6_1 = commitBuilder().add("c.txt", "6").message("subject: 6").create(); String id6 = getChangeId(c6_1); - pushHead(testRepo, "refs/for/master/" + name("otherConnectingTopic"), false); + pushHead(testRepo, "refs/for/master%topic=" + name("otherConnectingTopic"), false); if (isSubmitWholeTopicEnabled()) { assertSubmittedTogether(id1, id6, id5, id3, id2, id1);
diff --git a/javatests/com/google/gerrit/acceptance/server/event/EventPayloadIT.java b/javatests/com/google/gerrit/acceptance/server/event/EventPayloadIT.java index 8744cfad..d973934 100644 --- a/javatests/com/google/gerrit/acceptance/server/event/EventPayloadIT.java +++ b/javatests/com/google/gerrit/acceptance/server/event/EventPayloadIT.java
@@ -19,8 +19,8 @@ import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.ExtensionRegistry; import com.google.gerrit.acceptance.ExtensionRegistry.Registration; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.extensions.events.RevisionCreatedListener; import com.google.inject.Inject; import org.junit.Test;
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/EmailValidatorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/EmailValidatorIT.java index d0b7f15d..cc61dfb 100644 --- a/javatests/com/google/gerrit/acceptance/server/mail/EmailValidatorIT.java +++ b/javatests/com/google/gerrit/acceptance/server/mail/EmailValidatorIT.java
@@ -19,7 +19,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.server.mail.send.OutgoingEmailValidator; import com.google.inject.Inject; import java.io.BufferedReader;
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java index 13f0416..e03f6fe 100644 --- a/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java +++ b/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java
@@ -16,8 +16,8 @@ import static com.google.common.truth.Truth.assertThat; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.ChangeMessageInfo; import com.google.gerrit.extensions.common.CommentInfo;
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java index c395c81..0ae9ad2 100644 --- a/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java +++ b/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
@@ -16,7 +16,7 @@ import static com.google.common.truth.Truth.assertThat; -import com.google.gerrit.acceptance.GerritConfig; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.mail.EmailHeader; import java.net.URI; import java.util.Map;
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java index c502c79..3c066a3 100644 --- a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java +++ b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
@@ -154,16 +154,20 @@ AtomicInteger afterUpdateReposCalledCount = new AtomicInteger(); String result = - retryHelper.execute( - batchUpdateFactory -> { - try (BatchUpdate bu = newBatchUpdate(batchUpdateFactory)) { - bu.addOp( - id, - new UpdateRefAndAddMessageOp(updateRepoCalledCount, updateChangeCalledCount)); - bu.execute(new ConcurrentWritingListener(afterUpdateReposCalledCount)); - } - return "Done"; - }); + retryHelper + .changeUpdate( + "testUpdateRefAndAddMessageOp", + batchUpdateFactory -> { + try (BatchUpdate bu = newBatchUpdate(batchUpdateFactory)) { + bu.addOp( + id, + new UpdateRefAndAddMessageOp( + updateRepoCalledCount, updateChangeCalledCount)); + bu.execute(new ConcurrentWritingListener(afterUpdateReposCalledCount)); + } + return "Done"; + }) + .call(); assertThat(result).isEqualTo("Done"); assertThat(updateRepoCalledCount.get()).isEqualTo(2);
diff --git a/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java b/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java index 4fe0df4..f866fff 100644 --- a/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java +++ b/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java
@@ -22,10 +22,10 @@ import com.google.gerrit.acceptance.ChangeIndexedCounter; import com.google.gerrit.acceptance.ExtensionRegistry; import com.google.gerrit.acceptance.ExtensionRegistry.Registration; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.UseSsh; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.server.query.change.ChangeData; import com.google.inject.Inject;
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java index 4e9c4a4..9b8b19c 100644 --- a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java +++ b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
@@ -67,6 +67,7 @@ "receive-pack", "rename-group", "review", + "sequence", "set-account", "set-head", "set-members", @@ -96,6 +97,7 @@ "gerrit plugin", ImmutableList.of("add", "enable", "install", "ls", "reload", "remove", "rm")) .put("gerrit test-submit", ImmutableList.of("rule", "type")) + .put("gerrit sequence", ImmutableList.of("set", "show")) .build(); private static final ImmutableMap<String, List<String>> SLAVE_COMMANDS =
diff --git a/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java b/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java index 34406e0..f1db016 100644 --- a/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java +++ b/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
@@ -19,11 +19,11 @@ import com.google.common.base.Splitter; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.PushOneCommit.Result; import com.google.gerrit.acceptance.UseSsh; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.git.ObjectIds; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream;
diff --git a/javatests/com/google/gerrit/index/query/RangeUtilTest.java b/javatests/com/google/gerrit/index/query/RangeUtilTest.java new file mode 100644 index 0000000..681f9d99b --- /dev/null +++ b/javatests/com/google/gerrit/index/query/RangeUtilTest.java
@@ -0,0 +1,34 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.index.query; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.gerrit.index.query.RangeUtil.Range; +import org.junit.Test; + +public class RangeUtilTest { + @Test + public void getRangeForValueOutsideOfMinMaxRange_minNotGreaterThanMax() { + for (String operator : ImmutableList.of("=", ">", ">=", "<", "<=")) { + Range range = RangeUtil.getRange("foo", operator, 10, -4, 4); + assertThat(range.min).isAtMost(range.max); + + range = RangeUtil.getRange("foo", operator, -10, -4, 4); + assertThat(range.min).isAtMost(range.max); + } + } +}
diff --git a/javatests/com/google/gerrit/mail/BUILD b/javatests/com/google/gerrit/mail/BUILD index bd2c478..3d63844 100644 --- a/javatests/com/google/gerrit/mail/BUILD +++ b/javatests/com/google/gerrit/mail/BUILD
@@ -15,7 +15,6 @@ "//lib:guava-retrying", "//lib:jgit", "//lib:jgit-junit", - "//lib/commons:codec", "//lib/guice", "//lib/truth", "//lib/truth:truth-java8-extension",
diff --git a/javatests/com/google/gerrit/prettify/BUILD b/javatests/com/google/gerrit/prettify/BUILD new file mode 100644 index 0000000..0eb7cee --- /dev/null +++ b/javatests/com/google/gerrit/prettify/BUILD
@@ -0,0 +1,13 @@ +load("//tools/bzl:junit.bzl", "junit_tests") + +junit_tests( + name = "prettify_tests", + srcs = glob(["**/*.java"]), + deps = [ + "//java/com/google/gerrit/prettify:server", + "//java/com/google/gerrit/prettify/common/testing", + "//java/com/google/gerrit/testing:gerrit-test-util", + "//lib:guava", + "//lib/truth", + ], +)
diff --git a/javatests/com/google/gerrit/prettify/common/SparseFileContentBuilderTest.java b/javatests/com/google/gerrit/prettify/common/SparseFileContentBuilderTest.java new file mode 100644 index 0000000..a751d50 --- /dev/null +++ b/javatests/com/google/gerrit/prettify/common/SparseFileContentBuilderTest.java
@@ -0,0 +1,170 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.prettify.common; + +import static com.google.gerrit.prettify.common.testing.SparseFileContentSubject.assertThat; +import static com.google.gerrit.testing.GerritJUnit.assertThrows; + +import com.google.common.collect.ImmutableMap; +import org.junit.Ignore; +import org.junit.Test; + +public class SparseFileContentBuilderTest { + + @Test + public void addLineWithNegativeNumber() { + SparseFileContentBuilder builder = new SparseFileContentBuilder(10); + assertThrows(IllegalArgumentException.class, () -> builder.addLine(-1, "First line")); + + assertThrows(IllegalArgumentException.class, () -> builder.addLine(-5, "First line")); + } + + @Test + @Ignore + public void addLineNumberZeroFileSize() { + // Temporary ignore - see comments in SparseFileContentBuilder.build() method + SparseFileContentBuilder builder = new SparseFileContentBuilder(0); + assertThrows(IllegalArgumentException.class, () -> builder.addLine(0, "First line")); + } + + @Test + @Ignore + public void addLineNumberNonZeroFileSize() { + // Temporary ignore - see comments in SparseFileContentBuilder.build() method + SparseFileContentBuilder builder = new SparseFileContentBuilder(5); + assertThrows(IllegalArgumentException.class, () -> builder.addLine(5, "First line")); + assertThrows(IllegalArgumentException.class, () -> builder.addLine(6, "First line")); + assertThrows(IllegalArgumentException.class, () -> builder.addLine(7, "First line")); + } + + @Test + public void addLineIncorrectOrder() { + SparseFileContentBuilder builder = new SparseFileContentBuilder(5); + + builder.addLine(0, "First line"); + builder.addLine(1, "Second line"); + builder.addLine(3, "Third line"); + builder.addLine(4, "Fourth line"); + assertThrows(IllegalArgumentException.class, () -> builder.addLine(4, "Other Line")); + + assertThrows(IllegalArgumentException.class, () -> builder.addLine(2, "Other Line")); + } + + @Test + public void emptyContentZeroSize() { + SparseFileContentBuilder builder = new SparseFileContentBuilder(0); + + SparseFileContent content = builder.build(); + assertThat(content).getSize().isEqualTo(0); + assertThat(content).getRangesCount().isEqualTo(0); + assertThat(content).lines().isEmpty(); + } + + @Test + public void emptyContentNonZeroSize() { + SparseFileContentBuilder builder = new SparseFileContentBuilder(4); + SparseFileContent content = builder.build(); + assertThat(content).getSize().isEqualTo(4); + assertThat(content).getRangesCount().isEqualTo(0); + assertThat(content).lines().isEmpty(); + } + + @Test + public void oneLineContentLineNumberZero() { + SparseFileContentBuilder builder = new SparseFileContentBuilder(1); + + builder.addLine(0, "First line"); + SparseFileContent content = builder.build(); + assertThat(content).getSize().isEqualTo(1); + assertThat(content).getRangesCount().isEqualTo(1); + assertThat(content).lines().containsExactlyEntriesIn(ImmutableMap.of(0, "First line")); + } + + @Test + public void oneLineContentLineNumberNotZero() { + SparseFileContentBuilder builder = new SparseFileContentBuilder(6); + + builder.addLine(5, "First line"); + SparseFileContent content = builder.build(); + assertThat(content).getSize().isEqualTo(6); + assertThat(content).getRangesCount().isEqualTo(1); + assertThat(content).lines().containsExactlyEntriesIn(ImmutableMap.of(5, "First line")); + } + + @Test + public void multiLineContinuousContentStartingFromZero() { + SparseFileContentBuilder builder = new SparseFileContentBuilder(5); + + builder.addLine(0, "First line"); + builder.addLine(1, "Second line"); + builder.addLine(2, "Third line"); + SparseFileContent content = builder.build(); + assertThat(content).getSize().isEqualTo(5); + assertThat(content).getRangesCount().isEqualTo(1); + assertThat(content) + .lines() + .containsExactlyEntriesIn( + ImmutableMap.of( + 0, "First line", + 1, "Second line", + 2, "Third line")); + } + + @Test + public void multiLineContentStartingFromNonZeroLine() { + SparseFileContentBuilder builder = new SparseFileContentBuilder(8); + + builder.addLine(5, "First line"); + builder.addLine(6, "Second line"); + builder.addLine(7, "Third line"); + SparseFileContent content = builder.build(); + assertThat(content).getSize().isEqualTo(8); + assertThat(content).getRangesCount().isEqualTo(1); + assertThat(content) + .lines() + .containsExactlyEntriesIn( + ImmutableMap.of( + 5, "First line", + 6, "Second line", + 7, "Third line")); + } + + @Test + public void multiLineContentWithGaps() { + SparseFileContentBuilder builder = new SparseFileContentBuilder(10000); + builder.addLine(0, "First line"); + builder.addLine(1, "Second line"); + builder.addLine(3, "Third line"); + builder.addLine(4, "Fourth line"); + builder.addLine(5, "Fifth line"); + builder.addLine(6, "Sixth line"); + builder.addLine(10, "Seventh line"); + SparseFileContent content = builder.build(); + assertThat(content).getSize().isEqualTo(10000); + assertThat(content).getRangesCount().isEqualTo(3); + assertThat(content) + .lines() + .containsExactlyEntriesIn( + ImmutableMap.builder() + .put(0, "First line") + .put(1, "Second line") + .put(3, "Third line") + .put(4, "Fourth line") + .put(5, "Fifth line") + .put(6, "Sixth line") + .put(10, "Seventh line") + .build()); + } +}
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD index b94a709..022813b 100644 --- a/javatests/com/google/gerrit/server/BUILD +++ b/javatests/com/google/gerrit/server/BUILD
@@ -55,6 +55,8 @@ "//java/com/google/gerrit/server/account/externalids/testing", "//java/com/google/gerrit/server/cache/serialize", "//java/com/google/gerrit/server/cache/testing", + "//java/com/google/gerrit/server/fixes/testing", + "//java/com/google/gerrit/server/git/receive:ref_cache", "//java/com/google/gerrit/server/ioutil", "//java/com/google/gerrit/server/logging", "//java/com/google/gerrit/server/project/testing:project-test-util", @@ -73,7 +75,6 @@ "//lib:protobuf", "//lib/auto:auto-value", "//lib/auto:auto-value-annotations", - "//lib/commons:codec", "//lib/flogger:api", "//lib/guice", "//lib/mockito",
diff --git a/javatests/com/google/gerrit/server/account/externalids/ExternalIDCacheLoaderTest.java b/javatests/com/google/gerrit/server/account/externalids/ExternalIDCacheLoaderTest.java index 054b1aa..2f64ed0 100644 --- a/javatests/com/google/gerrit/server/account/externalids/ExternalIDCacheLoaderTest.java +++ b/javatests/com/google/gerrit/server/account/externalids/ExternalIDCacheLoaderTest.java
@@ -18,9 +18,9 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import com.google.common.collect.ImmutableList; import com.google.gerrit.entities.Account; import com.google.gerrit.entities.RefNames; @@ -44,7 +44,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; @@ -52,8 +51,7 @@ public class ExternalIDCacheLoaderTest { private static AllUsersName ALL_USERS = new AllUsersName(AllUsersNameProvider.DEFAULT); - @Mock Cache<ObjectId, AllExternalIds> externalIdCache; - + private Cache<ObjectId, AllExternalIds> externalIdCache; private ExternalIdCacheLoader loader; private GitRepositoryManager repoManager = new InMemoryRepositoryManager(); private ExternalIdReader externalIdReader; @@ -61,6 +59,7 @@ @Before public void setUp() throws Exception { + externalIdCache = CacheBuilder.newBuilder().build(); repoManager.createRepository(ALL_USERS).close(); externalIdReader = new ExternalIdReader(repoManager, ALL_USERS, new DisabledMetricMaker()); externalIdReaderSpy = Mockito.spy(externalIdReader); @@ -78,8 +77,7 @@ public void reloadsSingleUpdateUsingPartialReload() throws Exception { ObjectId firstState = insertExternalId(1, 1); ObjectId head = insertExternalId(2, 2); - - when(externalIdCache.getIfPresent(firstState)).thenReturn(allFromGit(firstState)); + externalIdCache.put(firstState, allFromGit(firstState)); assertThat(loader.load(head)).isEqualTo(allFromGit(head)); verifyZeroInteractions(externalIdReaderSpy); @@ -91,8 +89,7 @@ insertExternalId(2, 2); insertExternalId(3, 3); ObjectId head = insertExternalId(4, 4); - - when(externalIdCache.getIfPresent(firstState)).thenReturn(allFromGit(firstState)); + externalIdCache.put(firstState, allFromGit(firstState)); assertThat(loader.load(head)).isEqualTo(allFromGit(head)); verifyZeroInteractions(externalIdReaderSpy); @@ -100,11 +97,9 @@ @Test public void reloadsAllExternalIdsWhenNoOldStateIsCached() throws Exception { - ObjectId firstState = insertExternalId(1, 1); + insertExternalId(1, 1); ObjectId head = insertExternalId(2, 2); - when(externalIdCache.getIfPresent(firstState)).thenReturn(null); - assertThat(loader.load(head)).isEqualTo(allFromGit(head)); verify(externalIdReaderSpy, times(1)).all(head); } @@ -144,8 +139,7 @@ ObjectId firstState = insertExternalId(1, 1); ObjectId head = deleteExternalId(1, 1); assertThat(allFromGit(head).byAccount().size()).isEqualTo(0); - - when(externalIdCache.getIfPresent(firstState)).thenReturn(allFromGit(firstState)); + externalIdCache.put(firstState, allFromGit(firstState)); assertThat(loader.load(head)).isEqualTo(allFromGit(head)); verifyZeroInteractions(externalIdReaderSpy); @@ -159,8 +153,7 @@ externalId(1, 1), ExternalId.create("fooschema", "bar1", Account.id(1), "foo@bar.com", "password")); assertThat(allFromGit(head).byAccount().size()).isEqualTo(1); - - when(externalIdCache.getIfPresent(firstState)).thenReturn(allFromGit(firstState)); + externalIdCache.put(firstState, allFromGit(firstState)); assertThat(loader.load(head)).isEqualTo(allFromGit(head)); verifyZeroInteractions(externalIdReaderSpy); @@ -177,7 +170,7 @@ head = repo.exactRef(RefNames.REFS_EXTERNAL_IDS).getObjectId(); } - when(externalIdCache.getIfPresent(firstState)).thenReturn(allFromGit(firstState)); + externalIdCache.put(firstState, allFromGit(firstState)); assertThat(loader.load(head)).isEqualTo(allFromGit(head)); verifyZeroInteractions(externalIdReaderSpy); @@ -190,8 +183,7 @@ ObjectId oldState = inserExternalIds(257); assertAllFilesHaveSlashesInPath(); ObjectId head = insertExternalId(500, 500); - - when(externalIdCache.getIfPresent(oldState)).thenReturn(allFromGit(oldState)); + externalIdCache.put(oldState, allFromGit(oldState)); assertThat(loader.load(head)).isEqualTo(allFromGit(head)); verifyZeroInteractions(externalIdReaderSpy); @@ -205,8 +197,7 @@ // Create one more external ID and then have the Loader compute the new state ObjectId head = insertExternalId(500, 500); assertAllFilesHaveSlashesInPath(); // NoteMap resharded - - when(externalIdCache.getIfPresent(oldState)).thenReturn(allFromGit(oldState)); + externalIdCache.put(oldState, allFromGit(oldState)); assertThat(loader.load(head)).isEqualTo(allFromGit(head)); verifyZeroInteractions(externalIdReaderSpy);
diff --git a/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java b/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java index 2485613..de23ef4 100644 --- a/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java +++ b/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java
@@ -36,7 +36,6 @@ import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.permissions.ProjectPermission; import java.util.Collection; -import java.util.Map; import java.util.Set; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; @@ -85,7 +84,7 @@ } @Override - public Map<String, Ref> filter(Map<String, Ref> refs, Repository repo, RefFilterOptions opts) + public Collection<Ref> filter(Collection<Ref> refs, Repository repo, RefFilterOptions opts) throws PermissionBackendException { throw new UnsupportedOperationException("not implemented"); }
diff --git a/javatests/com/google/gerrit/server/fixes/LineIdentifierTest.java b/javatests/com/google/gerrit/server/fixes/LineIdentifierTest.java deleted file mode 100644 index ba80c02..0000000 --- a/javatests/com/google/gerrit/server/fixes/LineIdentifierTest.java +++ /dev/null
@@ -1,255 +0,0 @@ -// 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.fixes; - -import static com.google.common.truth.Truth.assertThat; -import static com.google.gerrit.testing.GerritJUnit.assertThrows; - -import org.junit.Test; - -public class LineIdentifierTest { - @Test - public void lineNumberMustBePositive() { - LineIdentifier lineIdentifier = new LineIdentifier("First line\nSecond line"); - StringIndexOutOfBoundsException thrown = - assertThrows( - StringIndexOutOfBoundsException.class, () -> lineIdentifier.getStartIndexOfLine(0)); - assertThat(thrown).hasMessageThat().contains("positive"); - } - - @Test - public void lineNumberMustIndicateAnAvailableLine() { - LineIdentifier lineIdentifier = new LineIdentifier("First line\nSecond line"); - StringIndexOutOfBoundsException thrown = - assertThrows( - StringIndexOutOfBoundsException.class, () -> lineIdentifier.getStartIndexOfLine(3)); - assertThat(thrown).hasMessageThat().contains("Line 3 isn't available"); - } - - @Test - public void startIndexOfFirstLineIsRecognized() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567"); - int startIndex = lineIdentifier.getStartIndexOfLine(1); - assertThat(startIndex).isEqualTo(0); - } - - @Test - public void lengthOfFirstLineIsCorrect() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567"); - int lineLength = lineIdentifier.getLengthOfLine(1); - assertThat(lineLength).isEqualTo(8); - } - - @Test - public void startIndexOfSecondLineIsRecognized() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567"); - int startIndex = lineIdentifier.getStartIndexOfLine(2); - assertThat(startIndex).isEqualTo(9); - } - - @Test - public void lengthOfSecondLineIsCorrect() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567"); - int lineLength = lineIdentifier.getLengthOfLine(2); - assertThat(lineLength).isEqualTo(3); - } - - @Test - public void startIndexOfLastLineIsRecognized() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567"); - int startIndex = lineIdentifier.getStartIndexOfLine(3); - assertThat(startIndex).isEqualTo(13); - } - - @Test - public void lengthOfLastLineIsCorrect() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567"); - int lineLength = lineIdentifier.getLengthOfLine(3); - assertThat(lineLength).isEqualTo(7); - } - - @Test - public void emptyFirstLineIsRecognized() { - LineIdentifier lineIdentifier = new LineIdentifier("\n123\n1234567"); - int startIndex = lineIdentifier.getStartIndexOfLine(1); - assertThat(startIndex).isEqualTo(0); - } - - @Test - public void lengthOfEmptyFirstLineIsCorrect() { - LineIdentifier lineIdentifier = new LineIdentifier("\n123\n1234567"); - int lineLength = lineIdentifier.getLengthOfLine(1); - assertThat(lineLength).isEqualTo(0); - } - - @Test - public void emptyIntermediaryLineIsRecognized() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678\n\n1234567"); - int startIndex = lineIdentifier.getStartIndexOfLine(2); - assertThat(startIndex).isEqualTo(9); - } - - @Test - public void lengthOfEmptyIntermediaryLineIsCorrect() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678\n\n1234567"); - int lineLength = lineIdentifier.getLengthOfLine(2); - assertThat(lineLength).isEqualTo(0); - } - - @Test - public void lineAfterIntermediaryLineIsRecognized() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678\n\n1234567"); - int startIndex = lineIdentifier.getStartIndexOfLine(3); - assertThat(startIndex).isEqualTo(10); - } - - @Test - public void emptyLastLineIsRecognized() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n"); - int startIndex = lineIdentifier.getStartIndexOfLine(3); - assertThat(startIndex).isEqualTo(13); - } - - @Test - public void lengthOfEmptyLastLineIsCorrect() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n"); - int lineLength = lineIdentifier.getLengthOfLine(3); - assertThat(lineLength).isEqualTo(0); - } - - @Test - public void startIndexOfSingleLineIsRecognized() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678"); - int startIndex = lineIdentifier.getStartIndexOfLine(1); - assertThat(startIndex).isEqualTo(0); - } - - @Test - public void lengthOfSingleLineIsCorrect() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678"); - int lineLength = lineIdentifier.getLengthOfLine(1); - assertThat(lineLength).isEqualTo(8); - } - - @Test - public void startIndexOfSingleEmptyLineIsRecognized() { - LineIdentifier lineIdentifier = new LineIdentifier(""); - int startIndex = lineIdentifier.getStartIndexOfLine(1); - assertThat(startIndex).isEqualTo(0); - } - - @Test - public void lengthOfSingleEmptyLineIsCorrect() { - LineIdentifier lineIdentifier = new LineIdentifier(""); - int lineLength = lineIdentifier.getLengthOfLine(1); - assertThat(lineLength).isEqualTo(0); - } - - @Test - public void lookingUpSubsequentLinesIsPossible() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567\n12"); - - int firstLineStartIndex = lineIdentifier.getStartIndexOfLine(1); - assertThat(firstLineStartIndex).isEqualTo(0); - - int secondLineStartIndex = lineIdentifier.getStartIndexOfLine(2); - assertThat(secondLineStartIndex).isEqualTo(9); - } - - @Test - public void lookingUpNotSubsequentLinesInAscendingOrderIsPossible() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567\n12"); - - int firstLineStartIndex = lineIdentifier.getStartIndexOfLine(1); - assertThat(firstLineStartIndex).isEqualTo(0); - - int fourthLineStartIndex = lineIdentifier.getStartIndexOfLine(4); - assertThat(fourthLineStartIndex).isEqualTo(21); - } - - @Test - public void lookingUpNotSubsequentLinesInDescendingOrderIsPossible() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567\n12"); - - int fourthLineStartIndex = lineIdentifier.getStartIndexOfLine(4); - assertThat(fourthLineStartIndex).isEqualTo(21); - - int secondLineStartIndex = lineIdentifier.getStartIndexOfLine(2); - assertThat(secondLineStartIndex).isEqualTo(9); - } - - @Test - public void linesSeparatedByOnlyCarriageReturnAreRecognized() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678\r123\r12"); - int startIndex = lineIdentifier.getStartIndexOfLine(2); - assertThat(startIndex).isEqualTo(9); - } - - @Test - public void lengthOfLinesSeparatedByOnlyCarriageReturnIsCorrect() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678\r123\r12"); - int lineLength = lineIdentifier.getLengthOfLine(2); - assertThat(lineLength).isEqualTo(3); - } - - @Test - public void linesSeparatedByLineFeedAndCarriageReturnAreRecognized() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678\r\n123\r\n12"); - int startIndex = lineIdentifier.getStartIndexOfLine(2); - assertThat(startIndex).isEqualTo(10); - } - - @Test - public void lengthOfLinesSeparatedByLineFeedAndCarriageReturnIsCorrect() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678\r\n123\r\n12"); - int lineLength = lineIdentifier.getLengthOfLine(2); - assertThat(lineLength).isEqualTo(3); - } - - @Test - public void linesSeparatedByMixtureOfCarriageReturnAndLineFeedAreRecognized() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678\r123\r\n12\n123456\r\n1234"); - int startIndex = lineIdentifier.getStartIndexOfLine(5); - assertThat(startIndex).isEqualTo(25); - } - - @Test - public void linesSeparatedBySomeUnicodeLinebreakCharacterAreRecognized() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678\u2029123\u202912"); - int startIndex = lineIdentifier.getStartIndexOfLine(2); - assertThat(startIndex).isEqualTo(9); - } - - @Test - public void lengthOfLinesSeparatedBySomeUnicodeLinebreakCharacterIsCorrect() { - LineIdentifier lineIdentifier = new LineIdentifier("12345678\u2029123\u202912"); - int lineLength = lineIdentifier.getLengthOfLine(2); - assertThat(lineLength).isEqualTo(3); - } - - @Test - public void blanksAreNotInterpretedAsLineSeparators() { - LineIdentifier lineIdentifier = new LineIdentifier("1 2345678\n123\n12"); - int startIndex = lineIdentifier.getStartIndexOfLine(2); - assertThat(startIndex).isEqualTo(10); - } - - @Test - public void tabsAreNotInterpretedAsLineSeparators() { - LineIdentifier lineIdentifier = new LineIdentifier("123\t45678\n123\n12"); - int startIndex = lineIdentifier.getStartIndexOfLine(2); - assertThat(startIndex).isEqualTo(10); - } -}
diff --git a/javatests/com/google/gerrit/server/fixes/fixCalculator/EmptyContentTest.java b/javatests/com/google/gerrit/server/fixes/fixCalculator/EmptyContentTest.java new file mode 100644 index 0000000..51fbc67 --- /dev/null +++ b/javatests/com/google/gerrit/server/fixes/fixCalculator/EmptyContentTest.java
@@ -0,0 +1,68 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.fixes.fixCalculator; + +import static com.google.gerrit.server.fixes.testing.FixResultSubject.assertThat; +import static com.google.gerrit.server.fixes.testing.GitEditSubject.assertThat; + +import com.google.gerrit.server.fixes.FixCalculator.FixResult; +import org.eclipse.jgit.diff.Edit; +import org.junit.Test; + +public class EmptyContentTest { + @Test + public void insertSingleLineNoEOL() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("", 1, 0, 1, 0, "Abc"); + assertThat(fixResult).text().isEqualTo("Abc"); + assertThat(fixResult).edits().onlyElement(); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isInsert(0, 0, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 3); + } + + @Test + public void insertSingleLineWithEOL() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("", 1, 0, 1, 0, "Abc\n"); + assertThat(fixResult).text().isEqualTo("Abc\n"); + assertThat(fixResult).edits().onlyElement(); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isInsert(0, 0, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 4); + } + + @Test + public void insertMultilineNoEOL() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("", 1, 0, 1, 0, "Abc\nDEFGH"); + assertThat(fixResult).text().isEqualTo("Abc\nDEFGH"); + assertThat(fixResult).edits().onlyElement(); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isInsert(0, 0, 2); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 9); + } + + @Test + public void insertMultilineWithEOL() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("", 1, 0, 1, 0, "Abc\nDEFGH\n"); + assertThat(fixResult).text().isEqualTo("Abc\nDEFGH\n"); + assertThat(fixResult).edits().onlyElement(); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isInsert(0, 0, 2); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 10); + } +}
diff --git a/javatests/com/google/gerrit/server/fixes/fixCalculator/FixCalculatorVariousTest.java b/javatests/com/google/gerrit/server/fixes/fixCalculator/FixCalculatorVariousTest.java new file mode 100644 index 0000000..861af3e --- /dev/null +++ b/javatests/com/google/gerrit/server/fixes/fixCalculator/FixCalculatorVariousTest.java
@@ -0,0 +1,168 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.fixes.fixCalculator; + +import static com.google.gerrit.server.fixes.testing.FixResultSubject.assertThat; +import static com.google.gerrit.server.fixes.testing.GitEditSubject.assertThat; +import static com.google.gerrit.testing.GerritJUnit.assertThrows; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.collect.ImmutableList; +import com.google.gerrit.entities.Comment.Range; +import com.google.gerrit.entities.FixReplacement; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.server.fixes.FixCalculator; +import com.google.gerrit.server.fixes.FixCalculator.FixResult; +import com.google.gerrit.server.patch.Text; +import org.eclipse.jgit.diff.Edit; +import org.junit.Test; + +public class FixCalculatorVariousTest { + private static final String multilineContentString = + "First line\nSecond line\nThird line\nFourth line\nFifth line\n"; + private static final Text multilineContent = new Text(multilineContentString.getBytes(UTF_8)); + + public static FixResult calculateFixSingleReplacement( + String content, int startLine, int startChar, int endLine, int endChar, String replacement) + throws ResourceConflictException { + FixReplacement fixReplacement = + new FixReplacement( + "AnyPath", new Range(startLine, startChar, endLine, endChar), replacement); + return FixCalculator.calculateFix( + new Text(content.getBytes(UTF_8)), ImmutableList.of(fixReplacement)); + } + + @Test + public void lineNumberMustBePositive() { + assertThrows( + ResourceConflictException.class, + () -> calculateFixSingleReplacement("First line\nSecond line", 0, 0, 0, 0, "Abc")); + } + + @Test + public void insertAtTheEndOfSingleLineContentHasEOLMarkInvalidPosition() throws Exception { + assertThrows( + ResourceConflictException.class, + () -> calculateFixSingleReplacement("First line\n", 1, 11, 1, 11, "Abc")); + } + + @Test + public void severalChangesInTheSameLineNonSorted() throws Exception { + FixReplacement replace = new FixReplacement("path", new Range(2, 1, 2, 3), "ABC"); + FixReplacement insert = new FixReplacement("path", new Range(2, 5, 2, 5), "DEFG"); + FixReplacement delete = new FixReplacement("path", new Range(2, 7, 2, 9), ""); + FixResult result = + FixCalculator.calculateFix(multilineContent, ImmutableList.of(replace, delete, insert)); + assertThat(result) + .text() + .isEqualTo("First line\nSABConDEFGd ne\nThird line\nFourth line\nFifth line\n"); + assertThat(result).edits().hasSize(1); + Edit edit = result.edits.get(0); + assertThat(edit).isReplace(1, 1, 1, 1); + assertThat(edit).internalEdits().hasSize(3); + assertThat(edit).internalEdits().element(0).isReplace(1, 2, 1, 3); + assertThat(edit).internalEdits().element(1).isInsert(5, 6, 4); + assertThat(edit).internalEdits().element(2).isDelete(7, 2, 12); + } + + @Test + public void severalChangesInConsecutiveLines() throws Exception { + FixReplacement replace = new FixReplacement("path", new Range(2, 1, 2, 3), "ABC"); + FixReplacement insert = new FixReplacement("path", new Range(3, 5, 3, 5), "DEFG"); + FixReplacement delete = new FixReplacement("path", new Range(4, 7, 4, 9), ""); + FixResult result = + FixCalculator.calculateFix(multilineContent, ImmutableList.of(replace, insert, delete)); + assertThat(result) + .text() + .isEqualTo("First line\nSABCond line\nThirdDEFG line\nFourth ne\nFifth line\n"); + assertThat(result).edits().hasSize(1); + Edit edit = result.edits.get(0); + assertThat(edit).isReplace(1, 3, 1, 3); + assertThat(edit).internalEdits().hasSize(3); + assertThat(edit).internalEdits().element(0).isReplace(1, 2, 1, 3); + assertThat(edit).internalEdits().element(1).isInsert(17, 18, 4); + assertThat(edit).internalEdits().element(2).isDelete(30, 2, 35); + } + + @Test + public void severalChangesInNonConsecutiveLines() throws Exception { + FixReplacement replace = new FixReplacement("path", new Range(1, 1, 1, 3), "ABC"); + FixReplacement insert = new FixReplacement("path", new Range(3, 5, 3, 5), "DEFG"); + FixReplacement delete = new FixReplacement("path", new Range(5, 9, 6, 0), ""); + FixResult result = + FixCalculator.calculateFix(multilineContent, ImmutableList.of(replace, insert, delete)); + assertThat(result) + .text() + .isEqualTo("FABCst line\nSecond line\nThirdDEFG line\nFourth line\nFifth lin"); + assertThat(result).edits().hasSize(3); + assertThat(result).edits().element(0).isReplace(0, 1, 0, 1); + assertThat(result).edits().element(0).internalEdits().onlyElement().isReplace(1, 2, 1, 3); + assertThat(result).edits().element(1).isReplace(2, 1, 2, 1); + assertThat(result).edits().element(1).internalEdits().onlyElement().isInsert(5, 5, 4); + assertThat(result).edits().element(2).isReplace(4, 1, 4, 1); + assertThat(result).edits().element(2).internalEdits().onlyElement().isDelete(9, 2, 9); + } + + @Test + public void multipleChanges() throws Exception { + String str = + "First line\nSecond line\nThird line\nFourth line\nFifth line\nSixth line" + + "\nSeventh line\nEighth line\nNinth line\nTenth line\n"; + Text content = new Text(str.getBytes(UTF_8)); + + FixReplacement multiLineReplace = + new FixReplacement("path", new Range(1, 2, 3, 3), "AB\nC\nDEFG\nQ\n"); + FixReplacement multiLineDelete = new FixReplacement("path", new Range(4, 8, 5, 8), ""); + FixReplacement singleLineInsert = new FixReplacement("path", new Range(5, 10, 5, 10), "QWERTY"); + + FixReplacement singleLineReplace = new FixReplacement("path", new Range(7, 3, 7, 7), "XY"); + FixReplacement multiLineInsert = + new FixReplacement("path", new Range(8, 7, 8, 7), "KLMNO\nASDF"); + + FixReplacement singleLineDelete = new FixReplacement("path", new Range(10, 3, 10, 7), ""); + + FixResult result = + FixCalculator.calculateFix( + content, + ImmutableList.of( + multiLineReplace, + multiLineDelete, + singleLineInsert, + singleLineReplace, + multiLineInsert, + singleLineDelete)); + assertThat(result) + .text() + .isEqualTo( + "FiAB\nC\nDEFG\nQ\nrd line\nFourth lneQWERTY\nSixth line\nSevXY line\nEighth KLMNO\nASDFline\nNinth line\nTenine\n"); + assertThat(result).edits().hasSize(3); + assertThat(result).edits().element(0).isReplace(0, 5, 0, 6); + assertThat(result) + .edits() + .element(0) + .internalEdits() + .containsExactly( + new Edit(2, 26, 2, 14), new Edit(42, 54, 30, 30), new Edit(56, 56, 32, 38)); + + assertThat(result).edits().element(1).isReplace(6, 2, 7, 3); + assertThat(result) + .edits() + .element(1) + .internalEdits() + .containsExactly(new Edit(3, 7, 3, 5), new Edit(20, 20, 18, 28)); + assertThat(result).edits().element(2).isReplace(9, 1, 11, 1); + assertThat(result).edits().element(2).internalEdits().onlyElement().isDelete(3, 4, 3); + } +}
diff --git a/javatests/com/google/gerrit/server/fixes/fixCalculator/MultilineContentNoEOLTest.java b/javatests/com/google/gerrit/server/fixes/fixCalculator/MultilineContentNoEOLTest.java new file mode 100644 index 0000000..dd36e3a --- /dev/null +++ b/javatests/com/google/gerrit/server/fixes/fixCalculator/MultilineContentNoEOLTest.java
@@ -0,0 +1,337 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.fixes.fixCalculator; + +import static com.google.gerrit.server.fixes.testing.FixResultSubject.assertThat; +import static com.google.gerrit.server.fixes.testing.GitEditSubject.assertThat; + +import com.google.gerrit.server.fixes.FixCalculator.FixResult; +import org.eclipse.jgit.diff.Edit; +import org.junit.Test; + +public class MultilineContentNoEOLTest { + + @Test + public void insertSingleLineNoEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 1, 0, 1, 0, "Abc"); + assertThat(fixResult).text().isEqualTo("AbcFirst line\nSecond line\nThird line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 3); + } + + @Test + public void insertSingleLineNoEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 2, 5, 2, 5, "Abc"); + assertThat(fixResult).text().isEqualTo("First line\nSeconAbcd line\nThird line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(1, 1, 1, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(5, 5, 3); + } + + @Test + public void insertSingleLineNoEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 3, 10, 3, 10, "Abc"); + assertThat(fixResult).text().isEqualTo("First line\nSecond line\nThird lineAbc"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(2, 1, 2, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(10, 10, 3); + } + + @Test + public void insertSingleLineWithEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 1, 0, 1, 0, "Abc\n"); + assertThat(fixResult).text().isEqualTo("Abc\nFirst line\nSecond line\nThird line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isInsert(0, 0, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 4); + } + + @Test + public void insertSingleLineWithEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 2, 5, 2, 5, "Abc\n"); + assertThat(fixResult).text().isEqualTo("First line\nSeconAbc\nd line\nThird line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(1, 1, 1, 2); + assertThat(edit).internalEdits().onlyElement().isInsert(5, 5, 4); + } + + @Test + public void insertSingleLineWithEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 3, 10, 3, 10, "Abc\n"); + assertThat(fixResult).text().isEqualTo("First line\nSecond line\nThird lineAbc\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(2, 1, 2, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(10, 10, 4); + } + + @Test + public void insertMultilineLineWithEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 1, 0, 1, 0, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("Abc\nDefgh\nFirst line\nSecond line\nThird line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isInsert(0, 0, 2); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 10); + } + + @Test + public void insertMultilineLineWithEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 2, 5, 2, 5, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("First line\nSeconAbc\nDefgh\nd line\nThird line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(1, 1, 1, 3); + assertThat(edit).internalEdits().onlyElement().isInsert(5, 5, 10); + } + + @Test + public void insertMultilineLineWithEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 3, 10, 3, 10, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("First line\nSecond line\nThird lineAbc\nDefgh\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(2, 1, 2, 2); + assertThat(edit).internalEdits().onlyElement().isInsert(10, 10, 10); + } + + @Test + public void replaceWithSingleLineNoEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 1, 0, 1, 2, "Abc"); + assertThat(fixResult).text().isEqualTo("Abcrst line\nSecond line\nThird line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isReplace(0, 2, 0, 3); + } + + @Test + public void replaceWithSingleLineNoEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 2, 3, 2, 5, "Abc"); + assertThat(fixResult).text().isEqualTo("First line\nSecAbcd line\nThird line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(1, 1, 1, 1); + assertThat(edit).internalEdits().onlyElement().isReplace(3, 2, 3, 3); + } + + @Test + public void replaceWithSingleLineNoEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 3, 8, 3, 10, "Abc"); + assertThat(fixResult).text().isEqualTo("First line\nSecond line\nThird liAbc"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(2, 1, 2, 1); + assertThat(edit).internalEdits().onlyElement().isReplace(8, 2, 8, 3); + } + + @Test + public void replaceWithSingleLineWithEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 1, 0, 1, 2, "Abc\n"); + assertThat(fixResult).text().isEqualTo("Abc\nrst line\nSecond line\nThird line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(0, 2, 0, 4); + } + + @Test + public void replaceWithSingleLineWithEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 2, 3, 2, 5, "Abc\n"); + assertThat(fixResult).text().isEqualTo("First line\nSecAbc\nd line\nThird line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(1, 1, 1, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(3, 2, 3, 4); + } + + @Test + public void replaceWithSingleLineWithEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 3, 8, 3, 10, "Abc\n"); + assertThat(fixResult).text().isEqualTo("First line\nSecond line\nThird liAbc\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(2, 1, 2, 1); + assertThat(edit).internalEdits().onlyElement().isReplace(8, 2, 8, 4); + } + + @Test + public void replaceMultilineLineWithEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 1, 0, 1, 2, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("Abc\nDefgh\nrst line\nSecond line\nThird line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 3); + assertThat(edit).internalEdits().onlyElement().isReplace(0, 2, 0, 10); + } + + @Test + public void replaceMultilineLineWithEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 2, 3, 2, 5, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("First line\nSecAbc\nDefgh\nd line\nThird line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(1, 1, 1, 3); + assertThat(edit).internalEdits().onlyElement().isReplace(3, 2, 3, 10); + } + + @Test + public void replaceMultilineLineWithEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 3, 8, 3, 10, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("First line\nSecond line\nThird liAbc\nDefgh\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(2, 1, 2, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(8, 2, 8, 10); + } + + @Test + public void replaceMultilineLineNoEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 1, 0, 1, 2, "Abc\nDefgh"); + assertThat(fixResult).text().isEqualTo("Abc\nDefghrst line\nSecond line\nThird line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(0, 2, 0, 9); + } + + @Test + public void replaceMultilineLineNoEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 2, 3, 2, 5, "Abc\nDefgh"); + assertThat(fixResult).text().isEqualTo("First line\nSecAbc\nDefghd line\nThird line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(1, 1, 1, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(3, 2, 3, 9); + } + + @Test + public void replaceMultilineLineNoEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 3, 8, 3, 10, "Abc\nDefgh"); + assertThat(fixResult).text().isEqualTo("First line\nSecond line\nThird liAbc\nDefgh"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(2, 1, 2, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(8, 2, 8, 9); + } + + @Test + public void replaceWholeContent() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 1, 0, 3, 10, "Abc"); + assertThat(fixResult).text().isEqualTo("Abc"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 3, 0, 1); + assertThat(edit).internalEdits().onlyElement().isReplace(0, 33, 0, 3); + } + + @Test + public void deleteWholeContent() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 1, 0, 3, 10, ""); + assertThat(fixResult).text().isEqualTo(""); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isDelete(0, 3, 0); + assertThat(edit).internalEdits().onlyElement().isDelete(0, 33, 0); + } + + @Test + public void deleteAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 1, 0, 1, 4, ""); + assertThat(fixResult).text().isEqualTo("t line\nSecond line\nThird line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isDelete(0, 4, 0); + } + + @Test + public void deleteInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 1, 5, 3, 1, ""); + assertThat(fixResult).text().isEqualTo("Firsthird line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 3, 0, 1); + assertThat(edit).internalEdits().onlyElement().isDelete(5, 19, 5); + } + + @Test + public void deleteAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line", 3, 7, 3, 10, ""); + assertThat(fixResult).text().isEqualTo("First line\nSecond line\nThird l"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(2, 1, 2, 1); + assertThat(edit).internalEdits().onlyElement().isDelete(7, 3, 7); + } +}
diff --git a/javatests/com/google/gerrit/server/fixes/fixCalculator/MultilineContentWithEOLTest.java b/javatests/com/google/gerrit/server/fixes/fixCalculator/MultilineContentWithEOLTest.java new file mode 100644 index 0000000..a2868c8 --- /dev/null +++ b/javatests/com/google/gerrit/server/fixes/fixCalculator/MultilineContentWithEOLTest.java
@@ -0,0 +1,337 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.fixes.fixCalculator; + +import static com.google.gerrit.server.fixes.testing.FixResultSubject.assertThat; +import static com.google.gerrit.server.fixes.testing.GitEditSubject.assertThat; + +import com.google.gerrit.server.fixes.FixCalculator.FixResult; +import org.eclipse.jgit.diff.Edit; +import org.junit.Test; + +public class MultilineContentWithEOLTest { + + @Test + public void insertSingleLineNoEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 1, 0, 1, 0, "Abc"); + assertThat(fixResult).text().isEqualTo("AbcFirst line\nSecond line\nThird line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 3); + } + + @Test + public void insertSingleLineNoEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 2, 5, 2, 5, "Abc"); + assertThat(fixResult).text().isEqualTo("First line\nSeconAbcd line\nThird line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(1, 1, 1, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(5, 5, 3); + } + + @Test + public void insertSingleLineNoEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 4, 0, 4, 0, "Abc"); + assertThat(fixResult).text().isEqualTo("First line\nSecond line\nThird line\nAbc"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isInsert(3, 3, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 3); + } + + @Test + public void insertSingleLineWithEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 1, 0, 1, 0, "Abc\n"); + assertThat(fixResult).text().isEqualTo("Abc\nFirst line\nSecond line\nThird line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isInsert(0, 0, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 4); + } + + @Test + public void insertSingleLineWithEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 2, 5, 2, 5, "Abc\n"); + assertThat(fixResult).text().isEqualTo("First line\nSeconAbc\nd line\nThird line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(1, 1, 1, 2); + assertThat(edit).internalEdits().onlyElement().isInsert(5, 5, 4); + } + + @Test + public void insertSingleLineWithEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 4, 0, 4, 0, "Abc\n"); + assertThat(fixResult).text().isEqualTo("First line\nSecond line\nThird line\nAbc\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isInsert(3, 3, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 4); + } + + @Test + public void insertMultilineLineWithEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 1, 0, 1, 0, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("Abc\nDefgh\nFirst line\nSecond line\nThird line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isInsert(0, 0, 2); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 10); + } + + @Test + public void insertMultilineLineWithEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 2, 5, 2, 5, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("First line\nSeconAbc\nDefgh\nd line\nThird line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(1, 1, 1, 3); + assertThat(edit).internalEdits().onlyElement().isInsert(5, 5, 10); + } + + @Test + public void insertMultilineLineWithEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 4, 0, 4, 0, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("First line\nSecond line\nThird line\nAbc\nDefgh\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isInsert(3, 3, 2); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 10); + } + + @Test + public void replaceWithSingleLineNoEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 1, 0, 1, 2, "Abc"); + assertThat(fixResult).text().isEqualTo("Abcrst line\nSecond line\nThird line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isReplace(0, 2, 0, 3); + } + + @Test + public void replaceWithSingleLineNoEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 2, 3, 2, 5, "Abc"); + assertThat(fixResult).text().isEqualTo("First line\nSecAbcd line\nThird line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(1, 1, 1, 1); + assertThat(edit).internalEdits().onlyElement().isReplace(3, 2, 3, 3); + } + + @Test + public void replaceWithSingleLineNoEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 3, 9, 4, 0, "Abc"); + assertThat(fixResult).text().isEqualTo("First line\nSecond line\nThird linAbc"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(2, 1, 2, 1); + assertThat(edit).internalEdits().onlyElement().isReplace(9, 2, 9, 3); + } + + @Test + public void replaceWithSingleLineWithEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 1, 0, 1, 2, "Abc\n"); + assertThat(fixResult).text().isEqualTo("Abc\nrst line\nSecond line\nThird line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(0, 2, 0, 4); + } + + @Test + public void replaceWithSingleLineWithEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 2, 3, 2, 5, "Abc\n"); + assertThat(fixResult).text().isEqualTo("First line\nSecAbc\nd line\nThird line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(1, 1, 1, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(3, 2, 3, 4); + } + + @Test + public void replaceWithSingleLineWithEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 3, 9, 4, 0, "Abc\n"); + assertThat(fixResult).text().isEqualTo("First line\nSecond line\nThird linAbc\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(2, 1, 2, 1); + assertThat(edit).internalEdits().onlyElement().isReplace(9, 2, 9, 4); + } + + @Test + public void replaceMultilineLineWithEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 1, 0, 1, 2, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("Abc\nDefgh\nrst line\nSecond line\nThird line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 3); + assertThat(edit).internalEdits().onlyElement().isReplace(0, 2, 0, 10); + } + + @Test + public void replaceMultilineLineWithEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 2, 3, 2, 5, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("First line\nSecAbc\nDefgh\nd line\nThird line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(1, 1, 1, 3); + assertThat(edit).internalEdits().onlyElement().isReplace(3, 2, 3, 10); + } + + @Test + public void replaceMultilineLineWithEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 3, 9, 4, 0, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("First line\nSecond line\nThird linAbc\nDefgh\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(2, 1, 2, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(9, 2, 9, 10); + } + + @Test + public void replaceMultilineLineNoEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 1, 0, 1, 2, "Abc\nDefgh"); + assertThat(fixResult).text().isEqualTo("Abc\nDefghrst line\nSecond line\nThird line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(0, 2, 0, 9); + } + + @Test + public void replaceMultilineLineNoEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 2, 3, 2, 5, "Abc\nDefgh"); + assertThat(fixResult).text().isEqualTo("First line\nSecAbc\nDefghd line\nThird line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(1, 1, 1, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(3, 2, 3, 9); + } + + @Test + public void replaceMultilineLineNoEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 3, 9, 4, 0, "Abc\nDefgh"); + assertThat(fixResult).text().isEqualTo("First line\nSecond line\nThird linAbc\nDefgh"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(2, 1, 2, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(9, 2, 9, 9); + } + + @Test + public void replaceWholeContent() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 1, 0, 4, 0, "Abc"); + assertThat(fixResult).text().isEqualTo("Abc"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 3, 0, 1); + assertThat(edit).internalEdits().onlyElement().isReplace(0, 34, 0, 3); + } + + @Test + public void deleteWholeContent() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 1, 0, 4, 0, ""); + assertThat(fixResult).text().isEqualTo(""); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isDelete(0, 3, 0); + assertThat(edit).internalEdits().onlyElement().isDelete(0, 34, 0); + } + + @Test + public void deleteAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 1, 0, 1, 4, ""); + assertThat(fixResult).text().isEqualTo("t line\nSecond line\nThird line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isDelete(0, 4, 0); + } + + @Test + public void deleteInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 1, 5, 3, 1, ""); + assertThat(fixResult).text().isEqualTo("Firsthird line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 3, 0, 1); + assertThat(edit).internalEdits().onlyElement().isDelete(5, 19, 5); + } + + @Test + public void deleteAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\nSecond line\nThird line\n", 3, 7, 4, 0, ""); + assertThat(fixResult).text().isEqualTo("First line\nSecond line\nThird l"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(2, 1, 2, 1); + assertThat(edit).internalEdits().onlyElement().isDelete(7, 4, 7); + } +}
diff --git a/javatests/com/google/gerrit/server/fixes/fixCalculator/OneLineContentNoEOLTest.java b/javatests/com/google/gerrit/server/fixes/fixCalculator/OneLineContentNoEOLTest.java new file mode 100644 index 0000000..3de4ef7 --- /dev/null +++ b/javatests/com/google/gerrit/server/fixes/fixCalculator/OneLineContentNoEOLTest.java
@@ -0,0 +1,320 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.fixes.fixCalculator; + +import static com.google.gerrit.server.fixes.testing.FixResultSubject.assertThat; +import static com.google.gerrit.server.fixes.testing.GitEditSubject.assertThat; + +import com.google.gerrit.server.fixes.FixCalculator.FixResult; +import org.eclipse.jgit.diff.Edit; +import org.junit.Test; + +public class OneLineContentNoEOLTest { + + @Test + public void insertSingleLineNoEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line", 1, 0, 1, 0, "Abc"); + assertThat(fixResult).text().isEqualTo("AbcFirst line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 3); + } + + @Test + public void insertSingleLineNoEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line", 1, 5, 1, 5, "Abc"); + assertThat(fixResult).text().isEqualTo("FirstAbc line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(5, 5, 3); + } + + @Test + public void insertSingleLineNoEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line", 1, 10, 1, 10, "Abc"); + assertThat(fixResult).text().isEqualTo("First lineAbc"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(10, 10, 3); + } + + @Test + public void insertSingleLineWithEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line", 1, 0, 1, 0, "Abc\n"); + assertThat(fixResult).text().isEqualTo("Abc\nFirst line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isInsert(0, 0, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 4); + } + + @Test + public void insertSingleLineWithEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line", 1, 5, 1, 5, "Abc\n"); + assertThat(fixResult).text().isEqualTo("FirstAbc\n line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 2); + assertThat(edit).internalEdits().onlyElement().isInsert(5, 5, 4); + } + + @Test + public void insertSingleLineWithEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line", 1, 10, 1, 10, "Abc\n"); + assertThat(fixResult).text().isEqualTo("First lineAbc\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(10, 10, 4); + } + + @Test + public void insertMultilineLineWithEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line", 1, 0, 1, 0, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("Abc\nDefgh\nFirst line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isInsert(0, 0, 2); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 10); + } + + @Test + public void insertMultilineLineWithEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line", 1, 5, 1, 5, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("FirstAbc\nDefgh\n line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 3); + assertThat(edit).internalEdits().onlyElement().isInsert(5, 5, 10); + } + + @Test + public void insertMultilineLineWithEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line", 1, 10, 1, 10, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("First lineAbc\nDefgh\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 2); + assertThat(edit).internalEdits().onlyElement().isInsert(10, 10, 10); + } + + @Test + public void replaceWithSingleLineNoEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line", 1, 0, 1, 2, "Abc"); + assertThat(fixResult).text().isEqualTo("Abcrst line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isReplace(0, 2, 0, 3); + } + + @Test + public void replaceWithSingleLineNoEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line", 1, 3, 1, 5, "Abc"); + assertThat(fixResult).text().isEqualTo("FirAbc line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isReplace(3, 2, 3, 3); + } + + @Test + public void replaceWithSingleLineNoEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line", 1, 8, 1, 10, "Abc"); + assertThat(fixResult).text().isEqualTo("First liAbc"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isReplace(8, 2, 8, 3); + } + + @Test + public void replaceWithSingleLineWithEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line", 1, 0, 1, 2, "Abc\n"); + assertThat(fixResult).text().isEqualTo("Abc\nrst line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(0, 2, 0, 4); + } + + @Test + public void replaceWithSingleLineWithEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line", 1, 3, 1, 5, "Abc\n"); + assertThat(fixResult).text().isEqualTo("FirAbc\n line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(3, 2, 3, 4); + } + + @Test + public void replaceWithSingleLineWithEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line", 1, 8, 1, 10, "Abc\n"); + assertThat(fixResult).text().isEqualTo("First liAbc\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isReplace(8, 2, 8, 4); + } + + @Test + public void replaceMultilineLineWithEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line", 1, 0, 1, 2, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("Abc\nDefgh\nrst line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 3); + assertThat(edit).internalEdits().onlyElement().isReplace(0, 2, 0, 10); + } + + @Test + public void replaceMultilineLineWithEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line", 1, 3, 1, 5, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("FirAbc\nDefgh\n line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 3); + assertThat(edit).internalEdits().onlyElement().isReplace(3, 2, 3, 10); + } + + @Test + public void replaceMultilineLineWithEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line", 1, 8, 1, 10, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("First liAbc\nDefgh\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(8, 2, 8, 10); + } + + @Test + public void replaceMultilineLineNoEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line", 1, 0, 1, 2, "Abc\nDefgh"); + assertThat(fixResult).text().isEqualTo("Abc\nDefghrst line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(0, 2, 0, 9); + } + + @Test + public void replaceMultilineLineNoEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line", 1, 3, 1, 5, "Abc\nDefgh"); + assertThat(fixResult).text().isEqualTo("FirAbc\nDefgh line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(3, 2, 3, 9); + } + + @Test + public void replaceMultilineLineNoEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line", 1, 8, 1, 10, "Abc\nDefgh"); + assertThat(fixResult).text().isEqualTo("First liAbc\nDefgh"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(8, 2, 8, 9); + } + + @Test + public void replaceWholeContent() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line", 1, 0, 1, 10, "Abc"); + assertThat(fixResult).text().isEqualTo("Abc"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isReplace(0, 10, 0, 3); + } + + @Test + public void deleteWholeContent() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line", 1, 0, 1, 10, ""); + assertThat(fixResult).text().isEqualTo(""); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isDelete(0, 1, 0); + assertThat(edit).internalEdits().onlyElement().isDelete(0, 10, 0); + } + + @Test + public void deleteAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line", 1, 0, 1, 4, ""); + assertThat(fixResult).text().isEqualTo("t line"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isDelete(0, 4, 0); + } + + @Test + public void deleteInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line", 1, 5, 1, 8, ""); + assertThat(fixResult).text().isEqualTo("Firstne"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isDelete(5, 3, 5); + } + + @Test + public void deleteAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line", 1, 7, 1, 10, ""); + assertThat(fixResult).text().isEqualTo("First l"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isDelete(7, 3, 7); + } +}
diff --git a/javatests/com/google/gerrit/server/fixes/fixCalculator/OneLineContentWithEOLTest.java b/javatests/com/google/gerrit/server/fixes/fixCalculator/OneLineContentWithEOLTest.java new file mode 100644 index 0000000..bae714b --- /dev/null +++ b/javatests/com/google/gerrit/server/fixes/fixCalculator/OneLineContentWithEOLTest.java
@@ -0,0 +1,320 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.fixes.fixCalculator; + +import static com.google.gerrit.server.fixes.testing.FixResultSubject.assertThat; +import static com.google.gerrit.server.fixes.testing.GitEditSubject.assertThat; + +import com.google.gerrit.server.fixes.FixCalculator.FixResult; +import org.eclipse.jgit.diff.Edit; +import org.junit.Test; + +public class OneLineContentWithEOLTest { + + @Test + public void insertSingleLineNoEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line\n", 1, 0, 1, 0, "Abc"); + assertThat(fixResult).text().isEqualTo("AbcFirst line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 3); + } + + @Test + public void insertSingleLineNoEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line\n", 1, 5, 1, 5, "Abc"); + assertThat(fixResult).text().isEqualTo("FirstAbc line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(5, 5, 3); + } + + @Test + public void insertSingleLineNoEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line\n", 2, 0, 2, 0, "Abc"); + assertThat(fixResult).text().isEqualTo("First line\nAbc"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isInsert(1, 1, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 3); + } + + @Test + public void insertSingleLineWithEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line\n", 1, 0, 1, 0, "Abc\n"); + assertThat(fixResult).text().isEqualTo("Abc\nFirst line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isInsert(0, 0, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 4); + } + + @Test + public void insertSingleLineWithEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line\n", 1, 5, 1, 5, "Abc\n"); + assertThat(fixResult).text().isEqualTo("FirstAbc\n line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 2); + assertThat(edit).internalEdits().onlyElement().isInsert(5, 5, 4); + } + + @Test + public void insertSingleLineWithEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line\n", 2, 0, 2, 0, "Abc\n"); + assertThat(fixResult).text().isEqualTo("First line\nAbc\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isInsert(1, 1, 1); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 4); + } + + @Test + public void insertMultilineLineWithEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\n", 1, 0, 1, 0, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("Abc\nDefgh\nFirst line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isInsert(0, 0, 2); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 10); + } + + @Test + public void insertMultilineLineWithEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\n", 1, 5, 1, 5, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("FirstAbc\nDefgh\n line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 3); + assertThat(edit).internalEdits().onlyElement().isInsert(5, 5, 10); + } + + @Test + public void insertMultilineLineWithEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\n", 2, 0, 2, 0, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("First line\nAbc\nDefgh\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isInsert(1, 1, 2); + assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 10); + } + + @Test + public void replaceWithSingleLineNoEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line\n", 1, 0, 1, 2, "Abc"); + assertThat(fixResult).text().isEqualTo("Abcrst line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isReplace(0, 2, 0, 3); + } + + @Test + public void replaceWithSingleLineNoEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line\n", 1, 3, 1, 5, "Abc"); + assertThat(fixResult).text().isEqualTo("FirAbc line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isReplace(3, 2, 3, 3); + } + + @Test + public void replaceWithSingleLineNoEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line\n", 1, 9, 2, 0, "Abc"); + assertThat(fixResult).text().isEqualTo("First linAbc"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isReplace(9, 2, 9, 3); + } + + @Test + public void replaceWithSingleLineWithEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line\n", 1, 0, 1, 2, "Abc\n"); + assertThat(fixResult).text().isEqualTo("Abc\nrst line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(0, 2, 0, 4); + } + + @Test + public void replaceWithSingleLineWithEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line\n", 1, 3, 1, 5, "Abc\n"); + assertThat(fixResult).text().isEqualTo("FirAbc\n line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(3, 2, 3, 4); + } + + @Test + public void replaceWithSingleLineWithEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line\n", 1, 8, 2, 0, "Abc\n"); + assertThat(fixResult).text().isEqualTo("First liAbc\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isReplace(8, 3, 8, 4); + } + + @Test + public void replaceMultilineLineWithEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\n", 1, 0, 1, 2, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("Abc\nDefgh\nrst line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 3); + assertThat(edit).internalEdits().onlyElement().isReplace(0, 2, 0, 10); + } + + @Test + public void replaceMultilineLineWithEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\n", 1, 3, 1, 5, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("FirAbc\nDefgh\n line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 3); + assertThat(edit).internalEdits().onlyElement().isReplace(3, 2, 3, 10); + } + + @Test + public void replaceMultilineLineWithEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\n", 1, 8, 2, 0, "Abc\nDefgh\n"); + assertThat(fixResult).text().isEqualTo("First liAbc\nDefgh\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(8, 3, 8, 10); + } + + @Test + public void replaceMultilineLineNoEOLAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\n", 1, 0, 1, 2, "Abc\nDefgh"); + assertThat(fixResult).text().isEqualTo("Abc\nDefghrst line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(0, 2, 0, 9); + } + + @Test + public void replaceMultilineLineNoEOLInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\n", 1, 3, 1, 5, "Abc\nDefgh"); + assertThat(fixResult).text().isEqualTo("FirAbc\nDefgh line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(3, 2, 3, 9); + } + + @Test + public void replaceMultilineLineNoEOLAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement( + "First line\n", 1, 8, 2, 0, "Abc\nDefgh"); + assertThat(fixResult).text().isEqualTo("First liAbc\nDefgh"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 2); + assertThat(edit).internalEdits().onlyElement().isReplace(8, 3, 8, 9); + } + + @Test + public void replaceWholeContent() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line\n", 1, 0, 2, 0, "Abc"); + assertThat(fixResult).text().isEqualTo("Abc"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isReplace(0, 11, 0, 3); + } + + @Test + public void deleteWholeContent() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line\n", 1, 0, 2, 0, ""); + assertThat(fixResult).text().isEqualTo(""); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isDelete(0, 1, 0); + assertThat(edit).internalEdits().onlyElement().isDelete(0, 11, 0); + } + + @Test + public void deleteAtStart() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line\n", 1, 0, 1, 4, ""); + assertThat(fixResult).text().isEqualTo("t line\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isDelete(0, 4, 0); + } + + @Test + public void deleteInTheMiddle() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line\n", 1, 5, 1, 8, ""); + assertThat(fixResult).text().isEqualTo("Firstne\n"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isDelete(5, 3, 5); + } + + @Test + public void deleteAtEnd() throws Exception { + FixResult fixResult = + FixCalculatorVariousTest.calculateFixSingleReplacement("First line\n", 1, 7, 2, 0, ""); + assertThat(fixResult).text().isEqualTo("First l"); + assertThat(fixResult).edits().hasSize(1); + Edit edit = fixResult.edits.get(0); + assertThat(edit).isReplace(0, 1, 0, 1); + assertThat(edit).internalEdits().onlyElement().isDelete(7, 4, 7); + } +}
diff --git a/javatests/com/google/gerrit/server/git/GroupCollectorTest.java b/javatests/com/google/gerrit/server/git/GroupCollectorTest.java index 6175385..63f83b0 100644 --- a/javatests/com/google/gerrit/server/git/GroupCollectorTest.java +++ b/javatests/com/google/gerrit/server/git/GroupCollectorTest.java
@@ -21,10 +21,13 @@ import com.google.common.collect.SortedSetMultimap; import com.google.gerrit.entities.Change; import com.google.gerrit.entities.PatchSet; +import com.google.gerrit.server.git.receive.ReceivePackRefCache; +import java.io.IOException; import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevSort; import org.eclipse.jgit.revwalk.RevWalk; @@ -44,8 +47,7 @@ RevCommit branchTip = tr.commit().create(); RevCommit a = tr.commit().parent(branchTip).create(); - SortedSetMultimap<ObjectId, String> groups = - collectGroups(newWalk(a, branchTip), patchSets(), groups()); + SortedSetMultimap<ObjectId, String> groups = collectGroups(newWalk(a, branchTip), groups()); assertThat(groups).containsEntry(a, a.name()); } @@ -56,8 +58,7 @@ RevCommit a = tr.commit().parent(branchTip).create(); RevCommit b = tr.commit().parent(a).create(); - SortedSetMultimap<ObjectId, String> groups = - collectGroups(newWalk(b, branchTip), patchSets(), groups()); + SortedSetMultimap<ObjectId, String> groups = collectGroups(newWalk(b, branchTip), groups()); assertThat(groups).containsEntry(a, a.name()); assertThat(groups).containsEntry(b, a.name()); @@ -67,12 +68,12 @@ public void commitWhoseParentIsExistingPatchSetGetsParentsGroup() throws Exception { RevCommit branchTip = tr.commit().create(); RevCommit a = tr.commit().parent(branchTip).create(); + createRef(psId(1, 1), a, tr); RevCommit b = tr.commit().parent(a).create(); String group = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; SortedSetMultimap<ObjectId, String> groups = - collectGroups( - newWalk(b, branchTip), patchSets().put(a, psId(1, 1)), groups().put(psId(1, 1), group)); + collectGroups(newWalk(b, branchTip), groups().put(psId(1, 1), group)); assertThat(groups).containsEntry(a, group); assertThat(groups).containsEntry(b, group); @@ -84,8 +85,7 @@ RevCommit a = tr.commit().parent(branchTip).create(); RevCommit b = tr.commit().parent(a).create(); - SortedSetMultimap<ObjectId, String> groups = - collectGroups(newWalk(b, branchTip), patchSets().put(a, psId(1, 1)), groups()); + SortedSetMultimap<ObjectId, String> groups = collectGroups(newWalk(b, branchTip), groups()); assertThat(groups).containsEntry(a, a.name()); assertThat(groups).containsEntry(b, a.name()); @@ -98,8 +98,7 @@ RevCommit b = tr.commit().parent(branchTip).create(); RevCommit m = tr.commit().parent(a).parent(b).create(); - SortedSetMultimap<ObjectId, String> groups = - collectGroups(newWalk(m, branchTip), patchSets(), groups()); + SortedSetMultimap<ObjectId, String> groups = collectGroups(newWalk(m, branchTip), groups()); assertThat(groups).containsEntry(a, a.name()); assertThat(groups).containsEntry(b, a.name()); @@ -110,13 +109,13 @@ public void mergeCommitWhereOneParentHasExistingGroup() throws Exception { RevCommit branchTip = tr.commit().create(); RevCommit a = tr.commit().parent(branchTip).create(); + createRef(psId(1, 1), a, tr); RevCommit b = tr.commit().parent(branchTip).create(); RevCommit m = tr.commit().parent(a).parent(b).create(); String group = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; SortedSetMultimap<ObjectId, String> groups = - collectGroups( - newWalk(m, branchTip), patchSets().put(b, psId(1, 1)), groups().put(psId(1, 1), group)); + collectGroups(newWalk(m, branchTip), groups().put(psId(1, 1), group)); // Merge commit and other parent get the existing group. assertThat(groups).containsEntry(a, group); @@ -128,16 +127,16 @@ public void mergeCommitWhereBothParentsHaveDifferentGroups() throws Exception { RevCommit branchTip = tr.commit().create(); RevCommit a = tr.commit().parent(branchTip).create(); + createRef(psId(1, 1), a, tr); RevCommit b = tr.commit().parent(branchTip).create(); + createRef(psId(2, 1), b, tr); RevCommit m = tr.commit().parent(a).parent(b).create(); String group1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; String group2 = "1234567812345678123456781234567812345678"; SortedSetMultimap<ObjectId, String> groups = collectGroups( - newWalk(m, branchTip), - patchSets().put(a, psId(1, 1)).put(b, psId(2, 1)), - groups().put(psId(1, 1), group1).put(psId(2, 1), group2)); + newWalk(m, branchTip), groups().put(psId(1, 1), group1).put(psId(2, 1), group2)); assertThat(groups).containsEntry(a, group1); assertThat(groups).containsEntry(b, group2); @@ -149,7 +148,9 @@ public void mergeCommitMergesGroupsFromParent() throws Exception { RevCommit branchTip = tr.commit().create(); RevCommit a = tr.commit().parent(branchTip).create(); + createRef(psId(1, 1), a, tr); RevCommit b = tr.commit().parent(branchTip).create(); + createRef(psId(2, 1), b, tr); RevCommit m = tr.commit().parent(a).parent(b).create(); String group1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; @@ -158,7 +159,6 @@ SortedSetMultimap<ObjectId, String> groups = collectGroups( newWalk(m, branchTip), - patchSets().put(a, psId(1, 1)).put(b, psId(2, 1)), groups().put(psId(1, 1), group1).put(psId(2, 1), group2a).put(psId(2, 1), group2b)); assertThat(groups).containsEntry(a, group1); @@ -171,12 +171,12 @@ public void mergeCommitWithOneUninterestingParentAndOtherParentIsExisting() throws Exception { RevCommit branchTip = tr.commit().create(); RevCommit a = tr.commit().parent(branchTip).create(); + createRef(psId(1, 1), a, tr); RevCommit m = tr.commit().parent(branchTip).parent(a).create(); String group = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; SortedSetMultimap<ObjectId, String> groups = - collectGroups( - newWalk(m, branchTip), patchSets().put(a, psId(1, 1)), groups().put(psId(1, 1), group)); + collectGroups(newWalk(m, branchTip), groups().put(psId(1, 1), group)); assertThat(groups).containsEntry(a, group); assertThat(groups).containsEntry(m, group); @@ -188,8 +188,7 @@ RevCommit a = tr.commit().parent(branchTip).create(); RevCommit m = tr.commit().parent(branchTip).parent(a).create(); - SortedSetMultimap<ObjectId, String> groups = - collectGroups(newWalk(m, branchTip), patchSets(), groups()); + SortedSetMultimap<ObjectId, String> groups = collectGroups(newWalk(m, branchTip), groups()); assertThat(groups).containsEntry(a, a.name()); assertThat(groups).containsEntry(m, a.name()); @@ -204,8 +203,7 @@ RevCommit m1 = tr.commit().parent(b).parent(c).create(); RevCommit m2 = tr.commit().parent(a).parent(m1).create(); - SortedSetMultimap<ObjectId, String> groups = - collectGroups(newWalk(m2, branchTip), patchSets(), groups()); + SortedSetMultimap<ObjectId, String> groups = collectGroups(newWalk(m2, branchTip), groups()); assertThat(groups).containsEntry(a, a.name()); assertThat(groups).containsEntry(b, a.name()); @@ -223,8 +221,7 @@ assertThat(m.getParentCount()).isEqualTo(2); assertThat(m.getParent(0)).isEqualTo(m.getParent(1)); - SortedSetMultimap<ObjectId, String> groups = - collectGroups(newWalk(m, branchTip), patchSets(), groups()); + SortedSetMultimap<ObjectId, String> groups = collectGroups(newWalk(m, branchTip), groups()); assertThat(groups).containsEntry(a, a.name()); assertThat(groups).containsEntry(m, a.name()); @@ -234,7 +231,9 @@ public void mergeCommitWithOneNewParentAndTwoExistingPatchSets() throws Exception { RevCommit branchTip = tr.commit().create(); RevCommit a = tr.commit().parent(branchTip).create(); + createRef(psId(1, 1), a, tr); RevCommit b = tr.commit().parent(branchTip).create(); + createRef(psId(2, 1), b, tr); RevCommit c = tr.commit().parent(b).create(); RevCommit m = tr.commit().parent(a).parent(c).create(); @@ -242,9 +241,7 @@ String group2 = "1234567812345678123456781234567812345678"; SortedSetMultimap<ObjectId, String> groups = collectGroups( - newWalk(m, branchTip), - patchSets().put(a, psId(1, 1)).put(b, psId(2, 1)), - groups().put(psId(1, 1), group1).put(psId(2, 1), group2)); + newWalk(m, branchTip), groups().put(psId(1, 1), group1).put(psId(2, 1), group2)); assertThat(groups).containsEntry(a, group1); assertThat(groups).containsEntry(b, group2); @@ -264,16 +261,7 @@ rw.markStart(rw.parseCommit(d)); // Schema upgrade case: all commits are existing patch sets, but none have // groups assigned yet. - SortedSetMultimap<ObjectId, String> groups = - collectGroups( - rw, - patchSets() - .put(branchTip, psId(1, 1)) - .put(a, psId(2, 1)) - .put(b, psId(3, 1)) - .put(c, psId(4, 1)) - .put(d, psId(5, 1)), - groups()); + SortedSetMultimap<ObjectId, String> groups = collectGroups(rw, groups()); assertThat(groups).containsEntry(a, a.name()); assertThat(groups).containsEntry(b, a.name()); @@ -287,6 +275,13 @@ return PatchSet.id(Change.id(c), p); } + private static void createRef(PatchSet.Id psId, ObjectId id, TestRepository<?> tr) + throws IOException { + RefUpdate ru = tr.getRepository().updateRef(psId.toRefName()); + ru.setNewObjectId(id); + assertThat(ru.update()).isEqualTo(RefUpdate.Result.NEW); + } + private RevWalk newWalk(ObjectId start, ObjectId branchTip) throws Exception { // Match RevWalk conditions from ReceiveCommits. RevWalk rw = new RevWalk(tr.getRepository()); @@ -297,12 +292,12 @@ return rw; } - private static SortedSetMultimap<ObjectId, String> collectGroups( - RevWalk rw, - ImmutableListMultimap.Builder<ObjectId, PatchSet.Id> patchSetsBySha, - ImmutableListMultimap.Builder<PatchSet.Id, String> groupLookup) - throws Exception { - GroupCollector gc = new GroupCollector(patchSetsBySha.build(), groupLookup.build()); + private SortedSetMultimap<ObjectId, String> collectGroups( + RevWalk rw, ImmutableListMultimap.Builder<PatchSet.Id, String> groupLookup) throws Exception { + ImmutableListMultimap<PatchSet.Id, String> groups = groupLookup.build(); + GroupCollector gc = + new GroupCollector( + ReceivePackRefCache.noCache(tr.getRepository().getRefDatabase()), (s) -> groups.get(s)); RevCommit c; while ((c = rw.next()) != null) { gc.visit(c); @@ -310,12 +305,6 @@ return gc.getGroups(); } - // Helper methods for constructing various map arguments, to avoid lots of - // type specifications. - private static ImmutableListMultimap.Builder<ObjectId, PatchSet.Id> patchSets() { - return ImmutableListMultimap.builder(); - } - private static ImmutableListMultimap.Builder<PatchSet.Id, String> groups() { return ImmutableListMultimap.builder(); }
diff --git a/javatests/com/google/gerrit/server/git/receive/ReceivePackRefCacheTest.java b/javatests/com/google/gerrit/server/git/receive/ReceivePackRefCacheTest.java new file mode 100644 index 0000000..698acd8 --- /dev/null +++ b/javatests/com/google/gerrit/server/git/receive/ReceivePackRefCacheTest.java
@@ -0,0 +1,140 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.git.receive; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.gerrit.entities.Change; +import com.google.gerrit.entities.RefNames; +import java.util.Map; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; +import org.junit.Test; + +/** Tests for {@link ReceivePackRefCache}. */ +public class ReceivePackRefCacheTest { + + @Test + public void noCache_prefixDelegatesToRefDb() throws Exception { + Ref ref = newRef("refs/changes/01/1/1", "badc0feebadc0feebadc0feebadc0feebadc0fee"); + RefDatabase mockRefDb = mock(RefDatabase.class); + ReceivePackRefCache cache = ReceivePackRefCache.noCache(mockRefDb); + when(mockRefDb.getRefsByPrefix(RefNames.REFS_HEADS)).thenReturn(ImmutableList.of(ref)); + + assertThat(cache.byPrefix(RefNames.REFS_HEADS)).containsExactly(ref); + verify(mockRefDb).getRefsByPrefix(RefNames.REFS_HEADS); + verifyNoMoreInteractions(mockRefDb); + } + + @Test + public void noCache_exactRefDelegatesToRefDb() throws Exception { + Ref ref = newRef("refs/changes/01/1/1", "badc0feebadc0feebadc0feebadc0feebadc0fee"); + RefDatabase mockRefDb = mock(RefDatabase.class); + ReceivePackRefCache cache = ReceivePackRefCache.noCache(mockRefDb); + when(mockRefDb.exactRef("refs/heads/master")).thenReturn(ref); + + assertThat(cache.exactRef("refs/heads/master")).isEqualTo(ref); + verify(mockRefDb).exactRef("refs/heads/master"); + verifyNoMoreInteractions(mockRefDb); + } + + @Test + public void noCache_tipsFromObjectIdDelegatesToRefDbAndFiltersByPrefix() throws Exception { + Ref refBla = newRef("refs/bla", "badc0feebadc0feebadc0feebadc0feebadc0fee"); + Ref refheads = newRef(RefNames.REFS_HEADS, "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); + + RefDatabase mockRefDb = mock(RefDatabase.class); + ReceivePackRefCache cache = ReceivePackRefCache.noCache(mockRefDb); + when(mockRefDb.getTipsWithSha1(ObjectId.zeroId())) + .thenReturn(ImmutableSet.of(refBla, refheads)); + + assertThat(cache.tipsFromObjectId(ObjectId.zeroId(), RefNames.REFS_HEADS)) + .containsExactly(refheads); + verify(mockRefDb).getTipsWithSha1(ObjectId.zeroId()); + verifyNoMoreInteractions(mockRefDb); + } + + @Test + public void advertisedRefs_prefixScans() throws Exception { + Ref refBla = + new ObjectIdRef.Unpeeled( + Ref.Storage.NEW, + "refs/bla/1", + ObjectId.fromString("badc0feebadc0feebadc0feebadc0feebadc0fee"), + 1); + ReceivePackRefCache cache = + ReceivePackRefCache.withAdvertisedRefs(() -> ImmutableMap.of(refBla.getName(), refBla)); + + assertThat(cache.byPrefix("refs/bla")).containsExactly(refBla); + } + + @Test + public void advertisedRefs_prefixScansChangeId() throws Exception { + Map<String, Ref> refs = setupTwoChanges(); + ReceivePackRefCache cache = ReceivePackRefCache.withAdvertisedRefs(() -> refs); + + assertThat(cache.byPrefix(RefNames.changeRefPrefix(Change.id(1)))) + .containsExactly(refs.get("refs/changes/01/1/1")); + } + + @Test + public void advertisedRefs_exactRef() throws Exception { + Map<String, Ref> refs = setupTwoChanges(); + ReceivePackRefCache cache = ReceivePackRefCache.withAdvertisedRefs(() -> refs); + + assertThat(cache.exactRef("refs/changes/01/1/1")).isEqualTo(refs.get("refs/changes/01/1/1")); + } + + @Test + public void advertisedRefs_tipsFromObjectIdWithNoPrefix() throws Exception { + Map<String, Ref> refs = setupTwoChanges(); + ReceivePackRefCache cache = ReceivePackRefCache.withAdvertisedRefs(() -> refs); + + assertThat( + cache.tipsFromObjectId( + ObjectId.fromString("badc0feebadc0feebadc0feebadc0feebadc0fee"), null)) + .containsExactly(refs.get("refs/changes/01/1/1")); + } + + @Test + public void advertisedRefs_tipsFromObjectIdWithPrefix() throws Exception { + Map<String, Ref> refs = setupTwoChanges(); + ReceivePackRefCache cache = ReceivePackRefCache.withAdvertisedRefs(() -> refs); + + assertThat( + cache.tipsFromObjectId( + ObjectId.fromString("badc0feebadc0feebadc0feebadc0feebadc0fee"), "/refs/some")) + .isEmpty(); + } + + private static Ref newRef(String name, String sha1) { + return new ObjectIdRef.Unpeeled(Ref.Storage.NEW, name, ObjectId.fromString(sha1), 1); + } + + private Map<String, Ref> setupTwoChanges() { + Ref ref1 = newRef("refs/changes/01/1/1", "badc0feebadc0feebadc0feebadc0feebadc0fee"); + Ref ref2 = newRef("refs/changes/02/2/1", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); + return ImmutableMap.of(ref1.getName(), ref1, ref2.getName(), ref2); + } +}
diff --git a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java index 0753127..3dfbefe 100644 --- a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java +++ b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
@@ -18,6 +18,7 @@ import com.google.gerrit.index.query.Predicate; import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.ChangeQueryBuilder; +import org.eclipse.jgit.lib.Config; import org.junit.Ignore; @Ignore @@ -26,8 +27,34 @@ super( new ChangeQueryBuilder.Definition<>(FakeQueryBuilder.class), new ChangeQueryBuilder.Arguments( - null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, indexes, null, null, null, null, null, null, null)); + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + indexes, + null, + null, + null, + null, + null, + null, + null, + null, + new Config())); } @Operator
diff --git a/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java b/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java index c887875..6f40680 100644 --- a/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java +++ b/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
@@ -163,34 +163,37 @@ // Not stale. assertThat( refsAreStale( - repoManager, - C, - ImmutableSetMultimap.of( - P1, RefState.create(ref1, id1.name()), - P2, RefState.create(ref2, id2.name())), - ImmutableListMultimap.of())) + repoManager, + C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, id1.name()), + P2, RefState.create(ref2, id2.name())), + ImmutableListMultimap.of()) + .isStale()) .isFalse(); // Wrong ref value. assertThat( refsAreStale( - repoManager, - C, - ImmutableSetMultimap.of( - P1, RefState.create(ref1, SHA1), - P2, RefState.create(ref2, id2.name())), - ImmutableListMultimap.of())) + repoManager, + C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, SHA1), + P2, RefState.create(ref2, id2.name())), + ImmutableListMultimap.of()) + .isStale()) .isTrue(); // Swapped repos. assertThat( refsAreStale( - repoManager, - C, - ImmutableSetMultimap.of( - P1, RefState.create(ref1, id2.name()), - P2, RefState.create(ref2, id1.name())), - ImmutableListMultimap.of())) + repoManager, + C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, id2.name()), + P2, RefState.create(ref2, id1.name())), + ImmutableListMultimap.of()) + .isStale()) .isTrue(); // Two refs in same repo, not stale. @@ -199,32 +202,35 @@ tr1.update(ref3, id3); assertThat( refsAreStale( - repoManager, - C, - ImmutableSetMultimap.of( - P1, RefState.create(ref1, id1.name()), - P1, RefState.create(ref3, id3.name())), - ImmutableListMultimap.of())) + repoManager, + C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, id1.name()), + P1, RefState.create(ref3, id3.name())), + ImmutableListMultimap.of()) + .isStale()) .isFalse(); // Ignore ref not mentioned. assertThat( refsAreStale( - repoManager, - C, - ImmutableSetMultimap.of(P1, RefState.create(ref1, id1.name())), - ImmutableListMultimap.of())) + repoManager, + C, + ImmutableSetMultimap.of(P1, RefState.create(ref1, id1.name())), + ImmutableListMultimap.of()) + .isStale()) .isFalse(); // One ref wrong. assertThat( refsAreStale( - repoManager, - C, - ImmutableSetMultimap.of( - P1, RefState.create(ref1, id1.name()), - P1, RefState.create(ref3, SHA1)), - ImmutableListMultimap.of())) + repoManager, + C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, id1.name()), + P1, RefState.create(ref3, SHA1)), + ImmutableListMultimap.of()) + .isStale()) .isTrue(); } @@ -236,10 +242,11 @@ // ref1 is only ref matching pattern. assertThat( refsAreStale( - repoManager, - C, - ImmutableSetMultimap.of(P1, RefState.create(ref1, id1.name())), - ImmutableListMultimap.of(P1, RefStatePattern.create("refs/heads/*")))) + repoManager, + C, + ImmutableSetMultimap.of(P1, RefState.create(ref1, id1.name())), + ImmutableListMultimap.of(P1, RefStatePattern.create("refs/heads/*"))) + .isStale()) .isFalse(); // Now ref2 matches pattern, so stale unless ref2 is present in state map. @@ -247,19 +254,21 @@ ObjectId id2 = tr1.update(ref2, tr1.commit().message("commit 2")); assertThat( refsAreStale( - repoManager, - C, - ImmutableSetMultimap.of(P1, RefState.create(ref1, id1.name())), - ImmutableListMultimap.of(P1, RefStatePattern.create("refs/heads/*")))) + repoManager, + C, + ImmutableSetMultimap.of(P1, RefState.create(ref1, id1.name())), + ImmutableListMultimap.of(P1, RefStatePattern.create("refs/heads/*"))) + .isStale()) .isTrue(); assertThat( refsAreStale( - repoManager, - C, - ImmutableSetMultimap.of( - P1, RefState.create(ref1, id1.name()), - P1, RefState.create(ref2, id2.name())), - ImmutableListMultimap.of(P1, RefStatePattern.create("refs/heads/*")))) + repoManager, + C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, id1.name()), + P1, RefState.create(ref2, id2.name())), + ImmutableListMultimap.of(P1, RefStatePattern.create("refs/heads/*"))) + .isStale()) .isFalse(); } @@ -272,10 +281,11 @@ // ref1 is only ref matching pattern. assertThat( refsAreStale( - repoManager, - C, - ImmutableSetMultimap.of(P1, RefState.create(ref1, id1.name())), - ImmutableListMultimap.of(P1, RefStatePattern.create("refs/*/foo")))) + repoManager, + C, + ImmutableSetMultimap.of(P1, RefState.create(ref1, id1.name())), + ImmutableListMultimap.of(P1, RefStatePattern.create("refs/*/foo"))) + .isStale()) .isFalse(); // Now ref2 matches pattern, so stale unless ref2 is present in state map. @@ -283,19 +293,21 @@ ObjectId id3 = tr1.update(ref3, tr1.commit().message("commit 3")); assertThat( refsAreStale( - repoManager, - C, - ImmutableSetMultimap.of(P1, RefState.create(ref1, id1.name())), - ImmutableListMultimap.of(P1, RefStatePattern.create("refs/*/foo")))) + repoManager, + C, + ImmutableSetMultimap.of(P1, RefState.create(ref1, id1.name())), + ImmutableListMultimap.of(P1, RefStatePattern.create("refs/*/foo"))) + .isStale()) .isTrue(); assertThat( refsAreStale( - repoManager, - C, - ImmutableSetMultimap.of( - P1, RefState.create(ref1, id1.name()), - P1, RefState.create(ref3, id3.name())), - ImmutableListMultimap.of(P1, RefStatePattern.create("refs/*/foo")))) + repoManager, + C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, id1.name()), + P1, RefState.create(ref3, id3.name())), + ImmutableListMultimap.of(P1, RefStatePattern.create("refs/*/foo"))) + .isStale()) .isFalse(); }
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java index 145e914..db0dec8 100644 --- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java +++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -45,6 +45,7 @@ import com.google.gerrit.entities.CommentRange; import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.PatchSetApproval; +import com.google.gerrit.entities.SubmissionId; import com.google.gerrit.exceptions.StorageException; import com.google.gerrit.mail.Address; import com.google.gerrit.server.AssigneeStatusUpdate; @@ -52,7 +53,6 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.config.GerritServerId; -import com.google.gerrit.server.logging.RequestId; import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk; import com.google.gerrit.server.util.time.TimeUtil; import com.google.gerrit.testing.TestChanges; @@ -432,7 +432,7 @@ @Test public void approvalsPostSubmit() throws Exception { Change c = newChange(); - RequestId submissionId = submissionId(c); + SubmissionId submissionId = new SubmissionId(c); ChangeUpdate update = newUpdate(c, changeOwner); update.putApproval("Code-Review", (short) 1); update.putApproval("Verified", (short) 1); @@ -467,7 +467,7 @@ @Test public void approvalsDuringSubmit() throws Exception { Change c = newChange(); - RequestId submissionId = submissionId(c); + SubmissionId submissionId = new SubmissionId(c); ChangeUpdate update = newUpdate(c, changeOwner); update.putApproval("Code-Review", (short) 1); update.putApproval("Verified", (short) 1); @@ -604,7 +604,7 @@ @Test public void submitRecords() throws Exception { Change c = newChange(); - RequestId submissionId = submissionId(c); + SubmissionId submissionId = new SubmissionId(c); ChangeUpdate update = newUpdate(c, changeOwner); update.setSubjectForCommit("Submit patch set 1"); @@ -640,13 +640,13 @@ null, submitLabel("Verified", "OK", changeOwner.getAccountId()), submitLabel("Alternative-Code-Review", "NEED", null))); - assertThat(notes.getChange().getSubmissionId()).isEqualTo(submissionId.toStringForStorage()); + assertThat(notes.getChange().getSubmissionId()).isEqualTo(submissionId.toString()); } @Test public void latestSubmitRecordsOnly() throws Exception { Change c = newChange(); - RequestId submissionId = submissionId(c); + SubmissionId submissionId = new SubmissionId(c); ChangeUpdate update = newUpdate(c, changeOwner); update.setSubjectForCommit("Submit patch set 1"); update.merge( @@ -669,7 +669,7 @@ assertThat(notes.getSubmitRecords()) .containsExactly( submitRecord("OK", null, submitLabel("Code-Review", "OK", changeOwner.getAccountId()))); - assertThat(notes.getChange().getSubmissionId()).isEqualTo(submissionId.toStringForStorage()); + assertThat(notes.getChange().getSubmissionId()).isEqualTo(submissionId.toString()); } @Test @@ -977,7 +977,7 @@ // Finish off by merging the change. update = newUpdate(c, changeOwner); update.merge( - submissionId(c), + new SubmissionId(c), ImmutableList.of( submitRecord( "NOT_READY", @@ -3140,8 +3140,4 @@ update.commit(); return tr.parseBody(commit); } - - private RequestId submissionId(Change c) { - return new RequestId(c.getId().toString()); - } }
diff --git a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java index 97781a4..5e25c13 100644 --- a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java +++ b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -21,9 +21,9 @@ import com.google.common.collect.ImmutableList; import com.google.gerrit.entities.Account; import com.google.gerrit.entities.Change; +import com.google.gerrit.entities.SubmissionId; import com.google.gerrit.mail.Address; import com.google.gerrit.server.CurrentUser; -import com.google.gerrit.server.logging.RequestId; import com.google.gerrit.server.util.time.TimeUtil; import com.google.gerrit.testing.ConfigSuite; import com.google.gerrit.testing.TestChanges; @@ -151,7 +151,7 @@ ChangeUpdate update = newUpdate(c, changeOwner); update.setSubjectForCommit("Submit patch set 1"); - RequestId submissionId = submissionId(c); + SubmissionId submissionId = new SubmissionId(c); update.merge( submissionId, ImmutableList.of( @@ -174,7 +174,7 @@ + "Patch-set: 1\n" + "Status: merged\n" + "Submission-id: " - + submissionId.toStringForStorage() + + submissionId.toString() + "\n" + "Submitted-with: NOT_READY\n" + "Submitted-with: OK: Verified: Gerrit User 1 <1@gerrit>\n" @@ -223,7 +223,7 @@ ChangeUpdate update = newUpdate(c, changeOwner); update.setSubjectForCommit("Submit patch set 1"); - RequestId submissionId = submissionId(c); + SubmissionId submissionId = new SubmissionId(c); update.merge( submissionId, ImmutableList.of(submitRecord("RULE_ERROR", "Problem with patch set:\n1"))); update.commit(); @@ -234,7 +234,7 @@ + "Patch-set: 1\n" + "Status: merged\n" + "Submission-id: " - + submissionId.toStringForStorage() + + submissionId.toString() + "\n" + "Submitted-with: RULE_ERROR Problem with patch set: 1\n", update.getResult()); @@ -427,8 +427,4 @@ RevCommit commit = parseCommit(commitId); assertThat(commit.getFullMessage()).isEqualTo(expected); } - - private RequestId submissionId(Change c) { - return new RequestId(c.getId().toString()); - } }
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java index 2c5fcc4..9c12052 100644 --- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java +++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -41,6 +41,7 @@ import com.google.common.collect.Lists; import com.google.common.collect.Streams; import com.google.common.truth.ThrowableSubject; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.LabelType; @@ -79,6 +80,7 @@ import com.google.gerrit.index.IndexConfig; import com.google.gerrit.index.Schema; import com.google.gerrit.index.query.Predicate; +import com.google.gerrit.index.query.QueryParseException; import com.google.gerrit.lifecycle.LifecycleManager; import com.google.gerrit.server.AnonymousUser; import com.google.gerrit.server.CurrentUser; @@ -2023,6 +2025,7 @@ } @Test + @GerritConfig(name = "index.change.indexMergeable", value = "true") public void mergeable() throws Exception { TestRepository<Repo> repo = createProject("repo"); RevCommit commit1 = repo.parseBody(repo.commit().add("file1", "contents1").create()); @@ -2040,7 +2043,7 @@ // If a change gets submitted, the remaining open changes get reindexed asynchronously to update // their mergeability information. If the further assertions in this test are done before the // asynchronous reindex completed they fail because the mergeability information in the index - // was not updated yet. To avoid this flakiness reindexAfterRefUpdate is switched off for the + // was not updated yet. To avoid this flakiness indexMergeable is switched off for the // tests and we index change2 synchronously here. gApi.changes().id(change2.getChangeId()).index(); @@ -2502,6 +2505,19 @@ assertQueryByIds("revertof:" + changeToRevert._number, Change.id(changeThatReverts._number)); } + @Test + public void submissionId() throws Exception { + TestRepository<Repo> repo = createProject("repo"); + Change change = insert(repo, newChange(repo)); + // create irrelevant change + 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; + + assertQueryByIds("submissionid:" + submissionId, change.getId()); + } + /** Change builder for helping in tests for dashboard sections. */ protected class DashboardChangeState { private final Account.Id ownerId; @@ -3066,6 +3082,20 @@ } } + @Test + @GerritConfig(name = "index.change.indexMergeable", value = "false") + public void mergeableFailsWhenNotIndexed() throws Exception { + TestRepository<Repo> repo = createProject("repo"); + RevCommit commit1 = repo.parseBody(repo.commit().add("file1", "contents1").create()); + 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"); + } + protected ChangeInserter newChange(TestRepository<Repo> repo) throws Exception { return newChange(repo, null, null, null, null, false); }
diff --git a/javatests/com/google/gerrit/server/query/change/BUILD b/javatests/com/google/gerrit/server/query/change/BUILD index d0162d3..e5b51e7 100644 --- a/javatests/com/google/gerrit/server/query/change/BUILD +++ b/javatests/com/google/gerrit/server/query/change/BUILD
@@ -16,12 +16,14 @@ "//prolog:gerrit-prolog-common", ], deps = [ + "//java/com/google/gerrit/acceptance/config", "//java/com/google/gerrit/acceptance/testsuite/project", "//java/com/google/gerrit/common:annotations", "//java/com/google/gerrit/common:server", "//java/com/google/gerrit/entities", "//java/com/google/gerrit/extensions:api", "//java/com/google/gerrit/index", + "//java/com/google/gerrit/index:query_exception", "//java/com/google/gerrit/lifecycle", "//java/com/google/gerrit/server", "//java/com/google/gerrit/server/project/testing:project-test-util",
diff --git a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java index a2f800a..c7ca887 100644 --- a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java +++ b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
@@ -26,16 +26,15 @@ import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.Project; import com.google.gerrit.entities.RefNames; +import com.google.gerrit.entities.SubmissionId; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.change.ChangeInserter; import com.google.gerrit.server.change.PatchSetInserter; import com.google.gerrit.server.git.GitRepositoryManager; -import com.google.gerrit.server.logging.RequestId; 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.notedb.TooManyUpdatesException; import com.google.gerrit.server.util.time.TimeUtil; import com.google.gerrit.testing.InMemoryTestEnvironment; import com.google.inject.Inject; @@ -51,6 +50,7 @@ public class BatchUpdateTest { private static final int MAX_UPDATES = 4; + private static final int MAX_PATCH_SETS = 3; @Rule public InMemoryTestEnvironment testEnvironment = @@ -58,6 +58,7 @@ () -> { Config cfg = new Config(); cfg.setInt("change", null, "maxUpdates", MAX_UPDATES); + cfg.setInt("change", null, "maxPatchSets", MAX_PATCH_SETS); return cfg; }); @@ -106,11 +107,10 @@ ObjectId oldMetaId = getMetaId(id); try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) { bu.addOp(id, new AddMessageOp("Excessive update")); - ResourceConflictException thrown = - assertThrows(ResourceConflictException.class, () -> bu.execute()); + ResourceConflictException thrown = assertThrows(ResourceConflictException.class, bu::execute); assertThat(thrown) .hasMessageThat() - .isEqualTo(TooManyUpdatesException.message(id, MAX_UPDATES)); + .contains("Change " + id + " may not exceed " + MAX_UPDATES); } assertThat(getUpdateCount(id)).isEqualTo(MAX_UPDATES); assertThat(getMetaId(id)).isEqualTo(oldMetaId); @@ -118,17 +118,16 @@ @Test public void cannotExceedMaxUpdatesCountingMultipleChangeUpdatesInSingleBatch() throws Exception { - Change.Id id = createChangeWithTwoPatchSets(MAX_UPDATES - 1); + Change.Id id = createChangeWithPatchSets(2); ObjectId oldMetaId = getMetaId(id); try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) { bu.addOp(id, new AddMessageOp("Update on PS1", PatchSet.id(id, 1))); bu.addOp(id, new AddMessageOp("Update on PS2", PatchSet.id(id, 2))); - ResourceConflictException thrown = - assertThrows(ResourceConflictException.class, () -> bu.execute()); + ResourceConflictException thrown = assertThrows(ResourceConflictException.class, bu::execute); assertThat(thrown) .hasMessageThat() - .isEqualTo(TooManyUpdatesException.message(id, MAX_UPDATES)); + .contains("Change " + id + " may not exceed " + MAX_UPDATES); } assertThat(getUpdateCount(id)).isEqualTo(MAX_UPDATES - 1); assertThat(getMetaId(id)).isEqualTo(oldMetaId); @@ -187,7 +186,7 @@ @Test public void exceedingMaxUpdatesAllowedWithSubmitAfterOtherOp() throws Exception { - Change.Id id = createChangeWithTwoPatchSets(MAX_UPDATES - 1); + Change.Id id = createChangeWithPatchSets(2); ObjectId oldMetaId = getMetaId(id); try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) { bu.addOp(id, new AddMessageOp("Message on PS1", PatchSet.id(id, 1))); @@ -222,6 +221,28 @@ assertThat(getMetaId(id)).isNotEqualTo(oldMetaId); } + @Test + public void limitPatchSetCount_exceed() throws Exception { + Change.Id changeId = createChangeWithPatchSets(MAX_PATCH_SETS); + ObjectId oldMetaId = getMetaId(changeId); + ChangeNotes notes = changeNotesFactory.create(project, changeId); + try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) { + ObjectId commitId = + repo.amend(notes.getCurrentPatchSet().commitId()).message("kaboom").create(); + bu.addOp( + changeId, + patchSetInserterFactory + .create(notes, PatchSet.id(changeId, MAX_PATCH_SETS + 1), commitId) + .setMessage("kaboom")); + ResourceConflictException thrown = assertThrows(ResourceConflictException.class, bu::execute); + assertThat(thrown) + .hasMessageThat() + .contains("Change " + changeId + " may not exceed " + MAX_PATCH_SETS + " patch sets"); + } + assertThat(changeNotesFactory.create(project, changeId).getPatchSets()).hasSize(MAX_PATCH_SETS); + assertThat(getMetaId(changeId)).isEqualTo(oldMetaId); + } + private Change.Id createChangeWithUpdates(int totalUpdates) throws Exception { checkArgument(totalUpdates > 0); checkArgument(totalUpdates <= MAX_UPDATES); @@ -243,21 +264,22 @@ return id; } - private Change.Id createChangeWithTwoPatchSets(int totalUpdates) throws Exception { - Change.Id id = createChangeWithUpdates(totalUpdates - 1); + private Change.Id createChangeWithPatchSets(int patchSets) throws Exception { + checkArgument(patchSets >= 2); + Change.Id id = createChangeWithUpdates(MAX_UPDATES - 2); ChangeNotes notes = changeNotesFactory.create(project, id); - - try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) { - ObjectId commitId = repo.amend(notes.getCurrentPatchSet().commitId()).message("PS2").create(); - bu.addOp( - id, - patchSetInserterFactory - .create(notes, PatchSet.id(id, 2), commitId) - .setMessage("Add PS2")); - bu.execute(); + for (int i = 2; i <= patchSets; ++i) { + try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) { + ObjectId commitId = + repo.amend(notes.getCurrentPatchSet().commitId()).message("PS" + i).create(); + bu.addOp( + id, + patchSetInserterFactory + .create(notes, PatchSet.id(id, i), commitId) + .setMessage("Add PS" + i)); + bu.execute(); + } } - - assertThat(getUpdateCount(id)).isEqualTo(totalUpdates); return id; } @@ -291,7 +313,7 @@ } } - private int getUpdateCount(Change.Id changeId) throws Exception { + private int getUpdateCount(Change.Id changeId) { return changeNotesFactory.create(project, changeId).getUpdateCount(); } @@ -310,7 +332,7 @@ cr.label = "Code-Review"; sr.labels = ImmutableList.of(cr); ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId()); - update.merge(new RequestId(), ImmutableList.of(sr)); + update.merge(new SubmissionId(ctx.getChange()), ImmutableList.of(sr)); update.setChangeMessage("Submitted"); return true; }
diff --git a/javatests/com/google/gerrit/server/util/git/BUILD b/javatests/com/google/gerrit/server/util/git/BUILD index 883898f..b789dff 100644 --- a/javatests/com/google/gerrit/server/util/git/BUILD +++ b/javatests/com/google/gerrit/server/util/git/BUILD
@@ -18,7 +18,6 @@ "//lib:protobuf", "//lib/auto:auto-value", "//lib/auto:auto-value-annotations", - "//lib/commons:codec", "//lib/guice", "//lib/truth", "//lib/truth:truth-java8-extension",
diff --git a/lib/BUILD b/lib/BUILD index 39c622a..199f4fb 100644 --- a/lib/BUILD +++ b/lib/BUILD
@@ -528,7 +528,7 @@ "//java/com/google/gerrit/acceptance:__pkg__", "//java/com/google/gerrit/extensions:__pkg__", "//java/com/google/gerrit/server:__pkg__", - "//plugins:__pkg__", + "//plugins:__subpackages__", ], exports = ["@javax-annotation//jar"], )
diff --git a/lib/jetty/BUILD b/lib/jetty/BUILD index 6417385..fe07794 100644 --- a/lib/jetty/BUILD +++ b/lib/jetty/BUILD
@@ -21,7 +21,6 @@ data = ["//lib:LICENSE-Apache2.0"], visibility = ["//visibility:public"], exports = [ - ":continuation", ":http", "@jetty-server//jar", ], @@ -32,20 +31,12 @@ data = ["//lib:LICENSE-Apache2.0"], visibility = ["//visibility:public"], exports = [ - ":continuation", ":http", "@jetty-jmx//jar", ], ) java_library( - name = "continuation", - data = ["//lib:LICENSE-Apache2.0"], - visibility = ["//visibility:public"], - exports = ["@jetty-continuation//jar"], -) - -java_library( name = "http", data = ["//lib:LICENSE-Apache2.0"], visibility = ["//visibility:public"],
diff --git a/modules/jgit b/modules/jgit index 9710c62..a7e454b 160000 --- a/modules/jgit +++ b/modules/jgit
@@ -1 +1 @@ -Subproject commit 9710c6292ab0c8db28c6e3051ff7f1cca9e9a031 +Subproject commit a7e454bc51d359c2d46b19fd559f770cad8fd7d4
diff --git a/package.json b/package.json index 6b9a38d..600f1ab 100644 --- a/package.json +++ b/package.json
@@ -4,18 +4,20 @@ "description": "Gerrit Code Review", "dependencies": {}, "devDependencies": { - "eslint": "^5.16.0", + "eslint": "^6.6.0", "eslint-config-google": "^0.13.0", - "eslint-plugin-html": "^5.0.5", + "eslint-plugin-html": "^6.0.0", + "eslint-plugin-jsdoc": "^18.4.3", "fried-twinkie": "^0.2.2", - "polylint": "^2.10.4", + "polymer-cli": "^1.9.11", "typescript": "^2.x.x", "web-component-tester": "^6.5.0" }, "scripts": { "start": "polygerrit-ui/run-server.sh", "test": "WCT_HEADLESS_MODE=1 WCT_ARGS='--verbose -l chrome' ./polygerrit-ui/app/run_test.sh", - "eslint": "./node_modules/eslint/bin/eslint.js --ignore-pattern 'bower_components/' --ignore-pattern 'gr-linked-text' --ignore-pattern 'scripts/vendor' --ext .html,.js polygerrit-ui/app || exit 0", + "eslint": "./node_modules/eslint/bin/eslint.js --ext .html,.js polygerrit-ui/app", + "eslintfix": "npm run eslint -- --fix", "test-template": "./polygerrit-ui/app/run_template_test.sh", "polylint": "bazel test polygerrit-ui/app:polylint_test" },
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor index 92ce310..9508693 160000 --- a/plugins/codemirror-editor +++ b/plugins/codemirror-editor
@@ -1 +1 @@ -Subproject commit 92ce310ecf717133601b9e824c38bc5e5eafecba +Subproject commit 9508693bd0a85560674143256314beac08f2d8ca
diff --git a/plugins/delete-project b/plugins/delete-project index 4ffca21..39dd25c 160000 --- a/plugins/delete-project +++ b/plugins/delete-project
@@ -1 +1 @@ -Subproject commit 4ffca21d1a581b61a1ff94acacb9249089b573b8 +Subproject commit 39dd25c822be14a69282d38e5205f0ca4f2902c1
diff --git a/plugins/gitiles b/plugins/gitiles index 0f8de56..22b8e24 160000 --- a/plugins/gitiles +++ b/plugins/gitiles
@@ -1 +1 @@ -Subproject commit 0f8de56cc0f68047e38ac62e11271dbc6d76aa29 +Subproject commit 22b8e242b5eaa9eae817b776bc862b096479ceaa
diff --git a/plugins/hooks b/plugins/hooks index 22d1dbb..6316be2 160000 --- a/plugins/hooks +++ b/plugins/hooks
@@ -1 +1 @@ -Subproject commit 22d1dbbbd2b34dc066e20e6fb26a8623f1ae47fc +Subproject commit 6316be2828808dafc546ecd11c055396d0b4951b
diff --git a/plugins/plugin-manager b/plugins/plugin-manager index c921234..828d666 160000 --- a/plugins/plugin-manager +++ b/plugins/plugin-manager
@@ -1 +1 @@ -Subproject commit c921234cec4c0d058b35a8d117f55fce4d4c5e65 +Subproject commit 828d666bbb4aae1a2c348a12d7855ec5db3be46f
diff --git a/plugins/replication b/plugins/replication index 0d91ffd..b3ed3c8 160000 --- a/plugins/replication +++ b/plugins/replication
@@ -1 +1 @@ -Subproject commit 0d91ffd189297f511c93d40a864978b5774805c1 +Subproject commit b3ed3c8f9a8bab51beed794de5be8fd6da44231a
diff --git a/plugins/reviewnotes b/plugins/reviewnotes index f1a3622..eac4cd9 160000 --- a/plugins/reviewnotes +++ b/plugins/reviewnotes
@@ -1 +1 @@ -Subproject commit f1a36220e0ef31fb024de9ad589dfdfdf301c295 +Subproject commit eac4cd97cb5818ff471c64914fb4e342baf28c05
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md index a73f243..41f2ec7 100644 --- a/polygerrit-ui/README.md +++ b/polygerrit-ui/README.md
@@ -46,6 +46,9 @@ ```sh ./polygerrit-ui/run-server.sh + +// or +npm run start ``` Then visit <http://localhost:8081>. @@ -64,14 +67,16 @@ The biggest draw back of this method is that you cannot log in, so cannot test scenarios that require it. -#### MITM Proxy +#### Chrome extension: Gerrit FE Dev Helper -[MITM Proxy](https://mitmproxy.org/) is an open source product for proxying -https servers. The -[contrib/mitm-ui/](https://gerrit.googlesource.com/gerrit/+/master/contrib/mitm-ui/) -directory contains scripts (and documentation) for using this technology -(instead of the Go server). These scripts are somewhat experimental and -unmaintained though. +To be able to bypass the auth and also help improve the productivity of Gerrit FE developers, +we created this chrome extension: [Gerrit FE Dev Helper](https://chrome.google.com/webstore/detail/gerrit-fe-dev-helper/jimgomcnodkialnpmienbomamgomglkd). + +It basically works as a proxy that will block / redirect requests from current sites to any given url base on certain rules. + +The source code is in [Gerrit - gerrit-fe-dev-helper](https://gerrit-review.googlesource.com/q/project:gerrit-fe-dev-helper), contributions are welcomed! + +To use this extension, just follow its [readme here](https://gerrit.googlesource.com/gerrit-fe-dev-helper/+/master/README.md). ## Running locally against a Gerrit test site @@ -182,6 +187,11 @@ ``` ## Template Type Safety + +> **Warning**: This feature is temporary disabled, because it doesn't work with Polymer 2 and Polymer 3. +Some of the checks are made by polymer linter. + + Polymer elements are not type checked against the element definition, making it trivial to break the display when refactoring or moving code. We now run additional tests to help ensure that template types are checked.
diff --git a/polygerrit-ui/app/.eslintrc.json b/polygerrit-ui/app/.eslintrc.json index 26e3a09..472bb32 100644 --- a/polygerrit-ui/app/.eslintrc.json +++ b/polygerrit-ui/app/.eslintrc.json
@@ -26,32 +26,35 @@ "brace-style": ["error", "1tbs", { "allowSingleLine": true }], "camelcase": "off", "comma-dangle": ["error", { - "arrays": "always-multiline", - "objects": "always-multiline", - "imports": "always-multiline", - "exports": "always-multiline", - "functions": "never" + "arrays": "always-multiline", + "objects": "always-multiline", + "imports": "always-multiline", + "exports": "always-multiline", + "functions": "never" }], "eol-last": "off", - "indent": "off", - "indent-legacy": ["error", 2, { + "indent": ["error", 2, { "MemberExpression": 2, "FunctionDeclaration": {"body": 1, "parameters": 2}, "FunctionExpression": {"body": 1, "parameters": 2}, - "CallExpression": {"arguments": 2}, + "CallExpression": {"arguments": 2 }, "ArrayExpression": 1, "ObjectExpression": 1, "SwitchCase": 1 }], "keyword-spacing": ["error", { "after": true, "before": true }], + "lines-between-class-members": ["error", "always"], "max-len": [ "error", 80, 2, {"ignoreComments": true} ], - "new-cap": ["error", { "capIsNewExceptions": ["Polymer"] }], + "new-cap": ["error", { "capIsNewExceptions": ["Polymer", "LegacyElementMixin", "GestureEventListeners", "LegacyDataMixin"] }], "no-console": "off", + "no-multiple-empty-lines": [ "error", { "max": 1 } ], + "no-prototype-builtins": "off", + "no-redeclare": "off", "no-restricted-syntax": [ "error", { @@ -70,17 +73,58 @@ "no-redeclare": "off", "operator-linebreak": "off", "object-shorthand": ["error", "always"], + "padding-line-between-statements": [ + "error", + { + "blankLine": "always", + "prev": "class", + "next": "*" + }, + { + "blankLine": "always", + "prev": "*", + "next": "class" + } + ], "prefer-arrow-callback": "error", "prefer-const": "error", "prefer-spread": "error", "quote-props": ["error", "consistent-as-needed"], - "require-jsdoc": "off", "semi": [2, "always"], "template-curly-spacing": "error", - "valid-jsdoc": "off" + + "require-jsdoc": 0, + "valid-jsdoc": 0, + "jsdoc/check-alignment": 2, + "jsdoc/check-examples": 0, + "jsdoc/check-indentation": 0, + "jsdoc/check-param-names": 0, + "jsdoc/check-syntax": 0, + "jsdoc/check-tag-names": 0, + "jsdoc/check-types": 0, + "jsdoc/implements-on-classes": 2, + "jsdoc/match-description": 0, + "jsdoc/newline-after-description": 2, + "jsdoc/no-types": 0, + "jsdoc/no-undefined-types": 0, + "jsdoc/require-description": 0, + "jsdoc/require-description-complete-sentence": 0, + "jsdoc/require-example": 0, + "jsdoc/require-hyphen-before-param-description": 0, + "jsdoc/require-jsdoc": 0, + "jsdoc/require-param": 0, + "jsdoc/require-param-description": 0, + "jsdoc/require-param-name": 2, + "jsdoc/require-param-type": 2, + "jsdoc/require-returns": 0, + "jsdoc/require-returns-check": 0, + "jsdoc/require-returns-description": 0, + "jsdoc/require-returns-type": 2, + "jsdoc/valid-types": 2 }, "plugins": [ - "html" + "html", + "jsdoc" ], "settings": { "html/report-bad-indent": "error"
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD index 6d30a14..b596de8 100644 --- a/polygerrit-ui/app/BUILD +++ b/polygerrit-ui/app/BUILD
@@ -46,6 +46,22 @@ ) filegroup( + name = "pg_code_without_test", + srcs = glob( + [ + "**/*.html", + "**/*.js", + ], + exclude = [ + "bower_components/**", + "**/*_test.html", + "test/**", + "samples/**", + ], + ), +) + +filegroup( name = "bower_components", srcs = glob( [ @@ -88,6 +104,7 @@ ], ) +# TODO(taoalpha): alias to `npm run eslint` or just remove once CI moved to npm sh_test( name = "lint_test", size = "large", @@ -108,7 +125,8 @@ size = "large", srcs = ["polylint_test.sh"], data = [ - ":pg_code", + "polymer.json", + ":pg_code_without_test", "//polygerrit-ui:polygerrit_components.bower_components.zip", ], # Should not run sandboxed. @@ -148,33 +166,6 @@ ], ) for directory in DIRECTORIES] -# Embed bundle -polygerrit_bundle( - name = "polygerrit_embed_ui", - srcs = glob( - [ - "**/*.html", - "**/*.js", - ], - exclude = [ - "bower_components/**", - "test/**", - "**/*_test.html", - ], - ), - outs = ["polygerrit_embed_ui.zip"], - app = "embed/embed.html", -) - -filegroup( - name = "embed_test_files", - srcs = glob( - [ - "embed/**/*_test.html", - ], - ), -) - filegroup( name = "template_test_srcs", srcs = [ @@ -182,21 +173,3 @@ "template_test_srcs/template_test.js", ], ) - -sh_test( - name = "embed_test", - size = "small", - srcs = ["embed_test.sh"], - data = [ - "embed/test.html", - "test/common-test-setup.html", - ":embed_test_files", - ":pg_code.zip", - ":test_components.zip", - ], - # Should not run sandboxed. - tags = [ - "local", - "manual", - ], -)
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html index 36e0201..f560ea8 100644 --- a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html +++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html
@@ -45,5 +45,22 @@ }); }, }; + + // eslint-disable-next-line no-unused-vars + function defineEmptyMixin() { + // This is a temporary function. + // Polymer linter doesn't process correctly the following code: + // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...} + // To workaround this issue, the mock mixin is declared in this method. + // In the following changes, legacy behaviors will be converted to mixins. + + /** + * @polymer + * @mixinFunction + */ + Gerrit.AsyncForeachMixin = base => + class extends base { + }; + } })(window); </script>
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html index 970bfc7..03ba6b1 100644 --- a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html +++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>async-foreach-behavior</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html index 1748647..92596e0 100644 --- a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html +++ b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html
@@ -28,5 +28,22 @@ return window.CANONICAL_PATH || ''; }, }; + + // eslint-disable-next-line no-unused-vars + function defineEmptyMixin() { + // This is a temporary function. + // Polymer linter doesn't process correctly the following code: + // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...} + // To workaround this issue, the mock mixin is declared in this method. + // In the following changes, legacy behaviors will be converted to mixins. + + /** + * @polymer + * @mixinFunction + */ + Gerrit.BaseUrlMixin = base => + class extends base { + }; + } })(window); </script>
diff --git a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html index b61b142..5669bc4 100644 --- a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html +++ b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>base-url-behavior</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.html b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.html index 64b725f..05a7a58 100644 --- a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.html +++ b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.html
@@ -31,6 +31,7 @@ /** * Get the docs base URL from either the server config or by probing. + * * @param {Object} config The server config. * @param {!Object} restApi A REST API instance * @return {!Promise<string>} A promise that resolves with the docs base @@ -56,7 +57,24 @@ cachedPromise = undefined; }, }, - Gerrit.BaseUrlBehavior, + Gerrit.BaseUrlBehavior, ]; + + // eslint-disable-next-line no-unused-vars + function defineEmptyMixin() { + // This is a temporary function. + // Polymer linter doesn't process correctly the following code: + // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...} + // To workaround this issue, the mock mixin is declared in this method. + // In the following changes, legacy behaviors will be converted to mixins. + + /** + * @polymer + * @mixinFunction + */ + Gerrit.DocsUrlMixin = base => + class extends base { + }; + } })(window); </script>
diff --git a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html index 2c513f3..e554012 100644 --- a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html +++ b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
@@ -19,7 +19,7 @@ <script src="/bower_components/web-component-tester/browser.js"></script> <link rel="import" href="../../test/common-test-setup.html"/> <title>docs-url-behavior</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <link rel="import" href="docs-url-behavior.html">
diff --git a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.html b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.html index 2d25b29..1377627 100644 --- a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.html +++ b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.html
@@ -25,6 +25,7 @@ /** * Are any ancestors of the element (or the element itself) members of the * given class. + * * @param {!Element} element * @param {string} className * @param {Element=} opt_stopElement If provided, stop traversing the @@ -42,5 +43,22 @@ return isDescendant; }, }; + + // eslint-disable-next-line no-unused-vars + function defineEmptyMixin() { + // This is a temporary function. + // Polymer linter doesn't process correctly the following code: + // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...} + // To workaround this issue, the mock mixin is declared in this method. + // In the following changes, legacy behaviors will be converted to mixins. + + /** + * @polymer + * @mixinFunction + */ + Gerrit.DomUtilMixin = base => + class extends base { + }; + } })(window); </script>
diff --git a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html index 8323ac6..a52e0e2 100644 --- a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html +++ b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>dom-util-behavior</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.html b/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.html index b5afab1..5b3d420 100644 --- a/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.html +++ b/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.html
@@ -51,5 +51,22 @@ return event; }, }; + + // eslint-disable-next-line no-unused-vars + function defineEmptyMixin() { + // This is a temporary function. + // Polymer linter doesn't process correctly the following code: + // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...} + // To workaround this issue, the mock mixin is declared in this method. + // In the following changes, legacy behaviors will be converted to mixins. + + /** + * @polymer + * @mixinFunction + */ + Gerrit.FireMixin = base => + class extends base { + }; + } })(window); </script>
diff --git a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.html b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.html index 0c75c44..9d3b1af 100644 --- a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.html +++ b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.html
@@ -150,5 +150,29 @@ }); }, }; + + // eslint-disable-next-line no-unused-vars + function defineEmptyMixin() { + // This is a temporary function. + // Polymer linter doesn't process correctly the following code: + // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...} + // To workaround this issue, the mock mixin is declared in this method. + // In the following changes, legacy behaviors will be converted to mixins. + + /** + * @polymer + * @mixinFunction + */ + Gerrit.AccessMixin = base => + class extends base { + static get properties() { + return { + permissionValues: { + type: Object, + }, + }; + } + }; + } })(window); </script>
diff --git a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html index 0d1ee57..48565a6 100644 --- a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html +++ b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>keyboard-shortcut-behavior</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html index 182d242..07350b9 100644 --- a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html +++ b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html
@@ -201,5 +201,22 @@ }; }, }; + + // eslint-disable-next-line no-unused-vars + function defineEmptyMixin() { + // This is a temporary function. + // Polymer linter doesn't process correctly the following code: + // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...} + // To workaround this issue, the mock mixin is declared in this method. + // In the following changes, legacy behaviors will be converted to mixins. + + /** + * @polymer + * @mixinFunction + */ + Gerrit.AdminNavMixin = base => + class extends base { + }; + } })(window); </script>
diff --git a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html index 0285e35..25b75c8 100644 --- a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html +++ b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>keyboard-shortcut-behavior</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -310,7 +310,6 @@ }); }); - suite('view plugin screen with plugin capability', () => { const account = { name: 'test-user', @@ -337,7 +336,6 @@ }); }); - suite('view plugin screen without plugin capability', () => { const account = { name: 'test-user',
diff --git a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.html b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.html index c462c6f..31b9423 100644 --- a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.html +++ b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.html
@@ -41,6 +41,7 @@ /** * Returns the complement to the given column array + * * @param {Array} columns * @return {!Array} */ @@ -63,6 +64,7 @@ * The Project column was renamed to Repo, but some users may have * preferences that use its old name. If that column is found, rename it * before use. + * * @param {!Array<string>} columns * @return {!Array<string>} If the column was renamed, returns a new array * with the corrected name. Otherwise, it returns the original param. @@ -75,5 +77,31 @@ return newColumns; }, }; + + // eslint-disable-next-line no-unused-vars + function defineEmptyMixin() { + // This is a temporary function. + // Polymer linter doesn't process correctly the following code: + // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...} + // To workaround this issue, the mock mixin is declared in this method. + // In the following changes, legacy behaviors will be converted to mixins. + + /** + * @polymer + * @mixinFunction + */ + Gerrit.ChangeTableMixin = base => + class extends base { + static get properties() { + return { + columnNames: { + type: Array, + }, + }; + } + + isColumnHidden(columnToCheck, columnsToDisplay) {} + }; + } })(window); </script>
diff --git a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html index 791e2af..52931bc 100644 --- a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html +++ b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>keyboard-shortcut-behavior</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.html b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.html index 3106fc8..e5ded0e 100644 --- a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.html +++ b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.html
@@ -39,5 +39,22 @@ return GrDisplayNameUtils.getGroupDisplayName(group); }, }; + + // eslint-disable-next-line no-unused-vars + function defineEmptyMixin() { + // This is a temporary function. + // Polymer linter doesn't process correctly the following code: + // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...} + // To workaround this issue, the mock mixin is declared in this method. + // In the following changes, legacy behaviors will be converted to mixins. + + /** + * @polymer + * @mixinFunction + */ + Gerrit.DisplayNameMixin = base => + class extends base { + }; + } })(window); </script>
diff --git a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html index 3d4eca1..aa217a1 100644 --- a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html +++ b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-display-name-behavior</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.html b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.html index f251db8..06912d5 100644 --- a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.html +++ b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.html
@@ -56,8 +56,29 @@ return 0; }, }, - Gerrit.BaseUrlBehavior, - Gerrit.URLEncodingBehavior, + Gerrit.BaseUrlBehavior, + Gerrit.URLEncodingBehavior, ]; + + // eslint-disable-next-line no-unused-vars + function defineEmptyMixin() { + // This is a temporary function. + // Polymer linter doesn't process correctly the following code: + // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...} + // To workaround this issue, the mock mixin is declared in this method. + // In the following changes, legacy behaviors will be converted to mixins. + + /** + * @polymer + * @mixinFunction + */ + Gerrit.ListViewMixin = base => + class extends base { + computeLoadingClass(loading) {} + + computeShownItems(items) {} + }; + } })(window); + </script>
diff --git a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html index 535483d..9b48cdc 100644 --- a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html +++ b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>keyboard-shortcut-behavior</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html index 0b5c340..84d6cab 100644 --- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html +++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
@@ -52,6 +52,7 @@ /** * Whether the given patch is a numbered parent of a merge (i.e. a negative * number). + * * @param {string|number} n * @return {Boolean} */ @@ -124,8 +125,8 @@ // 2 -> 3, 3 -> 5, etc. // Map an edit to the patchNum of parent*2... I.e. edit on 2 -> 4. const num = r => r._number === Gerrit.PatchSetBehavior.EDIT_NAME ? - 2 * editParent : - 2 * (r._number - 1) + 1; + 2 * editParent : + 2 * (r._number - 1) + 1; return revisions.sort((a, b) => num(b) - num(a)); }, @@ -265,6 +266,7 @@ /** * Convert parent indexes from patch range expressions to numbers. * For example, in a patch range expression `"-3"` becomes `3`. + * * @param {number|string} rangeBase * @return {number} */ @@ -272,5 +274,29 @@ return -parseInt(rangeBase + '', 10); }, }; + + // eslint-disable-next-line no-unused-vars + function defineEmptyMixin() { + // This is a temporary function. + // Polymer linter doesn't process correctly the following code: + // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...} + // To workaround this issue, the mock mixin is declared in this method. + // In the following changes, legacy behaviors will be converted to mixins. + + /** + * @polymer + * @mixinFunction + */ + Gerrit.PatchSetMixin = base => + class extends base { + computeLatestPatchNum(allPatchSets) {} + + hasEditPatchsetLoaded(patchRangeRecord) {} + + hasEditBasedOnCurrentPatchSet(allPatchSets) {} + + computeAllPatchSets(change) {} + }; + } })(window); </script>
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html index 3db4084..5e70179 100644 --- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html +++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
@@ -19,7 +19,7 @@ <script src="/bower_components/web-component-tester/browser.js"></script> <link rel="import" href="../../test/common-test-setup.html"/> <title>gr-patch-set-behavior</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <link rel="import" href="gr-patch-set-behavior.html">
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html index 5e597ae..67e4ca6 100644 --- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html +++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html
@@ -81,6 +81,12 @@ return path; }, + isMagicPath(path) { + return !!path && + (path === Gerrit.PathListBehavior.COMMIT_MESSAGE_PATH || path === + Gerrit.PathListBehavior.MERGE_LIST_PATH); + }, + computeTruncatedPath(path) { return Gerrit.PathListBehavior.truncatePath( Gerrit.PathListBehavior.computeDisplayPath(path)); @@ -110,5 +116,25 @@ return `\u2026/${pathPieces.slice(index).join('/')}`; }, }; + + // eslint-disable-next-line no-unused-vars + function defineEmptyMixin() { + // This is a temporary function. + // Polymer linter doesn't process correctly the following code: + // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...} + // To workaround this issue, the mock mixin is declared in this method. + // In the following changes, legacy behaviors will be converted to mixins. + + /** + * @polymer + * @mixinFunction + */ + Gerrit.PathListMixin = base => + class extends base { + computeDisplayPath(path) {} + + computeTruncatedPath(path) {} + }; + } })(window); </script>
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html index 0046290..12b981c 100644 --- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html +++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
@@ -19,7 +19,7 @@ <script src="/bower_components/web-component-tester/browser.js"></script> <link rel="import" href="../../test/common-test-setup.html"/> <title>gr-path-list-behavior</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <link rel="import" href="gr-path-list-behavior.html"> @@ -56,6 +56,14 @@ assert.equal(name('/MERGE_LIST'), 'Merge list'); }); + test('isMagicPath', () => { + const isMagic = Gerrit.PathListBehavior.isMagicPath; + assert.isFalse(isMagic(undefined)); + assert.isFalse(isMagic('/foo.cc')); + assert.isTrue(isMagic('/COMMIT_MSG')); + assert.isTrue(isMagic('/MERGE_LIST')); + }); + test('truncatePath with long path should add ellipsis', () => { const truncatePath = Gerrit.PathListBehavior.truncatePath; let path = 'level1/level2/level3/level4/file.js';
diff --git a/polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.html b/polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.html index 2fa9191..69ebf23 100644 --- a/polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.html +++ b/polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.html
@@ -34,5 +34,22 @@ }, PLUGIN_CONFIG_CHANGED: 'plugin-config-changed', }; + + // eslint-disable-next-line no-unused-vars + function defineEmptyMixin() { + // This is a temporary function. + // Polymer linter doesn't process correctly the following code: + // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...} + // To workaround this issue, the mock mixin is declared in this method. + // In the following changes, legacy behaviors will be converted to mixins. + + /** + * @polymer + * @mixinFunction + */ + Gerrit.RepoPluginConfigMixin = base => + class extends base { + }; + } })(window); </script>
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js index 0bf620f..73b8261 100644 --- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js +++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
@@ -146,4 +146,28 @@ } }, }; + + // eslint-disable-next-line no-unused-vars + function defineEmptyMixin() { + // This is a temporary function. + // Polymer linter doesn't process correctly the following code: + // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...} + // To workaround this issue, the mock mixin is declared in this method. + // In the following changes, legacy behaviors will be converted to mixins. + + /** + * @polymer + * @mixinFunction + */ + Gerrit.TooltipMixin = base => + class extends base { + static get properties() { + return { + hasTooltip: { + type: Boolean, + }, + }; + } + }; + } })(window);
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html index 173c8d4..f3354a4 100644 --- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html +++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
@@ -17,7 +17,7 @@ --> <title>tooltip-behavior</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html index 69703f6..0396c4f 100644 --- a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html +++ b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html
@@ -25,6 +25,7 @@ /** * Pretty-encodes a URL. Double-encodes the string, and then replaces * benevolent characters for legibility. + * * @param {string} url * @param {boolean=} replaceSlashes * @return {string} @@ -45,6 +46,7 @@ * Single decode for URL components. Will decode plus signs ('+') to spaces. * Note: because this function decodes once, it is not the inverse of * encodeURL. + * * @param {string} url * @return {string} */ @@ -53,5 +55,22 @@ return decodeURIComponent(withoutPlus); }, }; + + // eslint-disable-next-line no-unused-vars + function defineEmptyMixin() { + // This is a temporary function. + // Polymer linter doesn't process correctly the following code: + // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...} + // To workaround this issue, the mock mixin is declared in this method. + // In the following changes, legacy behaviors will be converted to mixins. + + /** + * @polymer + * @mixinFunction + */ + Gerrit.URLEncodingMixin = base => + class extends base { + }; + } })(window); </script>
diff --git a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html index 73e51d3..6cf2c68 100644 --- a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html +++ b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html
@@ -17,7 +17,7 @@ --> <title>gr-url-encoding-behavior</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html index 3c5a733..d4e2c72 100644 --- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html +++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
@@ -612,5 +612,22 @@ shortcutManager.bindShortcut(shortcut, ...bindings); }, }; + + // eslint-disable-next-line no-unused-vars + function defineEmptyMixin() { + // This is a temporary function. + // Polymer linter doesn't process correctly the following code: + // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...} + // To workaround this issue, the mock mixin is declared in this method. + // In the following changes, legacy behaviors will be converted to mixins. + + /** + * @polymer + * @mixinFunction + */ + Gerrit.KeyboardShortcutMixin = base => + class extends base { + }; + } })(window); </script>
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html index 5a563ab..ba143ec 100644 --- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html +++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>keyboard-shortcut-behavior</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -240,8 +240,8 @@ test('directory view', () => { const { - NEXT_FILE, NEXT_LINE, GO_TO_OPENED_CHANGES, SEARCH, - SAVE_COMMENT, + NEXT_FILE, NEXT_LINE, GO_TO_OPENED_CHANGES, SEARCH, + SAVE_COMMENT, } = kb.Shortcut; const {DIFFS, EVERYWHERE, NAVIGATION} = kb.ShortcutSection; const {GO_KEY, ShortcutManager} = kb;
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html index 4252e6e..2237866 100644 --- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html +++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
@@ -99,9 +99,9 @@ SKIP_MERGEABLE: 22, /** - * Skip diffstat computation that compute the insertions field (number of lines inserted) and - * deletions field (number of lines deleted) - */ + * Skip diffstat computation that compute the insertions field (number of lines inserted) and + * deletions field (number of lines deleted) + */ SKIP_DIFFSTAT: 23, }, @@ -175,7 +175,27 @@ return this.changeStatuses(change).join(', '); }, }, - Gerrit.BaseUrlBehavior, + Gerrit.BaseUrlBehavior, ]; + + // eslint-disable-next-line no-unused-vars + function defineEmptyMixin() { + // This is a temporary function. + // Polymer linter doesn't process correctly the following code: + // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...} + // To workaround this issue, the mock mixin is declared in this method. + // In the following changes, legacy behaviors will be converted to mixins. + + /** + * @polymer + * @mixinFunction + */ + Gerrit.RESTClientMixin = base => + class extends base { + changeStatusString(change) {} + + changeStatuses(change, opt_options) {} + }; + } })(window); </script>
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html index a77a01f..cfcc11e 100644 --- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html +++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>keyboard-shortcut-behavior</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior.html b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior.html index 43022d9..8f08f0c 100644 --- a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior.html +++ b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior.html
@@ -28,6 +28,7 @@ /** * Wraps a string to be used as a URL. An error is thrown if the string cannot * be considered safe. + * * @constructor * @param {string} url the unwrapped, potentially unsafe URL. */ @@ -40,6 +41,7 @@ /** * Get the string representation of the safe URL. + * * @returns {string} */ Gerrit.SafeTypes.SafeUrl.prototype.asString = function() {
diff --git a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html index ab446f1..7351647 100644 --- a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html +++ b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html
@@ -17,7 +17,7 @@ --> <title>safe-types-behavior</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html index ac65360..a52cb1a 100644 --- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html +++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html
@@ -86,7 +86,9 @@ padding-right: var(--spacing-m); } </style> - <style include="gr-form-styles"></style> + <style include="gr-form-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <fieldset id="section" class$="gr-form-styles [[_computeSectionClass(editing, canUpload, ownerOf, _editingRef, _deleted)]]"> <div id="mainContainer">
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js index b6f1b74..83390fb 100644 --- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js +++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
@@ -25,6 +25,7 @@ /** * Fired when a section that was previously added was removed. + * * @event added-section-removed */ @@ -36,61 +37,69 @@ const ON_BEHALF_OF = '(On Behalf Of)'; const LABEL = 'Label'; - Polymer({ - is: 'gr-access-section', + /** + * @appliesMixin Gerrit.AccessMixin + * @appliesMixin Gerrit.FireMixin + */ + class GrAccessSection extends Polymer.mixinBehaviors( [ + Gerrit.AccessBehavior, + /** + * Unused in this element, but called by other elements in tests + * e.g gr-repo-access_test. + */ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-access-section'; } - properties: { - capabilities: Object, - /** @type {?} */ - section: { - type: Object, - notify: true, - observer: '_updateSection', - }, - groups: Object, - labels: Object, - editing: { - type: Boolean, - value: false, - observer: '_handleEditingChanged', - }, - canUpload: Boolean, - ownerOf: Array, - _originalId: String, - _editingRef: { - type: Boolean, - value: false, - }, - _deleted: { - type: Boolean, - value: false, - }, - _permissions: Array, - }, + static get properties() { + return { + capabilities: Object, + /** @type {?} */ + section: { + type: Object, + notify: true, + observer: '_updateSection', + }, + groups: Object, + labels: Object, + editing: { + type: Boolean, + value: false, + observer: '_handleEditingChanged', + }, + canUpload: Boolean, + ownerOf: Array, + _originalId: String, + _editingRef: { + type: Boolean, + value: false, + }, + _deleted: { + type: Boolean, + value: false, + }, + _permissions: Array, + }; + } - behaviors: [ - Gerrit.AccessBehavior, - /** - * Unused in this element, but called by other elements in tests - * e.g gr-repo-access_test. - */ - Gerrit.FireBehavior, - ], - - listeners: { - 'access-saved': '_handleAccessSaved', - }, + created() { + super.created(); + this.addEventListener('access-saved', + () => this._handleAccessSaved()); + } _updateSection(section) { this._permissions = this.toSortedArray(section.value.permissions); this._originalId = section.id; - }, + } _handleAccessSaved() { // Set a new 'original' value to keep track of after the value has been // saved. this._updateSection(this.section); - }, + } _handleValueChange() { if (!this.section.value.added) { @@ -103,7 +112,7 @@ 'access-modified', {bubbles: true, composed: true})); } this.section.value.updatedId = this.section.id; - }, + } _handleEditingChanged(editing, editingOld) { // Ignore when editing gets set initially. @@ -123,7 +132,7 @@ } } } - }, + } _computePermissions(name, capabilities, labels) { let allPermissions; @@ -140,17 +149,17 @@ return allPermissions.filter(permission => { return !this.section.value.permissions[permission.id]; }); - }, + } _computeHideEditClass(section) { return section.id === 'GLOBAL_CAPABILITIES' ? 'hide' : ''; - }, + } _handleAddedPermissionRemoved(e) { const index = e.model.index; this._permissions = this._permissions.slice(0, index).concat( this._permissions.slice(index + 1, this._permissions.length)); - }, + } _computeLabelOptions(labels) { const labelOptions = []; @@ -172,7 +181,7 @@ }); } return labelOptions; - }, + } _computePermissionName(name, permission, permissionValues, capabilities) { if (name === GLOBAL_NAME) { @@ -186,7 +195,7 @@ } return `${LABEL} ${permission.value.label}${behalfOf}`; } - }, + } _computeSectionName(name) { // When a new section is created, it doesn't yet have a ref. Set into @@ -204,7 +213,7 @@ return `Reference: ${name}`; } return name; - }, + } _handleRemoveReference() { if (this.section.value.added) { @@ -215,27 +224,27 @@ this.section.value.deleted = true; this.dispatchEvent( new CustomEvent('access-modified', {bubbles: true, composed: true})); - }, + } _handleUndoRemove() { this._deleted = false; delete this.section.value.deleted; - }, + } editRefInput() { return Polymer.dom(this.root).querySelector(Polymer.Element ? - 'iron-input.editRefInput' : - 'input[is=iron-input].editRefInput'); - }, + 'iron-input.editRefInput' : + 'input[is=iron-input].editRefInput'); + } editReference() { this._editingRef = true; this.editRefInput().focus(); - }, + } _isEditEnabled(canUpload, ownerOf, sectionId) { return canUpload || (ownerOf && ownerOf.indexOf(sectionId) >= 0); - }, + } _computeSectionClass(editing, canUpload, ownerOf, editingRef, deleted) { const classList = []; @@ -249,11 +258,11 @@ classList.push('deleted'); } return classList.join(' '); - }, + } _computeEditBtnClass(name) { return name === GLOBAL_NAME ? 'global' : ''; - }, + } _handleAddPermission() { const value = this.$.permissionSelect.value; @@ -286,6 +295,8 @@ this.push('_permissions', permission); this.set(['section.value.permissions', permission.id], permission.value); - }, - }); + } + } + + customElements.define(GrAccessSection.is, GrAccessSection); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html index 5110dc1..55b2495 100644 --- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html +++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-access-section</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/page/page.js"></script> @@ -188,7 +188,6 @@ assert.isTrue(element.toSortedArray.lastCall. calledWithExactly(element.capabilities)); - // For everything else, include possible label values before filtering. name = 'refs/for/*'; assert.deepEqual(element._computePermissions(name, element.capabilities, @@ -206,8 +205,9 @@ id: 'administrateServer', value: {}, }; - assert.equal(element._computePermissionName(name, permission, - element.permissionValues, element.capabilities), + assert.equal( + element._computePermissionName(name, permission, + element.permissionValues, element.capabilities), element.capabilities[permission.id].name); name = 'refs/for/*'; @@ -216,8 +216,9 @@ value: {}, }; - assert.equal(element._computePermissionName( - name, permission, element.permissionValues, element.capabilities), + assert.equal( + element._computePermissionName( + name, permission, element.permissionValues, element.capabilities), element.permissionValues[permission.id].name); name = 'refs/for/*'; @@ -228,8 +229,9 @@ }, }; - assert.equal(element._computePermissionName(name, permission, - element.permissionValues, element.capabilities), + assert.equal( + element._computePermissionName(name, permission, + element.permissionValues, element.capabilities), 'Label Code-Review'); permission = { @@ -239,8 +241,9 @@ }, }; - assert.equal(element._computePermissionName(name, permission, - element.permissionValues, element.capabilities), + assert.equal( + element._computePermissionName(name, permission, + element.permissionValues, element.capabilities), 'Label Code-Review(On Behalf Of)'); });
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html index bf1086c..5207717 100644 --- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html +++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
@@ -30,8 +30,12 @@ <dom-module id="gr-admin-group-list"> <template> - <style include="shared-styles"></style> - <style include="gr-table-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="gr-table-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <gr-list-view create-new="[[_createNewCapability]]" filter="[[_filter]]"
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js index 4c099d6..5a62584 100644 --- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js +++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
@@ -17,65 +17,72 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-admin-group-list', + /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.ListViewMixin + */ + class GrAdminGroupList extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.ListViewBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-admin-group-list'; } - properties: { + static get properties() { + return { /** * URL params passed from the router. */ - params: { - type: Object, - observer: '_paramsChanged', - }, + params: { + type: Object, + observer: '_paramsChanged', + }, - /** - * Offset of currently visible query results. - */ - _offset: Number, - _path: { - type: String, - readOnly: true, - value: '/admin/groups', - }, - _hasNewGroupName: Boolean, - _createNewCapability: { - type: Boolean, - value: false, - }, - _groups: Array, + /** + * Offset of currently visible query results. + */ + _offset: Number, + _path: { + type: String, + readOnly: true, + value: '/admin/groups', + }, + _hasNewGroupName: Boolean, + _createNewCapability: { + type: Boolean, + value: false, + }, + _groups: Array, - /** - * Because we request one more than the groupsPerPage, _shownGroups - * may be one less than _groups. - * */ - _shownGroups: { - type: Array, - computed: 'computeShownItems(_groups)', - }, + /** + * Because we request one more than the groupsPerPage, _shownGroups + * may be one less than _groups. + * */ + _shownGroups: { + type: Array, + computed: 'computeShownItems(_groups)', + }, - _groupsPerPage: { - type: Number, - value: 25, - }, + _groupsPerPage: { + type: Number, + value: 25, + }, - _loading: { - type: Boolean, - value: true, - }, - _filter: String, - }, - - behaviors: [ - Gerrit.FireBehavior, - Gerrit.ListViewBehavior, - ], + _loading: { + type: Boolean, + value: true, + }, + _filter: String, + }; + } attached() { + super.attached(); this._getCreateGroupCapability(); this.fire('title-change', {title: 'Groups'}); this._maybeOpenCreateOverlay(this.params); - }, + } _paramsChanged(params) { this._loading = true; @@ -84,21 +91,22 @@ return this._getGroups(this._filter, this._groupsPerPage, this._offset); - }, + } /** * Opens the create overlay if the route has a hash 'create' + * * @param {!Object} params */ _maybeOpenCreateOverlay(params) { if (params && params.openCreateModal) { this.$.createOverlay.open(); } - }, + } _computeGroupUrl(id) { return Gerrit.Nav.getUrlForGroup(id); - }, + } _getCreateGroupCapability() { return this.$.restAPI.getAccount().then(account => { @@ -110,7 +118,7 @@ } }); }); - }, + } _getGroups(filter, groupsPerPage, offset) { this._groups = []; @@ -120,37 +128,39 @@ return; } this._groups = Object.keys(groups) - .map(key => { - const group = groups[key]; - group.name = key; - return group; - }); + .map(key => { + const group = groups[key]; + group.name = key; + return group; + }); this._loading = false; }); - }, + } _refreshGroupsList() { this.$.restAPI.invalidateGroupsCache(); return this._getGroups(this._filter, this._groupsPerPage, this._offset); - }, + } _handleCreateGroup() { this.$.createNewModal.handleCreateGroup().then(() => { this._refreshGroupsList(); }); - }, + } _handleCloseCreate() { this.$.createOverlay.close(); - }, + } _handleCreateClicked() { this.$.createOverlay.open(); - }, + } _visibleToAll(item) { return item.options.visible_to_all === true ? 'Y' : 'N'; - }, - }); + } + } + + customElements.define(GrAdminGroupList.is, GrAdminGroupList); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html index 58c7be4..455b3a2 100644 --- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html +++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-admin-group-list</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/page/page.js"></script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html index c7187a9..aae11d3 100644 --- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html +++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
@@ -43,8 +43,12 @@ <dom-module id="gr-admin-view"> <template> - <style include="shared-styles"></style> - <style include="gr-menu-page-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="gr-menu-page-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="gr-page-nav-styles"> gr-dropdown-list { --trigger-style: {
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js index d3f020d..afc373d 100644 --- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js +++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -17,66 +17,75 @@ (function() { 'use strict'; - const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/; - Polymer({ - is: 'gr-admin-view', + /** + * @appliesMixin Gerrit.AdminNavMixin + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.URLEncodingMixin + */ + class GrAdminView extends Polymer.mixinBehaviors( [ + Gerrit.AdminNavBehavior, + Gerrit.BaseUrlBehavior, + Gerrit.URLEncodingBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-admin-view'; } - properties: { + static get properties() { + return { /** @type {?} */ - params: Object, - path: String, - adminView: String, + params: Object, + path: String, + adminView: String, - _breadcrumbParentName: String, - _repoName: String, - _groupId: { - type: Number, - observer: '_computeGroupName', - }, - _groupIsInternal: Boolean, - _groupName: String, - _groupOwner: { - type: Boolean, - value: false, - }, - _subsectionLinks: Array, - _filteredLinks: Array, - _showDownload: { - type: Boolean, - value: false, - }, - _isAdmin: { - type: Boolean, - value: false, - }, - _showGroup: Boolean, - _showGroupAuditLog: Boolean, - _showGroupList: Boolean, - _showGroupMembers: Boolean, - _showRepoAccess: Boolean, - _showRepoCommands: Boolean, - _showRepoDashboards: Boolean, - _showRepoDetailList: Boolean, - _showRepoMain: Boolean, - _showRepoList: Boolean, - _showPluginList: Boolean, - }, + _breadcrumbParentName: String, + _repoName: String, + _groupId: { + type: Number, + observer: '_computeGroupName', + }, + _groupIsInternal: Boolean, + _groupName: String, + _groupOwner: { + type: Boolean, + value: false, + }, + _subsectionLinks: Array, + _filteredLinks: Array, + _showDownload: { + type: Boolean, + value: false, + }, + _isAdmin: { + type: Boolean, + value: false, + }, + _showGroup: Boolean, + _showGroupAuditLog: Boolean, + _showGroupList: Boolean, + _showGroupMembers: Boolean, + _showRepoAccess: Boolean, + _showRepoCommands: Boolean, + _showRepoDashboards: Boolean, + _showRepoDetailList: Boolean, + _showRepoMain: Boolean, + _showRepoList: Boolean, + _showPluginList: Boolean, + }; + } - behaviors: [ - Gerrit.AdminNavBehavior, - Gerrit.BaseUrlBehavior, - Gerrit.URLEncodingBehavior, - ], - - observers: [ - '_paramsChanged(params)', - ], + static get observers() { + return [ + '_paramsChanged(params)', + ]; + } attached() { + super.attached(); this.reload(); - }, + } reload() { const promises = [ @@ -105,37 +114,37 @@ .then(res => { this._filteredLinks = res.links; this._breadcrumbParentName = res.expandedSection ? - res.expandedSection.name : ''; + res.expandedSection.name : ''; if (!res.expandedSection) { this._subsectionLinks = []; return; } this._subsectionLinks = [res.expandedSection] - .concat(res.expandedSection.children).map(section => { - return { - text: !section.detailType ? 'Home' : section.name, - value: section.view + (section.detailType || ''), - view: section.view, - url: section.url, - detailType: section.detailType, - parent: this._groupId || this._repoName || '', - }; - }); + .concat(res.expandedSection.children).map(section => { + return { + text: !section.detailType ? 'Home' : section.name, + value: section.view + (section.detailType || ''), + view: section.view, + url: section.url, + detailType: section.detailType, + parent: this._groupId || this._repoName || '', + }; + }); }); }); - }, + } _computeSelectValue(params) { if (!params || !params.view) { return; } return params.view + (params.detail || ''); - }, + } _selectedIsCurrentPage(selected) { return (selected.parent === (this._repoName || this._groupId) && selected.view === this.params.view && selected.detailType === this.params.detail); - }, + } _handleSubsectionChange(e) { const selected = this._subsectionLinks @@ -146,7 +155,7 @@ return; } Gerrit.Nav.navigateToRelativeUrl(selected.url); - }, + } _paramsChanged(params) { const isGroupView = params.view === Gerrit.Nav.View.GROUP; @@ -195,19 +204,19 @@ } if (!needsReload) { return; } this.reload(); - }, + } // TODO (beckysiegel): Update these functions after router abstraction is // updated. They are currently copied from gr-dropdown (and should be // updated there as well once complete). _computeURLHelper(host, path) { return '//' + host + this.getBaseUrl() + path; - }, + } _computeRelativeURL(path) { const host = window.location.host; return this._computeURLHelper(host, path); - }, + } _computeLinkURL(link) { if (!link || typeof link.url === 'undefined') { return ''; } @@ -215,7 +224,7 @@ return link.url; } return this._computeRelativeURL(link.url); - }, + } /** * @param {string} itemView @@ -245,7 +254,7 @@ return ''; } return itemView === params.adminView ? 'selected' : ''; - }, + } _computeGroupName(groupId) { if (!groupId) { return ''; } @@ -271,11 +280,13 @@ this.reload(); }); }); - }, + } _updateGroupName(e) { this._groupName = e.detail.name; this.reload(); - }, - }); + } + } + + customElements.define(GrAdminView.is, GrAdminView); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html index 984be19..04bdab7 100644 --- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html +++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-admin-view</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -197,7 +197,10 @@ assert.equal(Polymer.dom(element.root) .querySelectorAll('.sectionTitle').length, 3); assert.equal(element.$$('.breadcrumbText').innerText, 'Test Repo'); - assert.equal(element.$$('#pageSelect').items.length, 6); + assert.equal( + element.shadowRoot.querySelector('#pageSelect').items.length, + 6 + ); done(); }); }); @@ -439,13 +442,16 @@ element.reload().then(() => { assert.deepEqual(element._filteredLinks, expectedFilteredLinks); assert.deepEqual(element._subsectionLinks, expectedSubsectionLinks); - assert.equal(element.$$('#pageSelect').value, 'repoaccess'); + assert.equal( + element.shadowRoot.querySelector('#pageSelect').value, + 'repoaccess' + ); assert.isTrue(element._selectedIsCurrentPage.calledOnce); // Doesn't trigger navigation from the page select menu. assert.isFalse(Gerrit.Nav.navigateToRelativeUrl.called); // When explicitly changed, navigation is called - element.$$('#pageSelect').value = 'repo'; + element.shadowRoot.querySelector('#pageSelect').value = 'repo'; assert.isTrue(element._selectedIsCurrentPage.calledTwice); assert.isTrue(Gerrit.Nav.navigateToRelativeUrl.calledOnce); done();
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js index acc76de..7400d2a 100644 --- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js +++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
@@ -23,9 +23,15 @@ TAGS: 'tags', }; - Polymer({ - is: 'gr-confirm-delete-item-dialog', - + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrConfirmDeleteItemDialog extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-confirm-delete-item-dialog'; } /** * Fired when the confirm button is pressed. * @@ -38,26 +44,24 @@ * @event cancel */ - properties: { - item: String, - itemType: String, - }, - - behaviors: [ - Gerrit.FireBehavior, - ], + static get properties() { + return { + item: String, + itemType: String, + }; + } _handleConfirmTap(e) { e.preventDefault(); e.stopPropagation(); this.fire('confirm', null, {bubbles: false}); - }, + } _handleCancelTap(e) { e.preventDefault(); e.stopPropagation(); this.fire('cancel', null, {bubbles: false}); - }, + } _computeItemName(detailType) { if (detailType === DETAIL_TYPES.BRANCHES) { @@ -67,6 +71,9 @@ } else if (detailType === DETAIL_TYPES.ID) { return 'ID'; } - }, - }); + } + } + + customElements.define(GrConfirmDeleteItemDialog.is, + GrConfirmDeleteItemDialog); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html index 3292cec..c83349b 100644 --- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html +++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-confirm-delete-item-dialog</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html index 2a95991..1d6e706 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html +++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
@@ -31,7 +31,9 @@ <dom-module id="gr-create-change-dialog"> <template> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="gr-form-styles"> input:not([type="checkbox"]), gr-autocomplete, @@ -41,11 +43,6 @@ .value { width: 32em; } - gr-autocomplete { - --gr-autocomplete: { - padding: 0 var(--spacing-xs); - } - } .hide { display: none; }
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js index 5e4d102..eb96794 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js +++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
@@ -20,44 +20,52 @@ const SUGGESTIONS_LIMIT = 15; const REF_PREFIX = 'refs/heads/'; - Polymer({ - is: 'gr-create-change-dialog', + /** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.URLEncodingMixin + */ + class GrCreateChangeDialog extends Polymer.mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + /** + * Unused in this element, but called by other elements in tests + * e.g gr-repo-commands_test. + */ + Gerrit.FireBehavior, + Gerrit.URLEncodingBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-create-change-dialog'; } - properties: { - repoName: String, - branch: String, - /** @type {?} */ - _repoConfig: Object, - subject: String, - topic: String, - _query: { - type: Function, - value() { - return this._getRepoBranchesSuggestions.bind(this); + static get properties() { + return { + repoName: String, + branch: String, + /** @type {?} */ + _repoConfig: Object, + subject: String, + topic: String, + _query: { + type: Function, + value() { + return this._getRepoBranchesSuggestions.bind(this); + }, }, - }, - baseChange: String, - baseCommit: String, - privateByDefault: String, - canCreate: { - type: Boolean, - notify: true, - value: false, - }, - _privateChangesEnabled: Boolean, - }, - - behaviors: [ - Gerrit.BaseUrlBehavior, - /** - * Unused in this element, but called by other elements in tests - * e.g gr-repo-commands_test. - */ - Gerrit.FireBehavior, - Gerrit.URLEncodingBehavior, - ], + baseChange: String, + baseCommit: String, + privateByDefault: String, + canCreate: { + type: Boolean, + notify: true, + value: false, + }, + _privateChangesEnabled: Boolean, + }; + } attached() { + super.attached(); if (!this.repoName) { return Promise.resolve(); } const promises = []; @@ -75,19 +83,21 @@ })); return Promise.all(promises); - }, + } - observers: [ - '_allowCreate(branch, subject)', - ], + static get observers() { + return [ + '_allowCreate(branch, subject)', + ]; + } _computeBranchClass(baseChange) { return baseChange ? 'hide' : ''; - }, + } _allowCreate(branch, subject) { this.canCreate = !!branch && !!subject; - }, + } handleCreateChange() { const isPrivate = this.$.privateChangeCheckBox.checked; @@ -99,7 +109,7 @@ if (!changeCreated) { return; } Gerrit.Nav.navigateToChange(changeCreated); }); - }, + } _getRepoBranchesSuggestions(input) { if (input.startsWith(REF_PREFIX)) { @@ -107,22 +117,22 @@ } return this.$.restAPI.getRepoBranches( input, this.repoName, SUGGESTIONS_LIMIT).then(response => { - const branches = []; - let branch; - for (const key in response) { - if (!response.hasOwnProperty(key)) { continue; } - if (response[key].ref.startsWith('refs/heads/')) { - branch = response[key].ref.substring('refs/heads/'.length); - } else { - branch = response[key].ref; - } - branches.push({ - name: branch, - }); - } - return branches; + const branches = []; + let branch; + for (const key in response) { + if (!response.hasOwnProperty(key)) { continue; } + if (response[key].ref.startsWith('refs/heads/')) { + branch = response[key].ref.substring('refs/heads/'.length); + } else { + branch = response[key].ref; + } + branches.push({ + name: branch, }); - }, + } + return branches; + }); + } _formatBooleanString(config) { if (config && config.configured_value === 'TRUE') { @@ -138,10 +148,12 @@ } else { return false; } - }, + } _computePrivateSectionClass(config) { return config ? 'hide' : ''; - }, - }); + } + } + + customElements.define(GrCreateChangeDialog.is, GrCreateChangeDialog); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html index 3a3683f..3367aa3 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html +++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-create-change-dialog</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.html index 8a4287b..d0a1fca 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.html +++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.html
@@ -26,7 +26,9 @@ <dom-module id="gr-create-group-dialog"> <template> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="gr-form-styles"> :host { display: inline-block;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js index 01aeb43..4072986 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js +++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
@@ -17,40 +17,48 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-create-group-dialog', + /** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.URLEncodingMixin + */ + class GrCreateGroupDialog extends Polymer.mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.URLEncodingBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-create-group-dialog'; } - properties: { - params: Object, - hasNewGroupName: { - type: Boolean, - notify: true, - value: false, - }, - _name: Object, - _groupCreated: { - type: Boolean, - value: false, - }, - }, + static get properties() { + return { + params: Object, + hasNewGroupName: { + type: Boolean, + notify: true, + value: false, + }, + _name: Object, + _groupCreated: { + type: Boolean, + value: false, + }, + }; + } - observers: [ - '_updateGroupName(_name)', - ], - - behaviors: [ - Gerrit.BaseUrlBehavior, - Gerrit.URLEncodingBehavior, - ], + static get observers() { + return [ + '_updateGroupName(_name)', + ]; + } _computeGroupUrl(groupId) { return this.getBaseUrl() + '/admin/groups/' + this.encodeURL(groupId, true); - }, + } _updateGroupName(name) { this.hasNewGroupName = !!name; - }, + } handleCreateGroup() { return this.$.restAPI.createGroup({name: this._name}) @@ -62,6 +70,8 @@ page.show(this._computeGroupUrl(group.group_id)); }); }); - }, - }); + } + } + + customElements.define(GrCreateGroupDialog.is, GrCreateGroupDialog); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html index ebca289..3a99526 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html +++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-create-group-dialog</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/page/page.js"></script> @@ -56,7 +56,8 @@ test('name is updated correctly', done => { assert.isFalse(element.hasNewGroupName); - ironInput(element.root).bindValue = GROUP_NAME; + const inputEl = element.root.querySelector('iron-input'); + inputEl.bindValue = GROUP_NAME; setTimeout(() => { assert.isTrue(element.hasNewGroupName);
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html index ea5b84b..5a9213b 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html +++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html
@@ -28,7 +28,9 @@ <dom-module id="gr-create-pointer-dialog"> <template> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="gr-form-styles"> :host { display: inline-block;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js index 4e9da90..cf59af8 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js +++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
@@ -22,35 +22,43 @@ tags: 'tags', }; - Polymer({ - is: 'gr-create-pointer-dialog', + /** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.URLEncodingMixin + */ + class GrCreatePointerDialog extends Polymer.mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.URLEncodingBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-create-pointer-dialog'; } - properties: { - detailType: String, - repoName: String, - hasNewItemName: { - type: Boolean, - notify: true, - value: false, - }, - itemDetail: String, - _itemName: String, - _itemRevision: String, - _itemAnnotation: String, - }, + static get properties() { + return { + detailType: String, + repoName: String, + hasNewItemName: { + type: Boolean, + notify: true, + value: false, + }, + itemDetail: String, + _itemName: String, + _itemRevision: String, + _itemAnnotation: String, + }; + } - behaviors: [ - Gerrit.BaseUrlBehavior, - Gerrit.URLEncodingBehavior, - ], - - observers: [ - '_updateItemName(_itemName)', - ], + static get observers() { + return [ + '_updateItemName(_itemName)', + ]; + } _updateItemName(name) { this.hasNewItemName = !!name; - }, + } _computeItemUrl(project) { if (this.itemDetail === DETAIL_TYPES.branches) { @@ -60,7 +68,7 @@ return this.getBaseUrl() + '/admin/repos/' + this.encodeURL(this.repoName, true) + ',tags'; } - }, + } handleCreateItem() { const USE_HEAD = this._itemRevision ? this._itemRevision : 'HEAD'; @@ -82,10 +90,12 @@ } }); } - }, + } _computeHideItemClass(type) { return type === DETAIL_TYPES.branches ? 'hideItem' : ''; - }, - }); + } + } + + customElements.define(GrCreatePointerDialog.is, GrCreatePointerDialog); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html index 08e8213..41b8eca 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html +++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-create-pointer-dialog</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -39,6 +39,10 @@ let element; let sandbox; + const ironInput = function(element) { + return Polymer.dom(element).querySelector('iron-input'); + }; + setup(() => { sandbox = sinon.sandbox.create(); stub('gr-rest-api-interface', {
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html index d1a2471..b78090c 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html +++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html
@@ -29,7 +29,9 @@ <dom-module id="gr-create-repo-dialog"> <template> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="gr-form-styles"> :host { display: inline-block; @@ -38,14 +40,7 @@ width: 20em; } gr-autocomplete { - border: none; - --gr-autocomplete: { - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - height: 2em; - padding: 0 var(--spacing-xs); - width: 20em; - } + width: 20em; } </style>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js index bb2b5f2..a6da3a1 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js +++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
@@ -17,69 +17,77 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-create-repo-dialog', + /** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.URLEncodingMixin + */ + class GrCreateRepoDialog extends Polymer.mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.URLEncodingBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-create-repo-dialog'; } - properties: { - params: Object, - hasNewRepoName: { - type: Boolean, - notify: true, - value: false, - }, + static get properties() { + return { + params: Object, + hasNewRepoName: { + type: Boolean, + notify: true, + value: false, + }, - /** @type {?} */ - _repoConfig: { - type: Object, - value: () => { + /** @type {?} */ + _repoConfig: { + type: Object, + value: () => { // Set default values for dropdowns. - return { - create_empty_commit: true, - permissions_only: false, - }; + return { + create_empty_commit: true, + permissions_only: false, + }; + }, }, - }, - _repoCreated: { - type: Boolean, - value: false, - }, - _repoOwner: String, - _repoOwnerId: { - type: String, - observer: '_repoOwnerIdUpdate', - }, - - _query: { - type: Function, - value() { - return this._getRepoSuggestions.bind(this); + _repoCreated: { + type: Boolean, + value: false, }, - }, - _queryGroups: { - type: Function, - value() { - return this._getGroupSuggestions.bind(this); + _repoOwner: String, + _repoOwnerId: { + type: String, + observer: '_repoOwnerIdUpdate', }, - }, - }, - observers: [ - '_updateRepoName(_repoConfig.name)', - ], + _query: { + type: Function, + value() { + return this._getRepoSuggestions.bind(this); + }, + }, + _queryGroups: { + type: Function, + value() { + return this._getGroupSuggestions.bind(this); + }, + }, + }; + } - behaviors: [ - Gerrit.BaseUrlBehavior, - Gerrit.URLEncodingBehavior, - ], + static get observers() { + return [ + '_updateRepoName(_repoConfig.name)', + ]; + } _computeRepoUrl(repoName) { return this.getBaseUrl() + '/admin/repos/' + this.encodeURL(repoName, true); - }, + } _updateRepoName(name) { this.hasNewRepoName = !!name; - }, + } _repoOwnerIdUpdate(id) { if (id) { @@ -87,7 +95,7 @@ } else { this.set('_repoConfig.owners', undefined); } - }, + } handleCreateRepo() { return this.$.restAPI.createRepo(this._repoConfig) @@ -97,7 +105,7 @@ page.show(this._computeRepoUrl(this._repoConfig.name)); } }); - }, + } _getRepoSuggestions(input) { return this.$.restAPI.getSuggestedProjects(input) @@ -112,7 +120,7 @@ } return repos; }); - }, + } _getGroupSuggestions(input) { return this.$.restAPI.getSuggestedGroups(input) @@ -127,6 +135,8 @@ } return groups; }); - }, - }); + } + } + + customElements.define(GrCreateRepoDialog.is, GrCreateRepoDialog); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html index 7e32c5c..fa481c2 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html +++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-create-repo-dialog</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html index c15f091..4ed751d 100644 --- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html +++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html
@@ -27,7 +27,9 @@ <dom-module id="gr-group-audit-log"> <template> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="gr-table-styles"> /* GenericList style centers the last column, but we don't want that here. */ .genericList tr th:last-of-type,
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js index 8901d4a..57b9689 100644 --- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js +++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
@@ -19,30 +19,38 @@ const GROUP_EVENTS = ['ADD_GROUP', 'REMOVE_GROUP']; - Polymer({ - is: 'gr-group-audit-log', + /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.ListViewMixin + */ + class GrGroupAuditLog extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.ListViewBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-group-audit-log'; } - properties: { - groupId: String, - _auditLog: Array, - _loading: { - type: Boolean, - value: true, - }, - }, - - behaviors: [ - Gerrit.FireBehavior, - Gerrit.ListViewBehavior, - ], + static get properties() { + return { + groupId: String, + _auditLog: Array, + _loading: { + type: Boolean, + value: true, + }, + }; + } attached() { + super.attached(); this.fire('title-change', {title: 'Audit Log'}); - }, + } ready() { + super.ready(); this._getAuditLogs(); - }, + } _getAuditLogs() { if (!this.groupId) { return ''; } @@ -60,11 +68,11 @@ this._auditLog = auditLog; this._loading = false; }); - }, + } _status(item) { return item.disabled ? 'Disabled' : 'Enabled'; - }, + } itemType(type) { let item; @@ -81,11 +89,11 @@ item = ''; } return item; - }, + } _isGroupEvent(type) { return GROUP_EVENTS.indexOf(type) !== -1; - }, + } _computeGroupUrl(group) { if (group && group.url && group.id) { @@ -93,11 +101,11 @@ } return ''; - }, + } _getIdForUser(account) { return account._account_id ? ' (' + account._account_id + ')' : ''; - }, + } _getNameForGroup(group) { if (group && group.name) { @@ -108,6 +116,8 @@ } return ''; - }, - }); + } + } + + customElements.define(GrGroupAuditLog.is, GrGroupAuditLog); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html index 313d465..d517242 100644 --- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html +++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-group-audit-log</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html index 86f66c4..cf24793 100644 --- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html +++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
@@ -33,19 +33,21 @@ <dom-module id="gr-group-members"> <template> - <style include="gr-form-styles"></style> - <style include="gr-table-styles"></style> - <style include="gr-subpage-styles"></style> + <style include="gr-form-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="gr-table-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="gr-subpage-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="shared-styles"> .input { width: 15em; } gr-autocomplete { width: 20em; - --gr-autocomplete: { - height: 2em; - width: 20em; - } } a { color: var(--primary-text-color);
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js index 958c0ac..f3a7ec7 100644 --- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js +++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
@@ -23,57 +23,65 @@ const URL_REGEX = '^(?:[a-z]+:)?//'; - Polymer({ - is: 'gr-group-members', + /** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.URLEncodingMixin + */ + class GrGroupMembers extends Polymer.mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.FireBehavior, + Gerrit.URLEncodingBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-group-members'; } - properties: { - groupId: Number, - _groupMemberSearchId: String, - _groupMemberSearchName: String, - _includedGroupSearchId: String, - _includedGroupSearchName: String, - _loading: { - type: Boolean, - value: true, - }, - _groupName: String, - _groupMembers: Object, - _includedGroups: Object, - _itemName: String, - _itemType: String, - _queryMembers: { - type: Function, - value() { - return this._getAccountSuggestions.bind(this); + static get properties() { + return { + groupId: Number, + _groupMemberSearchId: String, + _groupMemberSearchName: String, + _includedGroupSearchId: String, + _includedGroupSearchName: String, + _loading: { + type: Boolean, + value: true, }, - }, - _queryIncludedGroup: { - type: Function, - value() { - return this._getGroupSuggestions.bind(this); + _groupName: String, + _groupMembers: Object, + _includedGroups: Object, + _itemName: String, + _itemType: String, + _queryMembers: { + type: Function, + value() { + return this._getAccountSuggestions.bind(this); + }, }, - }, - _groupOwner: { - type: Boolean, - value: false, - }, - _isAdmin: { - type: Boolean, - value: false, - }, - }, - - behaviors: [ - Gerrit.BaseUrlBehavior, - Gerrit.FireBehavior, - Gerrit.URLEncodingBehavior, - ], + _queryIncludedGroup: { + type: Function, + value() { + return this._getGroupSuggestions.bind(this); + }, + }, + _groupOwner: { + type: Boolean, + value: false, + }, + _isAdmin: { + type: Boolean, + value: false, + }, + }; + } attached() { + super.attached(); this._loadGroupDetails(); this.fire('title-change', {title: 'Members'}); - }, + } _loadGroupDetails() { if (!this.groupId) { return; } @@ -113,15 +121,15 @@ this._loading = false; }); }); - }, + } _computeLoadingClass(loading) { return loading ? 'loading' : ''; - }, + } _isLoading() { return this._loading || this._loading === undefined; - }, + } _computeGroupUrl(url) { if (!url) { return; } @@ -136,21 +144,21 @@ return this.getBaseUrl() + url.slice(1); } return this.getBaseUrl() + url; - }, + } _handleSavingGroupMember() { return this.$.restAPI.saveGroupMembers(this._groupName, this._groupMemberSearchId).then(config => { - if (!config) { - return; - } - this.$.restAPI.getGroupMembers(this._groupName).then(members => { - this._groupMembers = members; - }); - this._groupMemberSearchName = ''; - this._groupMemberSearchId = ''; - }); - }, + if (!config) { + return; + } + this.$.restAPI.getGroupMembers(this._groupName).then(members => { + this._groupMembers = members; + }); + this._groupMemberSearchName = ''; + this._groupMemberSearchId = ''; + }); + } _handleDeleteConfirm() { this.$.overlay.close(); @@ -177,11 +185,11 @@ } }); } - }, + } _handleConfirmDialogCancel() { this.$.overlay.close(); - }, + } _handleDeleteMember(e) { const id = e.model.get('item._account_id'); @@ -196,7 +204,7 @@ this._itemId = id; this._itemType = 'member'; this.$.overlay.open(); - }, + } _handleSavingIncludedGroups() { return this.$.restAPI.saveIncludedGroup(this._groupName, @@ -222,7 +230,7 @@ this._includedGroupSearchName = ''; this._includedGroupSearchId = ''; }); - }, + } _handleDeleteIncludedGroup(e) { const id = decodeURIComponent(e.model.get('item.id')).replace(/\+/g, ' '); @@ -233,31 +241,31 @@ this._itemId = id; this._itemType = 'includedGroup'; this.$.overlay.open(); - }, + } _getAccountSuggestions(input) { if (input.length === 0) { return Promise.resolve([]); } return this.$.restAPI.getSuggestedAccounts( input, SUGGESTIONS_LIMIT).then(accounts => { - const accountSuggestions = []; - let nameAndEmail; - if (!accounts) { return []; } - for (const key in accounts) { - if (!accounts.hasOwnProperty(key)) { continue; } - if (accounts[key].email !== undefined) { - nameAndEmail = accounts[key].name + + const accountSuggestions = []; + let nameAndEmail; + if (!accounts) { return []; } + for (const key in accounts) { + if (!accounts.hasOwnProperty(key)) { continue; } + if (accounts[key].email !== undefined) { + nameAndEmail = accounts[key].name + ' <' + accounts[key].email + '>'; - } else { - nameAndEmail = accounts[key].name; - } - accountSuggestions.push({ - name: nameAndEmail, - value: accounts[key]._account_id, - }); - } - return accountSuggestions; + } else { + nameAndEmail = accounts[key].name; + } + accountSuggestions.push({ + name: nameAndEmail, + value: accounts[key]._account_id, }); - }, + } + return accountSuggestions; + }); + } _getGroupSuggestions(input) { return this.$.restAPI.getSuggestedGroups(input) @@ -272,10 +280,12 @@ } return groups; }); - }, + } _computeHideItemClass(owner, admin) { return admin || owner ? '' : 'canModify'; - }, - }); + } + } + + customElements.define(GrGroupMembers.is, GrGroupMembers); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html index 15a59c8..e90e6fd 100644 --- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html +++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-group-members</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -169,10 +169,10 @@ .querySelectorAll('.nameColumn a')[0].href, includedGroups[0].url); assert.equal(Polymer.dom(element.root) .querySelectorAll('.nameColumn a')[1].href, - 'https://test/site/group/url'); + 'https://test/site/group/url'); assert.equal(Polymer.dom(element.root) .querySelectorAll('.nameColumn a')[2].href, - 'https://test/site/group/url'); + 'https://test/site/group/url'); }); test('save members correctly', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.html b/polygerrit-ui/app/elements/admin/gr-group/gr-group.html index 19879cb..faabe84 100644 --- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.html +++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
@@ -28,7 +28,9 @@ <dom-module id="gr-group"> <template> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="gr-subpage-styles"> h3.edited:after { color: var(--deemphasized-text-color); @@ -38,7 +40,9 @@ margin-top: var(--spacing-s); } </style> - <style include="gr-form-styles"></style> + <style include="gr-form-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <main class="gr-form-styles read-only"> <div id="loading" class$="[[_computeLoadingClass(_loading)]]"> Loading...
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js index 68228b4..6597b4d 100644 --- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js +++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
@@ -30,78 +30,85 @@ }, }; - Polymer({ - is: 'gr-group', - + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrGroup extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-group'; } /** * Fired when the group name changes. * * @event name-changed */ - properties: { - groupId: Number, - _rename: { - type: Boolean, - value: false, - }, - _groupIsInternal: Boolean, - _description: { - type: Boolean, - value: false, - }, - _owner: { - type: Boolean, - value: false, - }, - _options: { - type: Boolean, - value: false, - }, - _loading: { - type: Boolean, - value: true, - }, - /** @type {?} */ - _groupConfig: Object, - _groupConfigOwner: String, - _groupName: Object, - _groupOwner: { - type: Boolean, - value: false, - }, - _submitTypes: { - type: Array, - value() { - return Object.values(OPTIONS); + static get properties() { + return { + groupId: Number, + _rename: { + type: Boolean, + value: false, }, - }, - _query: { - type: Function, - value() { - return this._getGroupSuggestions.bind(this); + _groupIsInternal: Boolean, + _description: { + type: Boolean, + value: false, }, - }, - _isAdmin: { - type: Boolean, - value: false, - }, - }, + _owner: { + type: Boolean, + value: false, + }, + _options: { + type: Boolean, + value: false, + }, + _loading: { + type: Boolean, + value: true, + }, + /** @type {?} */ + _groupConfig: Object, + _groupConfigOwner: String, + _groupName: Object, + _groupOwner: { + type: Boolean, + value: false, + }, + _submitTypes: { + type: Array, + value() { + return Object.values(OPTIONS); + }, + }, + _query: { + type: Function, + value() { + return this._getGroupSuggestions.bind(this); + }, + }, + _isAdmin: { + type: Boolean, + value: false, + }, + }; + } - behaviors: [ - Gerrit.FireBehavior, - ], - - observers: [ - '_handleConfigName(_groupConfig.name)', - '_handleConfigOwner(_groupConfig.owner, _groupConfigOwner)', - '_handleConfigDescription(_groupConfig.description)', - '_handleConfigOptions(_groupConfig.options.visible_to_all)', - ], + static get observers() { + return [ + '_handleConfigName(_groupConfig.name)', + '_handleConfigOwner(_groupConfig.owner, _groupConfigOwner)', + '_handleConfigDescription(_groupConfig.description)', + '_handleConfigOptions(_groupConfig.options.visible_to_all)', + ]; + } attached() { + super.attached(); this._loadGroup(); - }, + } _loadGroup() { if (!this.groupId) { return; } @@ -143,15 +150,15 @@ this._loading = false; }); }); - }, + } _computeLoadingClass(loading) { return loading ? 'loading' : ''; - }, + } _isLoading() { return this._loading || this._loading === undefined; - }, + } _handleSaveName() { return this.$.restAPI.saveGroupName(this.groupId, this._groupConfig.name) @@ -163,7 +170,7 @@ this._rename = false; } }); - }, + } _handleSaveOwner() { let owner = this._groupConfig.owner; @@ -172,16 +179,16 @@ } return this.$.restAPI.saveGroupOwner(this.groupId, owner).then(config => { - this._owner = false; - }); - }, + this._owner = false; + }); + } _handleSaveDescription() { return this.$.restAPI.saveGroupDescription(this.groupId, this._groupConfig.description).then(config => { - this._description = false; - }); - }, + this._description = false; + }); + } _handleSaveOptions() { const visible = this._groupConfig.options.visible_to_all; @@ -190,33 +197,33 @@ return this.$.restAPI.saveGroupOptions(this.groupId, options).then(config => { - this._options = false; - }); - }, + this._options = false; + }); + } _handleConfigName() { if (this._isLoading()) { return; } this._rename = true; - }, + } _handleConfigOwner() { if (this._isLoading()) { return; } this._owner = true; - }, + } _handleConfigDescription() { if (this._isLoading()) { return; } this._description = true; - }, + } _handleConfigOptions() { if (this._isLoading()) { return; } this._options = true; - }, + } _computeHeaderClass(configChanged) { return configChanged ? 'edited' : ''; - }, + } _getGroupSuggestions(input) { return this.$.restAPI.getSuggestedGroups(input) @@ -231,10 +238,12 @@ } return groups; }); - }, + } _computeGroupDisabled(owner, admin, groupIsInternal) { return groupIsInternal && (admin || owner) ? false : true; - }, - }); + } + } + + customElements.define(GrGroup.is, GrGroup); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html index 1672e85..0d7aae2 100644 --- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html +++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-group</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html index 931e2cd..e07f911 100644 --- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html +++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
@@ -82,8 +82,12 @@ display: block; } </style> - <style include="gr-form-styles"></style> - <style include="gr-menu-page-styles"></style> + <style include="gr-form-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="gr-menu-page-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <section id="permission" class$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]">
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js index b31aee6..e94b7fc 100644 --- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js +++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
@@ -25,91 +25,101 @@ ]; /** + * @appliesMixin Gerrit.AccessMixin + * @appliesMixin Gerrit.FireMixin + */ + /** * Fired when the permission has been modified or removed. * * @event access-modified */ - /** * Fired when a permission that was previously added was removed. + * * @event added-permission-removed */ + class GrPermission extends Polymer.mixinBehaviors( [ + Gerrit.AccessBehavior, + /** + * Unused in this element, but called by other elements in tests + * e.g gr-access-section_test. + */ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-permission'; } - Polymer({ - is: 'gr-permission', - - properties: { - labels: Object, - name: String, - /** @type {?} */ - permission: { - type: Object, - observer: '_sortPermission', - notify: true, - }, - groups: Object, - section: String, - editing: { - type: Boolean, - value: false, - observer: '_handleEditingChanged', - }, - _label: { - type: Object, - computed: '_computeLabel(permission, labels)', - }, - _groupFilter: String, - _query: { - type: Function, - value() { - return this._getGroupSuggestions.bind(this); + static get properties() { + return { + labels: Object, + name: String, + /** @type {?} */ + permission: { + type: Object, + observer: '_sortPermission', + notify: true, }, - }, - _rules: Array, - _groupsWithRules: Object, - _deleted: { - type: Boolean, - value: false, - }, - _originalExclusiveValue: Boolean, - }, + groups: Object, + section: String, + editing: { + type: Boolean, + value: false, + observer: '_handleEditingChanged', + }, + _label: { + type: Object, + computed: '_computeLabel(permission, labels)', + }, + _groupFilter: String, + _query: { + type: Function, + value() { + return this._getGroupSuggestions.bind(this); + }, + }, + _rules: Array, + _groupsWithRules: Object, + _deleted: { + type: Boolean, + value: false, + }, + _originalExclusiveValue: Boolean, + }; + } - behaviors: [ - Gerrit.AccessBehavior, - /** - * Unused in this element, but called by other elements in tests - * e.g gr-access-section_test. - */ - Gerrit.FireBehavior, - ], + static get observers() { + return [ + '_handleRulesChanged(_rules.splices)', + ]; + } - observers: [ - '_handleRulesChanged(_rules.splices)', - ], - - listeners: { - 'access-saved': '_handleAccessSaved', - }, + created() { + super.created(); + this.addEventListener('access-saved', + () => this._handleAccessSaved()); + } ready() { + super.ready(); this._setupValues(); - }, + } _setupValues() { if (!this.permission) { return; } this._originalExclusiveValue = !!this.permission.value.exclusive; Polymer.dom.flush(); - }, + } _handleAccessSaved() { // Set a new 'original' value to keep track of after the value has been // saved. this._setupValues(); - }, + } _permissionIsOwnerOrGlobal(permissionId, section) { return permissionId === 'owner' || section === 'GLOBAL_CAPABILITIES'; - }, + } _handleEditingChanged(editing, editingOld) { // Ignore when editing gets set initially. @@ -130,20 +140,20 @@ this.set(['permission', 'value', 'exclusive'], this._originalExclusiveValue); } - }, + } _handleAddedRuleRemoved(e) { const index = e.model.index; this._rules = this._rules.slice(0, index) .concat(this._rules.slice(index + 1, this._rules.length)); - }, + } _handleValueChange() { this.permission.value.modified = true; // Allows overall access page to know a change has been made. this.dispatchEvent( new CustomEvent('access-modified', {bubbles: true, composed: true})); - }, + } _handleRemovePermission() { if (this.permission.value.added) { @@ -154,16 +164,16 @@ this.permission.value.deleted = true; this.dispatchEvent( new CustomEvent('access-modified', {bubbles: true, composed: true})); - }, + } _handleRulesChanged(changeRecord) { // Update the groups to exclude in the autocomplete. this._groupsWithRules = this._computeGroupsWithRules(this._rules); - }, + } _sortPermission(permission) { this._rules = this.toSortedArray(permission.value.rules); - }, + } _computeSectionClass(editing, deleted) { const classList = []; @@ -174,12 +184,12 @@ classList.push('deleted'); } return classList.join(' '); - }, + } _handleUndoRemove() { this._deleted = false; delete this.permission.value.deleted; - }, + } _computeLabel(permission, labels) { if (!labels || !permission || @@ -195,7 +205,7 @@ values: this._computeLabelValues(labels[labelName].values), }; return label; - }, + } _computeLabelValues(values) { const valuesArr = []; @@ -211,7 +221,7 @@ valuesArr.push({value: parseInt(key, 10), text}); } return valuesArr; - }, + } /** * @param {!Array} rules @@ -224,12 +234,12 @@ groups[rule.id] = true; } return groups; - }, + } _computeGroupName(groups, groupId) { return groups && groups[groupId] && groups[groupId].name ? - groups[groupId].name : groupId; - }, + groups[groupId].name : groupId; + } _getGroupSuggestions() { return this.$.restAPI.getSuggestedGroups( @@ -249,7 +259,7 @@ return !this._groupsWithRules[group.value.id]; }); }); - }, + } /** * Handles adding a skeleton item to the dom-repeat. @@ -283,12 +293,14 @@ this.set(['permission', 'value', 'rules', groupId], value); this.dispatchEvent( new CustomEvent('access-modified', {bubbles: true, composed: true})); - }, + } _computeHasRange(name) { if (!name) { return false; } return RANGE_NAMES.includes(name.toUpperCase()); - }, - }); + } + } + + customElements.define(GrPermission.is, GrPermission); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html index 8e57534..7919b28 100644 --- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html +++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-permission</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/page/page.js"></script> @@ -385,7 +385,8 @@ assert.isFalse(element._originalExclusiveValue); assert.isNotOk(element.permission.value.modified); - MockInteractions.tap(element.$$('#exclusiveToggle')); + MockInteractions.tap(element.shadowRoot + .querySelector('#exclusiveToggle')); flushAsynchronousOperations(); assert.isTrue(element.permission.value.exclusive); assert.isTrue(element.permission.value.modified); @@ -405,21 +406,25 @@ }); test('Exclusive hidden for owner permission', () => { - assert.equal(getComputedStyle(element.$$('#exclusiveToggle')).display, - 'flex'); + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('#exclusiveToggle')).display, + 'flex'); element.set(['permission', 'id'], 'owner'); flushAsynchronousOperations(); - assert.equal(getComputedStyle(element.$$('#exclusiveToggle')).display, - 'none'); + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('#exclusiveToggle')).display, + 'none'); }); test('Exclusive hidden for any global permissions', () => { - assert.equal(getComputedStyle(element.$$('#exclusiveToggle')).display, - 'flex'); + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('#exclusiveToggle')).display, + 'flex'); element.section = 'GLOBAL_CAPABILITIES'; flushAsynchronousOperations(); - assert.equal(getComputedStyle(element.$$('#exclusiveToggle')).display, - 'none'); + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('#exclusiveToggle')).display, + 'none'); }); }); });
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.html b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.html index 2761526..f6c744b 100644 --- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.html +++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.html
@@ -25,7 +25,9 @@ <dom-module id="gr-plugin-config-array-editor"> <template> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="gr-form-styles"> .wrapper { width: 30em;
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js index bb0d501..854bbbc 100644 --- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js +++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js
@@ -17,39 +17,42 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-plugin-config-array-editor', - + class GrPluginConfigArrayEditor extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-plugin-config-array-editor'; } /** * Fired when the plugin config option changes. * * @event plugin-config-option-changed */ - properties: { + static get properties() { + return { /** @type {?} */ - pluginOption: Object, - /** @type {Boolean} */ - disabled: { - type: Boolean, - computed: '_computeDisabled(pluginOption.*)', - }, - /** @type {?} */ - _newValue: { - type: String, - value: '', - }, - }, + pluginOption: Object, + /** @type {Boolean} */ + disabled: { + type: Boolean, + computed: '_computeDisabled(pluginOption.*)', + }, + /** @type {?} */ + _newValue: { + type: String, + value: '', + }, + }; + } _computeDisabled(record) { return !(record && record.base && record.base.info && record.base.info.editable); - }, + } _handleAddTap(e) { e.preventDefault(); this._handleAdd(); - }, + } _handleInputKeydown(e) { // Enter. @@ -57,20 +60,20 @@ e.preventDefault(); this._handleAdd(); } - }, + } _handleAdd() { if (!this._newValue.length) { return; } this._dispatchChanged( this.pluginOption.info.values.concat([this._newValue])); this._newValue = ''; - }, + } _handleDelete(e) { const value = Polymer.dom(e).localTarget.dataset.item; this._dispatchChanged( this.pluginOption.info.values.filter(str => str !== value)); - }, + } _dispatchChanged(values) { const {_key, info} = this.pluginOption; @@ -81,10 +84,13 @@ }; this.dispatchEvent( new CustomEvent('plugin-config-option-changed', {detail})); - }, + } _computeShowInputRow(disabled) { return disabled ? 'hide' : ''; - }, - }); + } + } + + customElements.define(GrPluginConfigArrayEditor.is, + GrPluginConfigArrayEditor); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html index 39e4ddc..5198ad0 100644 --- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html +++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-plugin-config-array-editor</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.html b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.html index 6ef84bf..b056f92 100644 --- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.html +++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.html
@@ -25,8 +25,12 @@ <dom-module id="gr-plugin-list"> <template> - <style include="shared-styles"></style> - <style include="gr-table-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="gr-table-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <gr-list-view filter="[[_filter]]" items-per-page="[[_pluginsPerPage]]"
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js index b77f078..262f3d5 100644 --- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js +++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
@@ -17,60 +17,67 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-plugin-list', + /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.ListViewMixin + */ + class GrPluginList extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.ListViewBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-plugin-list'; } - properties: { + static get properties() { + return { /** * URL params passed from the router. */ - params: { - type: Object, - observer: '_paramsChanged', - }, - /** - * Offset of currently visible query results. - */ - _offset: { - type: Number, - value: 0, - }, - _path: { - type: String, - readOnly: true, - value: '/admin/plugins', - }, - _plugins: Array, - /** - * Because we request one more than the pluginsPerPage, _shownPlugins - * maybe one less than _plugins. - * */ - _shownPlugins: { - type: Array, - computed: 'computeShownItems(_plugins)', - }, - _pluginsPerPage: { - type: Number, - value: 25, - }, - _loading: { - type: Boolean, - value: true, - }, - _filter: { - type: String, - value: '', - }, - }, - - behaviors: [ - Gerrit.FireBehavior, - Gerrit.ListViewBehavior, - ], + params: { + type: Object, + observer: '_paramsChanged', + }, + /** + * Offset of currently visible query results. + */ + _offset: { + type: Number, + value: 0, + }, + _path: { + type: String, + readOnly: true, + value: '/admin/plugins', + }, + _plugins: Array, + /** + * Because we request one more than the pluginsPerPage, _shownPlugins + * maybe one less than _plugins. + * */ + _shownPlugins: { + type: Array, + computed: 'computeShownItems(_plugins)', + }, + _pluginsPerPage: { + type: Number, + value: 25, + }, + _loading: { + type: Boolean, + value: true, + }, + _filter: { + type: String, + value: '', + }, + }; + } attached() { + super.attached(); this.fire('title-change', {title: 'Plugins'}); - }, + } _paramsChanged(params) { this._loading = true; @@ -79,7 +86,7 @@ return this._getPlugins(this._filter, this._pluginsPerPage, this._offset); - }, + } _getPlugins(filter, pluginsPerPage, offset) { const errFn = response => { @@ -92,21 +99,23 @@ return; } this._plugins = Object.keys(plugins) - .map(key => { - const plugin = plugins[key]; - plugin.name = key; - return plugin; - }); + .map(key => { + const plugin = plugins[key]; + plugin.name = key; + return plugin; + }); this._loading = false; }); - }, + } _status(item) { return item.disabled === true ? 'Disabled' : 'Enabled'; - }, + } _computePluginUrl(id) { return this.getUrl('/', id); - }, - }); + } + } + + customElements.define(GrPluginList.is, GrPluginList); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html index 96fff60..98636a8 100644 --- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html +++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-plugin-list</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/page/page.js"></script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html index ea12908..fab730d 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html +++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html
@@ -33,7 +33,9 @@ <dom-module id="gr-repo-access"> <template> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="gr-subpage-styles"> gr-button, #inheritsFrom, @@ -68,7 +70,9 @@ display: inline-block; } </style> - <style include="gr-menu-page-styles"></style> + <style include="gr-menu-page-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <main class$="[[_computeMainClass(_ownerOf, _canUpload, _editing)]]"> <div id="loading" class$="[[_computeLoadingClass(_loading)]]"> Loading...
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js index 94bef54..bffad1c 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js +++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
@@ -67,67 +67,77 @@ */ Defs.projectAccessInput; + /** + * @appliesMixin Gerrit.AccessMixin + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.URLEncodingMixin + */ + class GrRepoAccess extends Polymer.mixinBehaviors( [ + Gerrit.AccessBehavior, + Gerrit.BaseUrlBehavior, + Gerrit.FireBehavior, + Gerrit.URLEncodingBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-repo-access'; } - Polymer({ - is: 'gr-repo-access', - - properties: { - repo: { - type: String, - observer: '_repoChanged', - }, - // The current path - path: String, - - _canUpload: { - type: Boolean, - value: false, - }, - _inheritFromFilter: String, - _query: { - type: Function, - value() { - return this._getInheritFromSuggestions.bind(this); + static get properties() { + return { + repo: { + type: String, + observer: '_repoChanged', }, - }, - _ownerOf: Array, - _capabilities: Object, - _groups: Object, - /** @type {?} */ - _inheritsFrom: Object, - _labels: Object, - _local: Object, - _editing: { - type: Boolean, - value: false, - observer: '_handleEditingChanged', - }, - _modified: { - type: Boolean, - value: false, - }, - _sections: Array, - _weblinks: Array, - _loading: { - type: Boolean, - value: true, - }, - }, + // The current path + path: String, - behaviors: [ - Gerrit.AccessBehavior, - Gerrit.BaseUrlBehavior, - Gerrit.FireBehavior, - Gerrit.URLEncodingBehavior, - ], + _canUpload: { + type: Boolean, + value: false, + }, + _inheritFromFilter: String, + _query: { + type: Function, + value() { + return this._getInheritFromSuggestions.bind(this); + }, + }, + _ownerOf: Array, + _capabilities: Object, + _groups: Object, + /** @type {?} */ + _inheritsFrom: Object, + _labels: Object, + _local: Object, + _editing: { + type: Boolean, + value: false, + observer: '_handleEditingChanged', + }, + _modified: { + type: Boolean, + value: false, + }, + _sections: Array, + _weblinks: Array, + _loading: { + type: Boolean, + value: true, + }, + }; + } - listeners: { - 'access-modified': '_handleAccessModified', - }, + created() { + super.created(); + this.addEventListener('access-modified', + () => + this._handleAccessModified()); + } _handleAccessModified() { this._modified = true; - }, + } /** * @param {string} repo @@ -139,7 +149,7 @@ if (!repo) { return Promise.resolve(); } return this._reload(repo); - }, + } _reload(repo) { const promises = []; @@ -167,7 +177,7 @@ // current value appears. If there is no parent repo, it is // initialized as an empty string. this._inheritFromFilter = res.inherits_from ? - this._inheritsFrom.name : ''; + this._inheritsFrom.name : ''; this._local = res.local; this._groups = res.groups; this._weblinks = res.config_web_links || []; @@ -196,7 +206,7 @@ this._sections = sections; this._loading = false; }); - }, + } _handleUpdateInheritFrom(e) { if (!this._inheritsFrom) { @@ -205,7 +215,7 @@ this._inheritsFrom.id = e.detail.value; this._inheritsFrom.name = this._inheritFromFilter; this._handleAccessModified(); - }, + } _getInheritFromSuggestions() { return this.$.restAPI.getRepos( @@ -222,33 +232,33 @@ } return projects; }); - }, + } _computeLoadingClass(loading) { return loading ? 'loading' : ''; - }, + } _handleEdit() { this._editing = !this._editing; - }, + } _editOrCancel(editing) { return editing ? 'Cancel' : 'Edit'; - }, + } _computeWebLinkClass(weblinks) { return weblinks && weblinks.length ? 'show' : ''; - }, + } _computeShowInherit(inheritsFrom) { return inheritsFrom ? 'show' : ''; - }, + } _handleAddedSectionRemoved(e) { const index = e.model.index; this._sections = this._sections.slice(0, index) .concat(this._sections.slice(index + 1, this._sections.length)); - }, + } _handleEditingChanged(editing, editingOld) { // Ignore when editing gets set initially. @@ -267,7 +277,7 @@ delete this._local[key]; } } - }, + } /** * @param {!Defs.projectAccessInput} addRemoveObj @@ -297,7 +307,7 @@ curPos = curPos[item]; } return addRemoveObj; - }, + } /** * Used to recursively remove any objects with a 'deleted' bit. @@ -314,7 +324,7 @@ this._recursivelyRemoveDeleted(obj[k]); } } - }, + } _recursivelyUpdateAddRemoveObj(obj, addRemoveObj, path = []) { for (const k in obj) { @@ -346,6 +356,7 @@ /** * As add / delete both can happen in the new section, * so here to make sure it will remove the deleted ones. + * * @see Issue 11339 */ this._recursivelyRemoveDeleted(addRemoveObj.add[k]); @@ -355,7 +366,7 @@ path.concat(k)); } } - }, + } /** * Returns an object formatted for saving or submitting access changes for @@ -370,16 +381,16 @@ }; const originalInheritsFromId = this._originalInheritsFrom ? - this.singleDecodeURL(this._originalInheritsFrom.id) : - null; + this.singleDecodeURL(this._originalInheritsFrom.id) : + null; const inheritsFromId = this._inheritsFrom ? - this.singleDecodeURL(this._inheritsFrom.id) : - null; + this.singleDecodeURL(this._inheritsFrom.id) : + null; const inheritFromChanged = // Inherit from changed - (originalInheritsFromId - && originalInheritsFromId !== inheritsFromId) || + (originalInheritsFromId && + originalInheritsFromId !== inheritsFromId) || // Inherit from added (did not have one initially); (!originalInheritsFromId && inheritsFromId); @@ -389,7 +400,7 @@ addRemoveObj.parent = inheritsFromId; } return addRemoveObj; - }, + } _handleCreateSection() { let newRef = 'refs/for/*'; @@ -404,7 +415,7 @@ Polymer.dom.flush(); Polymer.dom(this.root).querySelector('gr-access-section:last-of-type') .editReference(); - }, + } _getObjforSave() { const addRemoveObj = this._computeAddAndRemove(); @@ -427,7 +438,7 @@ obj.parent = addRemoveObj.parent; } return obj; - }, + } _handleSave() { const obj = this._getObjforSave(); @@ -436,7 +447,7 @@ .then(() => { this._reload(this.repo); }); - }, + } _handleSaveForReview() { const obj = this._getObjforSave(); @@ -445,15 +456,15 @@ .then(change => { Gerrit.Nav.navigateToChange(change); }); - }, + } _computeSaveReviewBtnClass(canUpload) { return !canUpload ? 'invisible' : ''; - }, + } _computeSaveBtnClass(ownerOf) { return ownerOf && ownerOf.length === 0 ? 'invisible' : ''; - }, + } _computeMainClass(ownerOf, canUpload, editing) { const classList = []; @@ -464,11 +475,13 @@ classList.push('editing'); } return classList.join(' '); - }, + } _computeParentHref(repoName) { return this.getBaseUrl() + `/admin/repos/${this.encodeURL(repoName, true)},access`; - }, - }); + } + } + + customElements.define(GrRepoAccess.is, GrRepoAccess); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html index 8b1de6e..fc7dea8 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html +++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo-access</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/page/page.js"></script> @@ -282,7 +282,8 @@ id: 'test-project', }; flushAsynchronousOperations(); - assert.equal(getComputedStyle(element.$$('#editInheritFromInput')) + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('#editInheritFromInput')) .display, 'none'); MockInteractions.tap(element.$.editBtn); @@ -300,7 +301,8 @@ assert.notEqual(getComputedStyle(element.$.saveBtn).display, 'none'); assert.isTrue(element.$.saveBtn.disabled); } - assert.notEqual(getComputedStyle(element.$$('#editInheritFromInput')) + assert.notEqual(getComputedStyle(element.shadowRoot + .querySelector('#editInheritFromInput')) .display, 'none'); // Save button should be enabled after access is modified @@ -365,7 +367,7 @@ id: 'test-project', }; flushAsynchronousOperations(); - element.$$('#editInheritFromInput').fire('commit'); + element.shadowRoot.querySelector('#editInheritFromInput').fire('commit'); sandbox.spy(element, '_handleAccessModified'); element.fire('access-modified'); assert.isTrue(element._handleAccessModified.called); @@ -676,7 +678,7 @@ Polymer.dom(element.$$('gr-access-section').root).querySelectorAll( 'gr-permission')[2]; newPermission._handleAddRuleItem( - {detail: {value: {id: 'Maintainers'}}}); + {detail: {value: {id: 'Maintainers'}}}); assert.deepEqual(element._computeAddAndRemove(), expectedInput); // Modify a section reference. @@ -909,7 +911,7 @@ Polymer.dom(element.$$('gr-access-section').root).querySelectorAll( 'gr-permission')[1]; readPermission._handleAddRuleItem( - {detail: {value: {id: 'Maintainers'}}}); + {detail: {value: {id: 'Maintainers'}}}); expectedInput = { add: {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js index 026c990..48e4c01 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js +++ b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
@@ -17,14 +17,18 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-repo-command', + class GrRepoCommand extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-repo-command'; } - properties: { - title: String, - disabled: Boolean, - tooltip: String, - }, + static get properties() { + return { + title: String, + disabled: Boolean, + tooltip: String, + }; + } /** * Fired when command button is tapped. @@ -35,6 +39,8 @@ _onCommandTap() { this.dispatchEvent( new CustomEvent('command-tap', {bubbles: true, composed: true})); - }, - }); + } + } + + customElements.define(GrRepoCommand.is, GrRepoCommand); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html index 49d8765..b8ec7e8 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html +++ b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo-command</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.html b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.html index 5089f34..b610460 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.html +++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.html
@@ -31,9 +31,15 @@ <dom-module id="gr-repo-commands"> <template> - <style include="shared-styles"></style> - <style include="gr-subpage-styles"></style> - <style include="gr-form-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="gr-subpage-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="gr-form-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <main class="gr-form-styles read-only"> <h1 id="Title">Repository Commands</h1> <div id="loading" class$="[[_computeLoadingClass(_loading)]]">Loading...</div>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js index 92f15b0..db1bd30 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js +++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
@@ -26,30 +26,36 @@ const CREATE_CHANGE_FAILED_MESSAGE = 'Failed to create change.'; const CREATE_CHANGE_SUCCEEDED_MESSAGE = 'Navigating to change'; - Polymer({ - is: 'gr-repo-commands', + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrRepoCommands extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-repo-commands'; } - properties: { - params: Object, - repo: String, - _loading: { - type: Boolean, - value: true, - }, - /** @type {?} */ - _repoConfig: Object, - _canCreate: Boolean, - }, - - behaviors: [ - Gerrit.FireBehavior, - ], + static get properties() { + return { + params: Object, + repo: String, + _loading: { + type: Boolean, + value: true, + }, + /** @type {?} */ + _repoConfig: Object, + _canCreate: Boolean, + }; + } attached() { + super.attached(); this._loadRepo(); this.fire('title-change', {title: 'Repo Commands'}); - }, + } _loadRepo() { if (!this.repo) { return Promise.resolve(); } @@ -65,15 +71,15 @@ this._repoConfig = config; this._loading = false; }); - }, + } _computeLoadingClass(loading) { return loading ? 'loading' : ''; - }, + } _isLoading() { return this._loading || this._loading === undefined; - }, + } _handleRunningGC() { return this.$.restAPI.runRepoGC(this.repo).then(response => { @@ -83,35 +89,37 @@ {detail: {message: GC_MESSAGE}, bubbles: true, composed: true})); } }); - }, + } _createNewChange() { this.$.createChangeOverlay.open(); - }, + } _handleCreateChange() { this.$.createNewChangeModal.handleCreateChange(); this._handleCloseCreateChange(); - }, + } _handleCloseCreateChange() { this.$.createChangeOverlay.close(); - }, + } _handleEditRepoConfig() { return this.$.restAPI.createChange(this.repo, CONFIG_BRANCH, EDIT_CONFIG_SUBJECT, undefined, false, true).then(change => { - const message = change ? - CREATE_CHANGE_SUCCEEDED_MESSAGE : - CREATE_CHANGE_FAILED_MESSAGE; - this.dispatchEvent(new CustomEvent( - 'show-alert', - {detail: {message}, bubbles: true, composed: true})); - if (!change) { return; } + const message = change ? + CREATE_CHANGE_SUCCEEDED_MESSAGE : + CREATE_CHANGE_FAILED_MESSAGE; + this.dispatchEvent(new CustomEvent( + 'show-alert', + {detail: {message}, bubbles: true, composed: true})); + if (!change) { return; } - Gerrit.Nav.navigateToRelativeUrl(Gerrit.Nav.getEditUrlForDiff( - change, CONFIG_PATH, INITIAL_PATCHSET)); - }); - }, - }); + Gerrit.Nav.navigateToRelativeUrl(Gerrit.Nav.getEditUrlForDiff( + change, CONFIG_PATH, INITIAL_PATCHSET)); + }); + } + } + + customElements.define(GrRepoCommands.is, GrRepoCommands); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html index 2976923..3c19175 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html +++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo-commands</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/page/page.js"></script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.html b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.html index 8af3a92..f74f705 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.html +++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.html
@@ -35,7 +35,9 @@ display: block; } </style> - <style include="gr-table-styles"></style> + <style include="gr-table-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <table id="list" class$="genericList [[_computeLoadingClass(_loading)]]"> <tr class="headerRow"> <th class="topHeader">Dashboard name</th>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js index 6e38566..f87f119 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js +++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
@@ -17,24 +17,29 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-repo-dashboards', + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrRepoDashboards extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-repo-dashboards'; } - properties: { - repo: { - type: String, - observer: '_repoChanged', - }, - _loading: { - type: Boolean, - value: true, - }, - _dashboards: Array, - }, - - behaviors: [ - Gerrit.FireBehavior, - ], + static get properties() { + return { + repo: { + type: String, + observer: '_repoChanged', + }, + _loading: { + type: Boolean, + value: true, + }, + _dashboards: Array, + }; + } _repoChanged(repo) { this._loading = true; @@ -49,7 +54,7 @@ // Group by ref and sort by id. const dashboards = res.concat.apply([], res).sort((a, b) => - a.id < b.id ? -1 : 1); + a.id < b.id ? -1 : 1); const dashboardsByRef = {}; dashboards.forEach(d => { if (!dashboardsByRef[d.ref]) { @@ -70,24 +75,26 @@ this._loading = false; Polymer.dom.flush(); }); - }, + } _getUrl(project, id) { if (!project || !id) { return ''; } return Gerrit.Nav.getUrlForRepoDashboard(project, id); - }, + } _computeLoadingClass(loading) { return loading ? 'loading' : ''; - }, + } _computeInheritedFrom(project, definingProject) { return project === definingProject ? '' : definingProject; - }, + } _computeIsDefault(isDefault) { return isDefault ? '✓' : ''; - }, - }); + } + } + + customElements.define(GrRepoDashboards.is, GrRepoDashboards); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html index 4f76983..06496de 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html +++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo-dashboards</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.html b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.html index 2f244f8..467cef0 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.html +++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.html
@@ -35,8 +35,12 @@ <dom-module id="gr-repo-detail-list"> <template> - <style include="gr-form-styles"></style> - <style include="gr-table-styles"></style> + <style include="gr-form-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="gr-table-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="shared-styles"> .tags td.name { min-width: 25em; @@ -81,7 +85,9 @@ display: none; } </style> - <style include="gr-table-styles"></style> + <style include="gr-table-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <gr-list-view create-new="[[_loggedIn]]" filter="[[_filter]]"
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js index da8ef52..109aa10 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js +++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
@@ -24,75 +24,82 @@ const PGP_START = '-----BEGIN PGP SIGNATURE-----'; - Polymer({ - is: 'gr-repo-detail-list', + /** + * @appliesMixin Gerrit.ListViewMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.URLEncodingMixin + */ + class GrRepoDetailList extends Polymer.mixinBehaviors( [ + Gerrit.ListViewBehavior, + Gerrit.FireBehavior, + Gerrit.URLEncodingBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-repo-detail-list'; } - properties: { + static get properties() { + return { /** * URL params passed from the router. */ - params: { - type: Object, - observer: '_paramsChanged', - }, - /** - * The kind of detail we are displaying, possibilities are determined by - * the const DETAIL_TYPES. - */ - detailType: String, + params: { + type: Object, + observer: '_paramsChanged', + }, + /** + * The kind of detail we are displaying, possibilities are determined by + * the const DETAIL_TYPES. + */ + detailType: String, - _editing: { - type: Boolean, - value: false, - }, - _isOwner: { - type: Boolean, - value: false, - }, - _loggedIn: { - type: Boolean, - value: false, - }, - /** - * Offset of currently visible query results. - */ - _offset: Number, - _repo: Object, - _items: Array, - /** - * Because we request one more than the projectsPerPage, _shownProjects - * maybe one less than _projects. - */ - _shownItems: { - type: Array, - computed: 'computeShownItems(_items)', - }, - _itemsPerPage: { - type: Number, - value: 25, - }, - _loading: { - type: Boolean, - value: true, - }, - _filter: String, - _refName: String, - _hasNewItemName: Boolean, - _isEditing: Boolean, - _revisedRef: String, - }, - - behaviors: [ - Gerrit.ListViewBehavior, - Gerrit.FireBehavior, - Gerrit.URLEncodingBehavior, - ], + _editing: { + type: Boolean, + value: false, + }, + _isOwner: { + type: Boolean, + value: false, + }, + _loggedIn: { + type: Boolean, + value: false, + }, + /** + * Offset of currently visible query results. + */ + _offset: Number, + _repo: Object, + _items: Array, + /** + * Because we request one more than the projectsPerPage, _shownProjects + * maybe one less than _projects. + */ + _shownItems: { + type: Array, + computed: 'computeShownItems(_items)', + }, + _itemsPerPage: { + type: Number, + value: 25, + }, + _loading: { + type: Boolean, + value: true, + }, + _filter: String, + _refName: String, + _hasNewItemName: Boolean, + _isEditing: Boolean, + _revisedRef: String, + }; + } _determineIfOwner(repo) { return this.$.restAPI.getRepoAccess(repo) .then(access => - this._isOwner = access && !!access[repo].is_owner); - }, + this._isOwner = access && !!access[repo].is_owner); + } _paramsChanged(params) { if (!params || !params.repo) { return; } @@ -113,7 +120,7 @@ return this._getItems(this._filter, this._repo, this._itemsPerPage, this._offset, this.detailType); - }, + } _getItems(filter, repo, itemsPerPage, offset, detailType) { this._loading = true; @@ -125,36 +132,36 @@ if (detailType === DETAIL_TYPES.BRANCHES) { return this.$.restAPI.getRepoBranches( filter, repo, itemsPerPage, offset, errFn).then(items => { - if (!items) { return; } - this._items = items; - this._loading = false; - }); + if (!items) { return; } + this._items = items; + this._loading = false; + }); } else if (detailType === DETAIL_TYPES.TAGS) { return this.$.restAPI.getRepoTags( filter, repo, itemsPerPage, offset, errFn).then(items => { - if (!items) { return; } - this._items = items; - this._loading = false; - }); + if (!items) { return; } + this._items = items; + this._loading = false; + }); } - }, + } _getPath(repo) { return `/admin/repos/${this.encodeURL(repo, false)},` + `${this.detailType}`; - }, + } _computeWeblink(repo) { if (!repo.web_links) { return ''; } const webLinks = repo.web_links; return webLinks.length ? webLinks : null; - }, + } _computeMessage(message) { if (!message) { return; } // Strip PGP info. return message.split(PGP_START)[0]; - }, + } _stripRefs(item, detailType) { if (detailType === DETAIL_TYPES.BRANCHES) { @@ -162,33 +169,33 @@ } else if (detailType === DETAIL_TYPES.TAGS) { return item.replace('refs/tags/', ''); } - }, + } _getLoggedIn() { return this.$.restAPI.getLoggedIn(); - }, + } _computeEditingClass(isEditing) { return isEditing ? 'editing' : ''; - }, + } _computeCanEditClass(ref, detailType, isOwner) { return isOwner && this._stripRefs(ref, detailType) === 'HEAD' ? - 'canEdit' : ''; - }, + 'canEdit' : ''; + } _handleEditRevision(e) { this._revisedRef = e.model.get('item.revision'); this._isEditing = true; - }, + } _handleCancelRevision() { this._isEditing = false; - }, + } _handleSaveRevision(e) { this._setRepoHead(this._repo, this._revisedRef, e); - }, + } _setRepoHead(repo, ref, e) { return this.$.restAPI.setRepoHead(repo, ref).then(res => { @@ -202,7 +209,7 @@ this._offset, this.detailType); } }); - }, + } _computeItemName(detailType) { if (detailType === DETAIL_TYPES.BRANCHES) { @@ -210,7 +217,7 @@ } else if (detailType === DETAIL_TYPES.TAGS) { return 'Tag'; } - }, + } _handleDeleteItemConfirm() { this.$.overlay.close(); @@ -233,18 +240,18 @@ } }); } - }, + } _handleConfirmDialogCancel() { this.$.overlay.close(); - }, + } _handleDeleteItem(e) { const name = this._stripRefs(e.model.get('item.ref'), this.detailType); if (!name) { return; } this._refName = name; this.$.overlay.open(); - }, + } _computeHideDeleteClass(owner, canDelete) { if (canDelete || owner) { @@ -252,20 +259,20 @@ } return ''; - }, + } _handleCreateItem() { this.$.createNewModal.handleCreateItem(); this._handleCloseCreate(); - }, + } _handleCloseCreate() { this.$.createOverlay.close(); - }, + } _handleCreateClicked() { this.$.createOverlay.open(); - }, + } _hideIfBranch(type) { if (type === DETAIL_TYPES.BRANCHES) { @@ -273,10 +280,12 @@ } return ''; - }, + } _computeHideTagger(tagger) { return tagger ? '' : 'hide'; - }, - }); + } + } + + customElements.define(GrRepoDetailList.is, GrRepoDetailList); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html index d8d4f7c..2b175c7 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html +++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo-detail-list</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/page/page.js"></script> @@ -155,9 +155,9 @@ const cancelBtn = Polymer.dom(element.root).querySelector('.cancelBtn'); const editBtn = Polymer.dom(element.root).querySelector('.editBtn'); const revisionNoEditing = Polymer.dom(element.root) - .querySelector('.revisionNoEditing'); + .querySelector('.revisionNoEditing'); const revisionWithEditing = Polymer.dom(element.root) - .querySelector('.revisionWithEditing'); + .querySelector('.revisionWithEditing'); sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true)); sandbox.stub(element.$.restAPI, 'getRepoAccess').returns(
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.html b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.html index 5e82c1e..08fd45c 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.html +++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.html
@@ -27,8 +27,12 @@ <dom-module id="gr-repo-list"> <template> - <style include="shared-styles"></style> - <style include="gr-table-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="gr-table-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style> .genericList tr td:last-of-type { text-align: left;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js index e0b054b..3e706b5 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js +++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
@@ -17,67 +17,73 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-repo-list', + /** + * @appliesMixin Gerrit.ListViewMixin + */ + class GrRepoList extends Polymer.mixinBehaviors( [ + Gerrit.ListViewBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-repo-list'; } - properties: { + static get properties() { + return { /** * URL params passed from the router. */ - params: { - type: Object, - observer: '_paramsChanged', - }, + params: { + type: Object, + observer: '_paramsChanged', + }, - /** - * Offset of currently visible query results. - */ - _offset: Number, - _path: { - type: String, - readOnly: true, - value: '/admin/repos', - }, - _hasNewRepoName: Boolean, - _createNewCapability: { - type: Boolean, - value: false, - }, - _repos: Array, + /** + * Offset of currently visible query results. + */ + _offset: Number, + _path: { + type: String, + readOnly: true, + value: '/admin/repos', + }, + _hasNewRepoName: Boolean, + _createNewCapability: { + type: Boolean, + value: false, + }, + _repos: Array, - /** - * Because we request one more than the projectsPerPage, _shownProjects - * maybe one less than _projects. - * */ - _shownRepos: { - type: Array, - computed: 'computeShownItems(_repos)', - }, + /** + * Because we request one more than the projectsPerPage, _shownProjects + * maybe one less than _projects. + * */ + _shownRepos: { + type: Array, + computed: 'computeShownItems(_repos)', + }, - _reposPerPage: { - type: Number, - value: 25, - }, + _reposPerPage: { + type: Number, + value: 25, + }, - _loading: { - type: Boolean, - value: true, - }, - _filter: { - type: String, - value: '', - }, - }, - - behaviors: [ - Gerrit.ListViewBehavior, - ], + _loading: { + type: Boolean, + value: true, + }, + _filter: { + type: String, + value: '', + }, + }; + } attached() { + super.attached(); this._getCreateRepoCapability(); this.fire('title-change', {title: 'Repos'}); this._maybeOpenCreateOverlay(this.params); - }, + } _paramsChanged(params) { this._loading = true; @@ -86,25 +92,26 @@ return this._getRepos(this._filter, this._reposPerPage, this._offset); - }, + } /** * Opens the create overlay if the route has a hash 'create' + * * @param {!Object} params */ _maybeOpenCreateOverlay(params) { if (params && params.openCreateModal) { this.$.createOverlay.open(); } - }, + } _computeRepoUrl(name) { return this.getUrl(this._path + '/', name); - }, + } _computeChangesLink(name) { return Gerrit.Nav.getUrlForProjectChanges(name); - }, + } _getCreateRepoCapability() { return this.$.restAPI.getAccount().then(account => { @@ -116,7 +123,7 @@ } }); }); - }, + } _getRepos(filter, reposPerPage, offset) { this._repos = []; @@ -127,36 +134,38 @@ this._repos = repos; this._loading = false; }); - }, + } _refreshReposList() { this.$.restAPI.invalidateReposCache(); return this._getRepos(this._filter, this._reposPerPage, this._offset); - }, + } _handleCreateRepo() { this.$.createNewModal.handleCreateRepo().then(() => { this._refreshReposList(); }); - }, + } _handleCloseCreate() { this.$.createOverlay.close(); - }, + } _handleCreateClicked() { this.$.createOverlay.open(); - }, + } _readOnly(item) { return item.state === 'READ_ONLY' ? 'Y' : ''; - }, + } _computeWeblink(repo) { if (!repo.web_links) { return ''; } const webLinks = repo.web_links; return webLinks.length ? webLinks : null; - }, - }); + } + } + + customElements.define(GrRepoList.is, GrRepoList); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html index c77592c..9569e56 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html +++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo-list</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/page/page.js"></script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.html b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.html index d2093e4..ef5b755 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.html +++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.html
@@ -30,8 +30,12 @@ <dom-module id="gr-repo-plugin-config"> <template> - <style include="shared-styles"></style> - <style include="gr-form-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="gr-form-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="gr-subpage-styles"> .inherited { color: var(--deemphasized-text-color);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js index 883a4e1..f11e5ed 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js +++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js
@@ -17,28 +17,32 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-repo-plugin-config', - + /** + * @appliesMixin Gerrit.RepoPluginConfigMixin + */ + class GrRepoPluginConfig extends Polymer.mixinBehaviors( [ + Gerrit.RepoPluginConfig, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-repo-plugin-config'; } /** * Fired when the plugin config changes. * * @event plugin-config-changed */ - properties: { + static get properties() { + return { /** @type {?} */ - pluginData: Object, - /** @type {Array} */ - _pluginConfigOptions: { - type: Array, - computed: '_computePluginConfigOptions(pluginData.*)', - }, - }, - - behaviors: [ - Gerrit.RepoPluginConfig, - ], + pluginData: Object, + /** @type {Array} */ + _pluginConfigOptions: { + type: Array, + computed: '_computePluginConfigOptions(pluginData.*)', + }, + }; + } _computePluginConfigOptions(dataRecord) { if (!dataRecord || !dataRecord.base || !dataRecord.base.config) { @@ -46,34 +50,37 @@ } const {config} = dataRecord.base; return Object.keys(config).map(_key => ({_key, info: config[_key]})); - }, + } _isArray(type) { return type === this.ENTRY_TYPES.ARRAY; - }, + } _isBoolean(type) { return type === this.ENTRY_TYPES.BOOLEAN; - }, + } _isList(type) { return type === this.ENTRY_TYPES.LIST; - }, + } _isString(type) { // Treat numbers like strings for simplicity. return type === this.ENTRY_TYPES.STRING || type === this.ENTRY_TYPES.INT || type === this.ENTRY_TYPES.LONG; - }, + } _computeDisabled(editable) { return editable === 'false'; - }, + } - _computeChecked(value) { + /** + * @param {?boolean} value - fallback to false if undefined + */ + _computeChecked(value = false) { return JSON.parse(value); - }, + } _handleStringChange(e) { const el = Polymer.dom(e).localTarget; @@ -81,7 +88,7 @@ const configChangeInfo = this._buildConfigChangeInfo(el.value, _key); this._handleChange(configChangeInfo); - }, + } _handleListChange(e) { const el = Polymer.dom(e).localTarget; @@ -89,7 +96,7 @@ const configChangeInfo = this._buildConfigChangeInfo(el.value, _key); this._handleChange(configChangeInfo); - }, + } _handleBooleanChange(e) { const el = Polymer.dom(e).localTarget; @@ -97,7 +104,7 @@ const configChangeInfo = this._buildConfigChangeInfo(JSON.stringify(el.checked), _key); this._handleChange(configChangeInfo); - }, + } _buildConfigChangeInfo(value, _key) { const info = this.pluginData.config[_key]; @@ -107,11 +114,11 @@ info, notifyPath: `${_key}.value`, }; - }, + } _handleArrayChange({detail}) { this._handleChange(detail); - }, + } _handleChange({_key, info, notifyPath}) { const {name, config} = this.pluginData; @@ -125,6 +132,8 @@ this.dispatchEvent(new CustomEvent( this.PLUGIN_CONFIG_CHANGED, {detail, bubbles: true, composed: true})); - }, - }); + } + } + + customElements.define(GrRepoPluginConfig.is, GrRepoPluginConfig); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html index 07da7c7..ff0e522 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html +++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo-plugin-config</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -54,7 +54,7 @@ {base: {config: {}}}), []); assert.deepEqual(element._computePluginConfigOptions( {base: {config: {testKey: 'testInfo'}}}), - [{_key: 'testKey', info: 'testInfo'}]); + [{_key: 'testKey', info: 'testInfo'}]); }); test('_computeDisabled', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html index 5de77b9..5e37261 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html +++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
@@ -32,7 +32,9 @@ <dom-module id="gr-repo"> <template> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="gr-subpage-styles"> h2.edited:after { color: var(--deemphasized-text-color); @@ -55,9 +57,13 @@ display: block; } </style> - <style include="gr-form-styles"></style> + <style include="gr-form-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <main class="gr-form-styles read-only"> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <div class="info"> <h1 id="Title" class$="name"> [[repo]]
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js index 918efba..5d24442 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js +++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
@@ -51,76 +51,84 @@ }, }; - Polymer({ - is: 'gr-repo', + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrRepo extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-repo'; } - properties: { - params: Object, - repo: String, + static get properties() { + return { + params: Object, + repo: String, - _configChanged: { - type: Boolean, - value: false, - }, - _loading: { - type: Boolean, - value: true, - }, - _loggedIn: { - type: Boolean, - value: false, - observer: '_loggedInChanged', - }, - /** @type {?} */ - _repoConfig: Object, - /** @type {?} */ - _pluginData: { - type: Array, - computed: '_computePluginData(_repoConfig.plugin_config.*)', - }, - _readOnly: { - type: Boolean, - value: true, - }, - _states: { - type: Array, - value() { - return Object.values(STATES); + _configChanged: { + type: Boolean, + value: false, }, - }, - _submitTypes: { - type: Array, - value() { - return Object.values(SUBMIT_TYPES); + _loading: { + type: Boolean, + value: true, }, - }, - _schemes: { - type: Array, - value() { return []; }, - computed: '_computeSchemes(_schemesObj)', - observer: '_schemesChanged', - }, - _selectedCommand: { - type: String, - value: 'Clone', - }, - _selectedScheme: String, - _schemesObj: Object, - }, + _loggedIn: { + type: Boolean, + value: false, + observer: '_loggedInChanged', + }, + /** @type {?} */ + _repoConfig: Object, + /** @type {?} */ + _pluginData: { + type: Array, + computed: '_computePluginData(_repoConfig.plugin_config.*)', + }, + _readOnly: { + type: Boolean, + value: true, + }, + _states: { + type: Array, + value() { + return Object.values(STATES); + }, + }, + _submitTypes: { + type: Array, + value() { + return Object.values(SUBMIT_TYPES); + }, + }, + _schemes: { + type: Array, + value() { return []; }, + computed: '_computeSchemes(_schemesObj)', + observer: '_schemesChanged', + }, + _selectedCommand: { + type: String, + value: 'Clone', + }, + _selectedScheme: String, + _schemesObj: Object, + }; + } - behaviors: [ - Gerrit.FireBehavior, - ], - - observers: [ - '_handleConfigChanged(_repoConfig.*)', - ], + static get observers() { + return [ + '_handleConfigChanged(_repoConfig.*)', + ]; + } attached() { + super.attached(); this._loadRepo(); this.fire('title-change', {title: this.repo}); - }, + } _computePluginData(configRecord) { if (!configRecord || @@ -129,7 +137,7 @@ const pluginConfig = configRecord.base; return Object.keys(pluginConfig) .map(name => ({name, config: pluginConfig[name]})); - }, + } _loadRepo() { if (!this.repo) { return Promise.resolve(); } @@ -179,15 +187,15 @@ })); return Promise.all(promises); - }, + } _computeLoadingClass(loading) { return loading ? 'loading' : ''; - }, + } _computeHideClass(arr) { return !arr || !arr.length ? 'hide' : ''; - }, + } _loggedInChanged(_loggedIn) { if (!_loggedIn) { return; } @@ -197,7 +205,7 @@ this._selectedScheme = prefs.download_scheme.toLowerCase(); } }); - }, + } _formatBooleanSelect(item) { if (!item) { return; } @@ -218,7 +226,7 @@ value: 'FALSE', }, ]; - }, + } _formatSubmitTypeSelect(projectConfig) { if (!projectConfig) { return; } @@ -248,15 +256,15 @@ }, ...allValues, ]; - }, + } _isLoading() { return this._loading || this._loading === undefined; - }, + } _getLoggedIn() { return this.$.restAPI.getLoggedIn(); - }, + } _formatRepoConfigForSave(repoConfig) { const configInputObj = {}; @@ -278,38 +286,38 @@ } } return configInputObj; - }, + } _handleSaveRepoConfig() { return this.$.restAPI.saveRepoConfig(this.repo, this._formatRepoConfigForSave(this._repoConfig)).then(() => { - this._configChanged = false; - }); - }, + this._configChanged = false; + }); + } _handleConfigChanged() { if (this._isLoading()) { return; } this._configChanged = true; - }, + } _computeButtonDisabled(readOnly, configChanged) { return readOnly || !configChanged; - }, + } _computeHeaderClass(configChanged) { return configChanged ? 'edited' : ''; - }, + } _computeSchemes(schemesObj) { return Object.keys(schemesObj); - }, + } _schemesChanged(schemes) { if (schemes.length === 0) { return; } if (!schemes.includes(this._selectedScheme)) { this._selectedScheme = schemes.sort()[0]; } - }, + } _computeCommands(repo, schemesObj, _selectedScheme) { if (!schemesObj || !repo || !_selectedScheme) { @@ -327,23 +335,25 @@ command: commandObj[title] .replace(/\$\{project\}/gi, encodeURI(repo)) .replace(/\$\{project-base-name\}/gi, - encodeURI(repo.substring(repo.lastIndexOf('/') + 1))), + encodeURI(repo.substring(repo.lastIndexOf('/') + 1))), }); } return commands; - }, + } _computeRepositoriesClass(config) { return config ? 'showConfig': ''; - }, + } _computeChangesUrl(name) { return Gerrit.Nav.getUrlForProjectChanges(name); - }, + } _handlePluginConfigChanged({detail: {name, config, notifyPath}}) { this._repoConfig.plugin_config[name] = config; this.notifyPath('_repoConfig.plugin_config.' + notifyPath); - }, - }); + } + } + + customElements.define(GrRepo.is, GrRepo); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html index f22c5a5..102a45e 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html +++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -367,7 +367,7 @@ element.$.matchAuthoredDateWithCommitterDateSelect.bindValue = configInputObj.match_author_to_committer_date; const inputElement = Polymer.Element ? - element.$.maxGitObjSizeIronInput : element.$.maxGitObjSizeInput; + element.$.maxGitObjSizeIronInput : element.$.maxGitObjSizeInput; inputElement.bindValue = configInputObj.max_object_size_limit; element.$.contributorAgreementSelect.bindValue = configInputObj.use_contributor_agreements;
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js index df5a9f3..24e95b6 100644 --- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js +++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
@@ -25,6 +25,7 @@ /** * Fired when a rule that was previously added was removed. + * * @event added-rule-removed */ @@ -63,62 +64,76 @@ }, ]; - Polymer({ - is: 'gr-rule-editor', + /** + * @appliesMixin Gerrit.AccessMixin + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.URLEncodingMixin + */ + class GrRuleEditor extends Polymer.mixinBehaviors( [ + Gerrit.AccessBehavior, + Gerrit.BaseUrlBehavior, + /** + * Unused in this element, but called by other elements in tests + * e.g gr-permission_test. + */ + Gerrit.FireBehavior, + Gerrit.URLEncodingBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-rule-editor'; } - properties: { - hasRange: Boolean, - /** @type {?} */ - label: Object, - editing: { - type: Boolean, - value: false, - observer: '_handleEditingChanged', - }, - groupId: String, - groupName: String, - permission: String, - /** @type {?} */ - rule: { - type: Object, - notify: true, - }, - section: String, + static get properties() { + return { + hasRange: Boolean, + /** @type {?} */ + label: Object, + editing: { + type: Boolean, + value: false, + observer: '_handleEditingChanged', + }, + groupId: String, + groupName: String, + permission: String, + /** @type {?} */ + rule: { + type: Object, + notify: true, + }, + section: String, - _deleted: { - type: Boolean, - value: false, - }, - _originalRuleValues: Object, - }, + _deleted: { + type: Boolean, + value: false, + }, + _originalRuleValues: Object, + }; + } - behaviors: [ - Gerrit.AccessBehavior, - Gerrit.BaseUrlBehavior, - /** - * Unused in this element, but called by other elements in tests - * e.g gr-permission_test. - */ - Gerrit.FireBehavior, - Gerrit.URLEncodingBehavior, - ], + static get observers() { + return [ + '_handleValueChange(rule.value.*)', + ]; + } - observers: [ - '_handleValueChange(rule.value.*)', - ], - - listeners: { - 'access-saved': '_handleAccessSaved', - }, + created() { + super.created(); + this.addEventListener('access-saved', + () => this._handleAccessSaved()); + } ready() { + super.ready(); // Called on ready rather than the observer because when new rules are // added, the observer is triggered prior to being ready. if (!this.rule) { return; } // Check needed for test purposes. this._setupValues(this.rule); - }, + } attached() { + super.attached(); if (!this.rule) { return; } // Check needed for test purposes. if (!this._originalRuleValues) { // Observer _handleValueChange is called after the ready() @@ -126,13 +141,13 @@ // avoid set .modified flag to true this._setOriginalRuleValues(this.rule.value); } - }, + } _setupValues(rule) { if (!rule.value) { this._setDefaultRuleValues(); } - }, + } _computeForce(permission, action) { if (this.permissionValues.push.id === permission && @@ -141,21 +156,21 @@ } return this.permissionValues.editTopicName.id === permission; - }, + } _computeForceClass(permission, action) { return this._computeForce(permission, action) ? 'force' : ''; - }, + } _computeGroupPath(group) { return `${this.getBaseUrl()}/admin/groups/${this.encodeURL(group, true)}`; - }, + } _handleAccessSaved() { // Set a new 'original' value to keep track of after the value has been // saved. this._setOriginalRuleValues(this.rule.value); - }, + } _handleEditingChanged(editing, editingOld) { // Ignore when editing gets set initially. @@ -164,7 +179,7 @@ if (!editing) { this._handleUndoChange(); } - }, + } _computeSectionClass(editing, deleted) { const classList = []; @@ -175,7 +190,7 @@ classList.push('deleted'); } return classList.join(' '); - }, + } _computeForceOptions(permission, action) { if (permission === this.permissionValues.push.id) { @@ -190,7 +205,7 @@ return FORCE_EDIT_OPTIONS; } return []; - }, + } _getDefaultRuleValues(permission, label) { const ruleAction = Action.ALLOW; @@ -207,19 +222,19 @@ } value.action = DROPDOWN_OPTIONS[0]; return value; - }, + } _setDefaultRuleValues() { this.set('rule.value', this._getDefaultRuleValues(this.permission, this.label)); - }, + } _computeOptions(permission) { if (permission === 'priority') { return PRIORITY_OPTIONS; } return DROPDOWN_OPTIONS; - }, + } _handleRemoveRule() { if (this.rule.value.added) { @@ -230,12 +245,12 @@ this.rule.value.deleted = true; this.dispatchEvent( new CustomEvent('access-modified', {bubbles: true, composed: true})); - }, + } _handleUndoRemove() { this._deleted = false; delete this.rule.value.deleted; - }, + } _handleUndoChange() { // gr-permission will take care of removing rules that were added but @@ -245,7 +260,7 @@ this._deleted = false; delete this.rule.value.deleted; delete this.rule.value.modified; - }, + } _handleValueChange() { if (!this._originalRuleValues) { return; } @@ -253,10 +268,12 @@ // Allows overall access page to know a change has been made. this.dispatchEvent( new CustomEvent('access-modified', {bubbles: true, composed: true})); - }, + } _setOriginalRuleValues(value) { this._originalRuleValues = Object.assign({}, value); - }, - }); + } + } + + customElements.define(GrRuleEditor.is, GrRuleEditor); })();
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html index 4ea3817..dbf5bb0 100644 --- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html +++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-rule-editor</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/page/page.js"></script> @@ -125,7 +125,7 @@ let permission = 'priority'; let label; assert.deepEqual(element._getDefaultRuleValues(permission, label), - {action: 'BATCH'}); + {action: 'BATCH'}); permission = 'label-Code-Review'; label = {values: [ {value: -2, text: 'This shall not be merged'}, @@ -139,7 +139,7 @@ permission = 'push'; label = undefined; assert.deepEqual(element._getDefaultRuleValues(permission, label), - {action: 'ALLOW', force: false}); + {action: 'ALLOW', force: false}); permission = 'submit'; assert.deepEqual(element._getDefaultRuleValues(permission, label), {action: 'ALLOW'});
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html index a6c86bb..f1338fb 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
@@ -124,7 +124,9 @@ } } </style> - <style include="gr-change-list-styles"></style> + <style include="gr-change-list-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <td class="cell leftPadding"></td> <td class="cell star" hidden$="[[!showStar]]" hidden> <gr-change-star change="{{change}}"></gr-change-star>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js index 8eb39891..dedf887 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -24,57 +24,67 @@ LARGE: 1000, }; - Polymer({ - is: 'gr-change-list-item', + /** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.ChangeTableMixin + * @appliesMixin Gerrit.PathListMixin + * @appliesMixin Gerrit.RESTClientMixin + * @appliesMixin Gerrit.URLEncodingMixin + */ + class GrChangeListItem extends Polymer.mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.ChangeTableBehavior, + Gerrit.PathListBehavior, + Gerrit.RESTClientBehavior, + Gerrit.URLEncodingBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-change-list-item'; } - properties: { - visibleChangeTableColumns: Array, - labelNames: { - type: Array, - }, + static get properties() { + return { + visibleChangeTableColumns: Array, + labelNames: { + type: Array, + }, - /** @type {?} */ - change: Object, - changeURL: { - type: String, - computed: '_computeChangeURL(change)', - }, - statuses: { - type: Array, - computed: 'changeStatuses(change)', - }, - showStar: { - type: Boolean, - value: false, - }, - showNumber: Boolean, - _changeSize: { - type: String, - computed: '_computeChangeSize(change)', - }, - _dynamicCellEndpoints: { - type: Array, - }, - }, - - behaviors: [ - Gerrit.BaseUrlBehavior, - Gerrit.ChangeTableBehavior, - Gerrit.PathListBehavior, - Gerrit.RESTClientBehavior, - Gerrit.URLEncodingBehavior, - ], + /** @type {?} */ + change: Object, + changeURL: { + type: String, + computed: '_computeChangeURL(change)', + }, + statuses: { + type: Array, + computed: 'changeStatuses(change)', + }, + showStar: { + type: Boolean, + value: false, + }, + showNumber: Boolean, + _changeSize: { + type: String, + computed: '_computeChangeSize(change)', + }, + _dynamicCellEndpoints: { + type: Array, + }, + }; + } attached() { + super.attached(); Gerrit.awaitPluginsLoaded().then(() => { this._dynamicCellEndpoints = Gerrit._endpoints.getDynamicEndpoints( 'change-list-item-cell'); }); - }, + } _computeChangeURL(change) { return Gerrit.Nav.getUrlForChange(change); - }, + } _computeLabelTitle(change, labelName) { const label = change.labels[labelName]; @@ -85,7 +95,7 @@ return labelName + '\nby ' + significantLabel.name; } return labelName; - }, + } _computeLabelClass(change, labelName) { const label = change.labels[labelName]; @@ -112,7 +122,7 @@ classes['u-gray-background'] = true; } return Object.keys(classes).sort().join(' '); - }, + } _computeLabelValue(change, labelName) { const label = change.labels[labelName]; @@ -130,22 +140,22 @@ return label.value; } return ''; - }, + } _computeRepoUrl(change) { return Gerrit.Nav.getUrlForProjectChanges(change.project, true, change.internalHost); - }, + } _computeRepoBranchURL(change) { return Gerrit.Nav.getUrlForBranch(change.branch, change.project, null, change.internalHost); - }, + } _computeTopicURL(change) { if (!change.topic) { return ''; } return Gerrit.Nav.getUrlForTopic(change.topic, change.internalHost); - }, + } /** * Computes the display string for the project column. If there is a host @@ -162,7 +172,7 @@ if (change.internalHost) { str += change.internalHost + '/'; } str += truncate ? this.truncatePath(change.project, 2) : change.project; return str; - }, + } _computeSizeTooltip(change) { if (change.insertions + change.deletions === 0 || @@ -171,7 +181,7 @@ } else { return `+${change.insertions}, -${change.deletions}`; } - }, + } /** * TShirt sizing is based on the following paper: @@ -193,7 +203,7 @@ } else { return 'XL'; } - }, + } toggleReviewed() { const newVal = !this.change.reviewed; @@ -203,6 +213,8 @@ composed: true, detail: {change: this.change, reviewed: newVal}, })); - }, - }); + } + } + + customElements.define(GrChangeListItem.is, GrChangeListItem); })();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html index 1cddbc5..3c402d1 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-list-item</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -57,33 +57,34 @@ 'cell label u-gray-background'); assert.equal(element._computeLabelClass( {labels: {}}, 'Verified'), 'cell label u-gray-background'); - assert.equal(element._computeLabelClass( - {labels: {Verified: {approved: true, value: 1}}}, 'Verified'), + assert.equal( + element._computeLabelClass( + {labels: {Verified: {approved: true, value: 1}}}, 'Verified'), 'cell label u-green u-monospace'); assert.equal(element._computeLabelClass( {labels: {Verified: {rejected: true, value: -1}}}, 'Verified'), - 'cell label u-monospace u-red'); + 'cell label u-monospace u-red'); assert.equal(element._computeLabelClass( {labels: {'Code-Review': {value: 1}}}, 'Code-Review'), - 'cell label u-green u-monospace'); + 'cell label u-green u-monospace'); assert.equal(element._computeLabelClass( {labels: {'Code-Review': {value: -1}}}, 'Code-Review'), - 'cell label u-monospace u-red'); + 'cell label u-monospace u-red'); assert.equal(element._computeLabelClass( {labels: {'Code-Review': {value: -1}}}, 'Verified'), - 'cell label u-gray-background'); + 'cell label u-gray-background'); assert.equal(element._computeLabelTitle({labels: {}}, 'Verified'), 'Label not applicable'); assert.equal(element._computeLabelTitle( {labels: {Verified: {approved: {name: 'Diffy'}}}}, 'Verified'), - 'Verified\nby Diffy'); + 'Verified\nby Diffy'); assert.equal(element._computeLabelTitle( {labels: {Verified: {approved: {name: 'Diffy'}}}}, 'Code-Review'), - 'Label not applicable'); + 'Label not applicable'); assert.equal(element._computeLabelTitle( {labels: {Verified: {rejected: {name: 'Diffy'}}}}, 'Verified'), - 'Verified\nby Diffy'); + 'Verified\nby Diffy'); assert.equal(element._computeLabelTitle( {labels: {'Code-Review': {disliked: {name: 'Diffy'}, value: -1}}}, 'Code-Review'), 'Code-Review\nby Diffy'); @@ -93,19 +94,19 @@ assert.equal(element._computeLabelTitle( {labels: {'Code-Review': {recommended: {name: 'Diffy'}, rejected: {name: 'Admin'}}}}, 'Code-Review'), - 'Code-Review\nby Admin'); + 'Code-Review\nby Admin'); assert.equal(element._computeLabelTitle( {labels: {'Code-Review': {approved: {name: 'Diffy'}, rejected: {name: 'Admin'}}}}, 'Code-Review'), - 'Code-Review\nby Admin'); + 'Code-Review\nby Admin'); assert.equal(element._computeLabelTitle( {labels: {'Code-Review': {recommended: {name: 'Diffy'}, disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'), - 'Code-Review\nby Admin'); + 'Code-Review\nby Admin'); assert.equal(element._computeLabelTitle( {labels: {'Code-Review': {approved: {name: 'Diffy'}, disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'), - 'Code-Review\nby Diffy'); + 'Code-Review\nby Diffy'); assert.equal(element._computeLabelValue({labels: {}}), ''); assert.equal(element._computeLabelValue({labels: {}}, 'Verified'), '');
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js index 02e9a04..3d2c108 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -29,111 +29,122 @@ const LIMIT_OPERATOR_PATTERN = /\blimit:(\d+)/i; - Polymer({ - is: 'gr-change-list-view', - + /** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.URLEncodingMixin + */ + class GrChangeListView extends Polymer.mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.FireBehavior, + Gerrit.URLEncodingBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-change-list-view'; } /** * Fired when the title of the page should change. * * @event title-change */ - behaviors: [ - Gerrit.BaseUrlBehavior, - Gerrit.FireBehavior, - Gerrit.URLEncodingBehavior, - ], - - properties: { + static get properties() { + return { /** * URL params passed from the router. */ - params: { - type: Object, - observer: '_paramsChanged', - }, + params: { + type: Object, + observer: '_paramsChanged', + }, - /** - * True when user is logged in. - */ - _loggedIn: { - type: Boolean, - computed: '_computeLoggedIn(account)', - }, + /** + * True when user is logged in. + */ + _loggedIn: { + type: Boolean, + computed: '_computeLoggedIn(account)', + }, - account: { - type: Object, - value: null, - }, + account: { + type: Object, + value: null, + }, - /** - * State persisted across restamps of the element. - * - * Need sub-property declaration since it is used in template before - * assignment. - * @type {{ selectedChangeIndex: (number|undefined) }} - * - */ - viewState: { - type: Object, - notify: true, - value() { return {}; }, - }, + /** + * State persisted across restamps of the element. + * + * Need sub-property declaration since it is used in template before + * assignment. + * + * @type {{ selectedChangeIndex: (number|undefined) }} + * + */ + viewState: { + type: Object, + notify: true, + value() { return {}; }, + }, - preferences: Object, + preferences: Object, - _changesPerPage: Number, + _changesPerPage: Number, - /** - * Currently active query. - */ - _query: { - type: String, - value: '', - }, + /** + * Currently active query. + */ + _query: { + type: String, + value: '', + }, - /** - * Offset of currently visible query results. - */ - _offset: Number, + /** + * Offset of currently visible query results. + */ + _offset: Number, - /** - * Change objects loaded from the server. - */ - _changes: { - type: Array, - observer: '_changesChanged', - }, + /** + * Change objects loaded from the server. + */ + _changes: { + type: Array, + observer: '_changesChanged', + }, - /** - * For showing a "loading..." string during ajax requests. - */ - _loading: { - type: Boolean, - value: true, - }, + /** + * For showing a "loading..." string during ajax requests. + */ + _loading: { + type: Boolean, + value: true, + }, - /** @type {?String} */ - _userId: { - type: String, - value: null, - }, + /** @type {?String} */ + _userId: { + type: String, + value: null, + }, - /** @type {?String} */ - _repo: { - type: String, - value: null, - }, - }, + /** @type {?String} */ + _repo: { + type: String, + value: null, + }, + }; + } - listeners: { - 'next-page': '_handleNextPage', - 'previous-page': '_handlePreviousPage', - }, + created() { + super.created(); + this.addEventListener('next-page', + () => this._handleNextPage()); + this.addEventListener('previous-page', + () => this._handlePreviousPage()); + } attached() { + super.attached(); this._loadPreferences(); - }, + } _paramsChanged(value) { if (value.view !== Gerrit.Nav.View.SEARCH) { return; } @@ -170,7 +181,7 @@ this._changes = changes; this._loading = false; }); - }, + } _loadPreferences() { return this.$.restAPI.getLoggedIn().then(loggedIn => { @@ -182,20 +193,20 @@ this.preferences = {}; } }); - }, + } _replaceCurrentLocation(url) { window.location.replace(url); - }, + } _getChanges() { return this.$.restAPI.getChanges(this._changesPerPage, this._query, this._offset); - }, + } _getPreferences() { return this.$.restAPI.getPreferences(); - }, + } _limitFor(query, defaultLimit) { const match = query.match(LIMIT_OPERATOR_PATTERN); @@ -203,7 +214,7 @@ return defaultLimit; } return parseInt(match[1], 10); - }, + } _computeNavLink(query, offset, direction, changesPerPage) { // Offset could be a string when passed from the router. @@ -211,32 +222,32 @@ const limit = this._limitFor(query, changesPerPage); const newOffset = Math.max(0, offset + (limit * direction)); return Gerrit.Nav.getUrlForSearchQuery(query, newOffset); - }, + } _computePrevArrowClass(offset) { return offset === 0 ? 'hide' : ''; - }, + } _computeNextArrowClass(changes) { const more = changes.length && changes[changes.length - 1]._more_changes; return more ? '' : 'hide'; - }, + } _computeNavClass(loading) { return loading || !this._changes || !this._changes.length ? 'hide' : ''; - }, + } _handleNextPage() { if (this.$.nextArrow.hidden) { return; } page.show(this._computeNavLink( this._query, this._offset, 1, this._changesPerPage)); - }, + } _handlePreviousPage() { if (this.$.prevArrow.hidden) { return; } page.show(this._computeNavLink( this._query, this._offset, -1, this._changesPerPage)); - }, + } _changesChanged(changes) { this._userId = null; @@ -255,28 +266,30 @@ if (REPO_QUERY_PATTERN.test(this._query)) { this._repo = changes[0].project; } - }, + } _computeHeaderClass(id) { return id ? '' : 'hide'; - }, + } _computePage(offset, changesPerPage) { return offset / changesPerPage + 1; - }, + } _computeLoggedIn(account) { return !!(account && Object.keys(account).length > 0); - }, + } _handleToggleStar(e) { this.$.restAPI.saveChangeStarred(e.detail.change._number, e.detail.starred); - }, + } _handleToggleReviewed(e) { this.$.restAPI.saveChangeReviewed(e.detail.change._number, e.detail.reviewed); - }, - }); + } + } + + customElements.define(GrChangeListView.is, GrChangeListView); })();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html index 2367aac..08a83af 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-list-view</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/page/page.js"></script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html index 699f07a..8fdb90c 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html +++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
@@ -31,7 +31,9 @@ <dom-module id="gr-change-list"> <template> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="gr-change-list-styles"> #changeList { border-collapse: collapse;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js index e32c773..d1136cc 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js +++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -22,9 +22,25 @@ const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--'; const MAX_SHORTCUT_CHARS = 5; - Polymer({ - is: 'gr-change-list', - + /** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.ChangeTableMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @appliesMixin Gerrit.RESTClientMixin + * @appliesMixin Gerrit.URLEncodingMixin + */ + class GrChangeList extends Polymer.mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.ChangeTableBehavior, + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + Gerrit.RESTClientBehavior, + Gerrit.URLEncodingBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-change-list'; } /** * Fired when next page key shortcut was pressed. * @@ -37,87 +53,74 @@ * @event previous-page */ - hostAttributes: { - tabindex: 0, - }, - - properties: { + static get properties() { + return { /** * The logged-in user's account, or an empty object if no user is logged * in. */ - account: { - type: Object, - value: null, - }, - /** - * An array of ChangeInfo objects to render. - * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info - */ - changes: { - type: Array, - observer: '_changesChanged', - }, - /** - * ChangeInfo objects grouped into arrays. The sections and changes - * properties should not be used together. - * - * @type {!Array<{ - * name: string, - * query: string, - * results: !Array<!Object> - * }>} - */ - sections: { - type: Array, - value() { return []; }, - }, - labelNames: { - type: Array, - computed: '_computeLabelNames(sections)', - }, - _dynamicHeaderEndpoints: { - type: Array, - }, - selectedIndex: { - type: Number, - notify: true, - }, - showNumber: Boolean, // No default value to prevent flickering. - showStar: { - type: Boolean, - value: false, - }, - showReviewedState: { - type: Boolean, - value: false, - }, - keyEventTarget: { - type: Object, - value() { return document.body; }, - }, - changeTableColumns: Array, - visibleChangeTableColumns: Array, - preferences: Object, - }, + account: { + type: Object, + value: null, + }, + /** + * An array of ChangeInfo objects to render. + * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info + */ + changes: { + type: Array, + observer: '_changesChanged', + }, + /** + * ChangeInfo objects grouped into arrays. The sections and changes + * properties should not be used together. + * + * @type {!Array<{ + * name: string, + * query: string, + * results: !Array<!Object> + * }>} + */ + sections: { + type: Array, + value() { return []; }, + }, + labelNames: { + type: Array, + computed: '_computeLabelNames(sections)', + }, + _dynamicHeaderEndpoints: { + type: Array, + }, + selectedIndex: { + type: Number, + notify: true, + }, + showNumber: Boolean, // No default value to prevent flickering. + showStar: { + type: Boolean, + value: false, + }, + showReviewedState: { + type: Boolean, + value: false, + }, + keyEventTarget: { + type: Object, + value() { return document.body; }, + }, + changeTableColumns: Array, + visibleChangeTableColumns: Array, + preferences: Object, + }; + } - behaviors: [ - Gerrit.BaseUrlBehavior, - Gerrit.ChangeTableBehavior, - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - Gerrit.RESTClientBehavior, - Gerrit.URLEncodingBehavior, - ], - - listeners: { - keydown: '_scopedKeydownHandler', - }, - - observers: [ - '_sectionsChanged(sections.*)', - '_computePreferences(account, preferences)', - ], + static get observers() { + return [ + '_sectionsChanged(sections.*)', + '_computePreferences(account, preferences)', + ]; + } keyboardShortcuts() { return { @@ -130,14 +133,26 @@ [this.Shortcut.TOGGLE_CHANGE_STAR]: '_toggleChangeStar', [this.Shortcut.REFRESH_CHANGE_LIST]: '_refreshChangeList', }; - }, + } + + created() { + super.created(); + this.addEventListener('keydown', + e => this._scopedKeydownHandler(e)); + } + + ready() { + super.ready(); + this._ensureAttribute('tabindex', 0); + } attached() { + super.attached(); Gerrit.awaitPluginsLoaded().then(() => { this._dynamicHeaderEndpoints = Gerrit._endpoints.getDynamicEndpoints( 'change-list-header'); }); - }, + } /** * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard @@ -151,11 +166,11 @@ // Enter. this._openChange(e); } - }, + } _lowerCase(column) { return column.toLowerCase(); - }, + } _computePreferences(account, preferences) { // Polymer 2: check for undefined @@ -169,19 +184,19 @@ this.showNumber = !!(preferences && preferences.legacycid_in_change_table); this.visibleChangeTableColumns = preferences.change_table.length > 0 ? - this.getVisibleColumns(preferences.change_table) : this.columnNames; + this.getVisibleColumns(preferences.change_table) : this.columnNames; } else { // Not logged in. this.showNumber = false; this.visibleChangeTableColumns = this.columnNames; } - }, + } _computeColspan(changeTableColumns, labelNames) { if (!changeTableColumns || !labelNames) return; return changeTableColumns.length + labelNames.length + NUMBER_FIXED_COLUMNS; - }, + } _computeLabelNames(sections) { if (!sections) { return []; } @@ -198,7 +213,7 @@ } } return labels.sort(); - }, + } _computeLabelShortcut(labelName) { if (labelName.startsWith(LABEL_PREFIX_INVALID_PROLOG)) { @@ -210,11 +225,11 @@ return a + i[0].toUpperCase(); }, '') .slice(0, MAX_SHORTCUT_CHARS); - }, + } _changesChanged(changes) { this.sections = changes ? [{results: changes}] : []; - }, + } _processQuery(query) { let tokens = query.split(' '); @@ -225,11 +240,11 @@ }); }); return tokens.join(' '); - }, + } _sectionHref(query) { return Gerrit.Nav.getUrlForSearchQuery(this._processQuery(query)); - }, + } /** * Maps an index local to a particular section to the absolute index @@ -245,19 +260,19 @@ idx += this.sections[i].results.length; } return idx + localIndex; - }, + } _computeItemSelected(sectionIndex, index, selectedIndex) { const idx = this._computeItemAbsoluteIndex(sectionIndex, index); return idx == selectedIndex; - }, + } _computeItemNeedsReview(account, change, showReviewedState) { return showReviewedState && !change.reviewed && !change.work_in_progress && this.changeIsOpen(change) && (!account || account._account_id != change.owner._account_id); - }, + } _computeItemHighlight(account, change) { // Do not show the assignee highlight if the change is not open. @@ -267,7 +282,7 @@ return false; } return account._account_id === change.assignee._account_id; - }, + } _nextChange(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -275,7 +290,7 @@ e.preventDefault(); this.$.cursor.next(); - }, + } _prevChange(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -283,7 +298,7 @@ e.preventDefault(); this.$.cursor.previous(); - }, + } _openChange(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -291,7 +306,7 @@ e.preventDefault(); Gerrit.Nav.navigateToChange(this._changeForIndex(this.selectedIndex)); - }, + } _nextPage(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -301,7 +316,7 @@ e.preventDefault(); this.fire('next-page'); - }, + } _prevPage(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -311,7 +326,7 @@ e.preventDefault(); this.fire('previous-page'); - }, + } _toggleChangeReviewed(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -319,7 +334,7 @@ e.preventDefault(); this._toggleReviewedForIndex(this.selectedIndex); - }, + } _toggleReviewedForIndex(index) { const changeEls = this._getListItems(); @@ -329,18 +344,18 @@ const changeEl = changeEls[index]; changeEl.toggleReviewed(); - }, + } _refreshChangeList(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } e.preventDefault(); this._reloadWindow(); - }, + } _reloadWindow() { window.location.reload(); - }, + } _toggleChangeStar(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -348,7 +363,7 @@ e.preventDefault(); this._toggleStarForIndex(this.selectedIndex); - }, + } _toggleStarForIndex(index) { const changeEls = this._getListItems(); @@ -358,7 +373,7 @@ const changeEl = changeEls[index]; changeEl.$$('gr-change-star').toggleStar(); - }, + } _changeForIndex(index) { const changeEls = this._getListItems(); @@ -366,12 +381,12 @@ return changeEls[index].change; } return null; - }, + } _getListItems() { return Array.from( Polymer.dom(this.root).querySelectorAll('gr-change-list-item')); - }, + } _sectionsChanged() { // Flush DOM operations so that the list item elements will be loaded. @@ -379,14 +394,16 @@ this.$.cursor.stops = this._getListItems(); this.$.cursor.moveToStart(); }); - }, + } _isOutgoing(section) { return !!section.isOutgoing; - }, + } _isEmpty(section) { return !section.results.length; - }, - }); + } + } + + customElements.define(GrChangeList.is, GrChangeList); })();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html index 87b1665..75fd167 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-list</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -113,7 +113,7 @@ test('computed fields', () => { assert.equal(element._computeLabelNames( - [{results: [{_number: 0, labels: {}}]}]).length, 0); + [{results: [{_number: 0, labels: {}}]}]).length, 0); assert.equal(element._computeLabelNames([ {results: [ {_number: 0, labels: {Verified: {approved: {}}}},
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js index 19e7a25..e6caacb 100644 --- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js +++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
@@ -17,8 +17,10 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-create-change-help', + class GrCreateChangeHelp extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-create-change-help'; } /** * Fired when the "Create change" button is tapped. @@ -30,6 +32,8 @@ e.preventDefault(); this.dispatchEvent( new CustomEvent('create-tap', {bubbles: true, composed: true})); - }, - }); + } + } + + customElements.define(GrCreateChangeHelp.is, GrCreateChangeHelp); })();
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html index c43d62a..f10fbf2 100644 --- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-create-change-help</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js index 5abb257..303128b 100644 --- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js +++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
@@ -23,37 +23,43 @@ PUSH_PREFIX: 'git push origin HEAD:refs/for/', }; - Polymer({ - is: 'gr-create-commands-dialog', + class GrCreateCommandsDialog extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-create-commands-dialog'; } - properties: { - branch: String, - _createNewCommitCommand: { - type: String, - readonly: true, - value: Commands.CREATE, - }, - _amendExistingCommitCommand: { - type: String, - readonly: true, - value: Commands.AMEND, - }, - _pushCommand: { - type: String, - computed: '_computePushCommand(branch)', - }, - }, + static get properties() { + return { + branch: String, + _createNewCommitCommand: { + type: String, + readonly: true, + value: Commands.CREATE, + }, + _amendExistingCommitCommand: { + type: String, + readonly: true, + value: Commands.AMEND, + }, + _pushCommand: { + type: String, + computed: '_computePushCommand(branch)', + }, + }; + } open() { this.$.commandsOverlay.open(); - }, + } _handleClose() { this.$.commandsOverlay.close(); - }, + } _computePushCommand(branch) { return Commands.PUSH_PREFIX + branch; - }, - }); + } + } + + customElements.define(GrCreateCommandsDialog.is, GrCreateCommandsDialog); })();
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html index 89ad573..2228b50 100644 --- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-create-commands-dialog</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js index c2bfbf5..cbc7bcd 100644 --- a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js +++ b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
@@ -23,28 +23,32 @@ * * @event confirm */ + class GrCreateDestinationDialog extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-create-destination-dialog'; } - Polymer({ - is: 'gr-create-destination-dialog', + static get properties() { + return { + _repo: String, + _branch: String, + _repoAndBranchSelected: { + type: Boolean, + value: false, + computed: '_computeRepoAndBranchSelected(_repo, _branch)', + }, + }; + } - properties: { - _repo: String, - _branch: String, - _repoAndBranchSelected: { - type: Boolean, - value: false, - computed: '_computeRepoAndBranchSelected(_repo, _branch)', - }, - }, open() { this._repo = ''; this._branch = ''; this.$.createOverlay.open(); - }, + } _handleClose() { this.$.createOverlay.close(); - }, + } _pickerConfirm(e) { this.$.createOverlay.close(); @@ -54,10 +58,13 @@ e.preventDefault(); e.stopPropagation(); this.dispatchEvent(new CustomEvent('confirm', {detail, bubbles: false})); - }, + } _computeRepoAndBranchSelected(repo, branch) { return !!(repo && branch); - }, - }); + } + } + + customElements.define(GrCreateDestinationDialog.is, + GrCreateDestinationDialog); })();
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js index aab0cba..0170b0b 100644 --- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js +++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -19,65 +19,72 @@ const PROJECT_PLACEHOLDER_PATTERN = /\$\{project\}/g; - Polymer({ - is: 'gr-dashboard-view', - + /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.RESTClientMixin + */ + class GrDashboardView extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.RESTClientBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-dashboard-view'; } /** * Fired when the title of the page should change. * * @event title-change */ - properties: { - account: { - type: Object, - value: null, - }, - preferences: Object, - /** @type {{ selectedChangeIndex: number }} */ - viewState: Object, - - /** @type {{ project: string, user: string }} */ - params: { - type: Object, - }, - - createChangeTap: { - type: Function, - value() { - return this._createChangeTap.bind(this); + static get properties() { + return { + account: { + type: Object, + value: null, }, - }, + preferences: Object, + /** @type {{ selectedChangeIndex: number }} */ + viewState: Object, - _results: Array, + /** @type {{ project: string, user: string }} */ + params: { + type: Object, + }, - /** - * For showing a "loading..." string during ajax requests. - */ - _loading: { - type: Boolean, - value: true, - }, + createChangeTap: { + type: Function, + value() { + return this._createChangeTap.bind(this); + }, + }, - _showDraftsBanner: { - type: Boolean, - value: false, - }, + _results: Array, - _showNewUserHelp: { - type: Boolean, - value: false, - }, - }, + /** + * For showing a "loading..." string during ajax requests. + */ + _loading: { + type: Boolean, + value: true, + }, - observers: [ - '_paramsChanged(params.*)', - ], + _showDraftsBanner: { + type: Boolean, + value: false, + }, - behaviors: [ - Gerrit.FireBehavior, - Gerrit.RESTClientBehavior, - ], + _showNewUserHelp: { + type: Boolean, + value: false, + }, + }; + } + + static get observers() { + return [ + '_paramsChanged(params.*)', + ]; + } get options() { return this.listChangesOptionsToHex( @@ -85,11 +92,12 @@ this.ListChangesOption.DETAILED_ACCOUNTS, this.ListChangesOption.REVIEWED ); - }, + } attached() { + super.attached(); this._loadPreferences(); - }, + } _loadPreferences() { return this.$.restAPI.getLoggedIn().then(loggedIn => { @@ -101,7 +109,7 @@ this.preferences = {}; } }); - }, + } _getProjectDashboard(project, dashboard) { const errFn = response => { @@ -109,33 +117,33 @@ }; return this.$.restAPI.getDashboard( project, dashboard, errFn).then(response => { - if (!response) { - return; - } + if (!response) { + return; + } + return { + title: response.title, + sections: response.sections.map(section => { + const suffix = response.foreach ? ' ' + response.foreach : ''; return { - title: response.title, - sections: response.sections.map(section => { - const suffix = response.foreach ? ' ' + response.foreach : ''; - return { - name: section.name, - query: (section.query + suffix).replace( - PROJECT_PLACEHOLDER_PATTERN, project), - }; - }), + name: section.name, + query: (section.query + suffix).replace( + PROJECT_PLACEHOLDER_PATTERN, project), }; - }); - }, + }), + }; + }); + } _computeTitle(user) { if (!user || user === 'self') { return 'My Reviews'; } return 'Dashboard for ' + user; - }, + } _isViewActive(params) { return params.view === Gerrit.Nav.View.DASHBOARD; - }, + } _paramsChanged(paramsChangeRecord) { const params = paramsChangeRecord.base; @@ -145,7 +153,7 @@ } return this._reload(); - }, + } /** * Reloads the element. @@ -156,11 +164,11 @@ this._loading = true; const {project, dashboard, title, user, sections} = this.params; const dashboardPromise = project ? - this._getProjectDashboard(project, dashboard) : - Promise.resolve(Gerrit.Nav.getUserDashboard( - user, - sections, - title || this._computeTitle(user))); + this._getProjectDashboard(project, dashboard) : + Promise.resolve(Gerrit.Nav.getUserDashboard( + user, + sections, + title || this._computeTitle(user))); const checkForNewUser = !project && user === 'self'; return dashboardPromise @@ -179,7 +187,7 @@ }); console.warn(err); }).then(() => { this._loading = false; }); - }, + } /** * Fetches the changes for each dashboard section and sets this._results @@ -194,8 +202,8 @@ const queries = res.sections .map(section => section.suffixForDashboard ? - section.query + ' ' + section.suffixForDashboard : - section.query); + section.query + ' ' + section.suffixForDashboard : + section.query); if (checkForNewUser) { queries.push('owner:self limit:1'); @@ -215,10 +223,10 @@ results, isOutgoing: res.sections[i].isOutgoing, })).filter((section, i) => i < res.sections.length && ( - !res.sections[i].hideIfEmpty || + !res.sections[i].hideIfEmpty || section.results.length)); }); - }, + } _computeSectionCountLabel(changes) { if (!changes || !changes.length || changes.length == 0) { @@ -228,25 +236,25 @@ const numChanges = changes.length; const andMore = more ? ' and more' : ''; return `(${numChanges}${andMore})`; - }, + } _computeUserHeaderClass(params) { - if (!params || !!params.project || !params.user - || params.user === 'self') { + if (!params || !!params.project || !params.user || + params.user === 'self') { return 'hide'; } return ''; - }, + } _handleToggleStar(e) { this.$.restAPI.saveChangeStarred(e.detail.change._number, e.detail.starred); - }, + } _handleToggleReviewed(e) { this.$.restAPI.saveChangeReviewed(e.detail.change._number, e.detail.reviewed); - }, + } /** * Banner is shown if a user is on their own dashboard and they have draft @@ -265,15 +273,15 @@ if (!closedChanges.length) { return; } this._showDraftsBanner = true; - }, + } _computeBannerClass(show) { return show ? '' : 'hide'; - }, + } _handleOpenDeleteDialog() { this.$.confirmDeleteOverlay.open(); - }, + } _handleConfirmDelete() { this.$.confirmDeleteDialog.disabled = true; @@ -281,23 +289,25 @@ this._closeConfirmDeleteOverlay(); this._reload(); }); - }, + } _closeConfirmDeleteOverlay() { this.$.confirmDeleteOverlay.close(); - }, + } _computeDraftsLink() { return Gerrit.Nav.getUrlForSearchQuery('has:draft -is:open'); - }, + } _createChangeTap(e) { this.$.destinationDialog.open(); - }, + } _handleDestinationConfirm(e) { this.$.commandsDialog.branch = e.detail.branch; this.$.commandsDialog.open(); - }, - }); + } + } + + customElements.define(GrDashboardView.is, GrDashboardView); })();
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html index 41d4192..0d15d13 100644 --- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-dashboard-view</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js index acc4295..96dcb98 100644 --- a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js +++ b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
@@ -17,14 +17,20 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-embed-dashboard', + class GrEmbedDashboard extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-embed-dashboard'; } - properties: { - account: Object, - sections: Array, - preferences: Object, - showNewUserHelp: Boolean, - }, - }); + static get properties() { + return { + account: Object, + sections: Array, + preferences: Object, + showNewUserHelp: Boolean, + }; + } + } + + customElements.define(GrEmbedDashboard.is, GrEmbedDashboard); })();
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.html b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.html index 0b4459c..5d4b8a3 100644 --- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.html +++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.html
@@ -24,8 +24,12 @@ <dom-module id="gr-repo-header"> <template> - <style include="shared-styles"></style> - <style include="dashboard-header-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="dashboard-header-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <div class="info"> <h1 class$="name"> [[repo]]
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js index 7ae4dab..c603c45 100644 --- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js +++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
@@ -17,18 +17,22 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-repo-header', + class GrRepoHeader extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-repo-header'; } - properties: { + static get properties() { + return { /** @type {?String} */ - repo: { - type: String, - observer: '_repoChanged', - }, - /** @type {String|null} */ - _repoUrl: String, - }, + repo: { + type: String, + observer: '_repoChanged', + }, + /** @type {String|null} */ + _repoUrl: String, + }; + } _repoChanged(repoName) { if (!repoName) { @@ -36,6 +40,8 @@ return; } this._repoUrl = Gerrit.Nav.getUrlForRepo(repoName); - }, - }); + } + } + + customElements.define(GrRepoHeader.is, GrRepoHeader); })();
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html index 266818e..85eb509 100644 --- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo-header</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html index fed1c12..8175849 100644 --- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html +++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
@@ -27,7 +27,9 @@ <dom-module id="gr-user-header"> <template> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="dashboard-header-styles"> .name { display: inline-block;
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js index 6942705..d9e9107 100644 --- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js +++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
@@ -17,40 +17,44 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-user-header', + class GrUserHeader extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-user-header'; } - properties: { + static get properties() { + return { /** @type {?String} */ - userId: { - type: String, - observer: '_accountChanged', - }, + userId: { + type: String, + observer: '_accountChanged', + }, - showDashboardLink: { - type: Boolean, - value: false, - }, + showDashboardLink: { + type: Boolean, + value: false, + }, - loggedIn: { - type: Boolean, - value: false, - }, + loggedIn: { + type: Boolean, + value: false, + }, - /** - * @type {?{name: ?, email: ?, registered_on: ?}} - */ - _accountDetails: { - type: Object, - value: null, - }, + /** + * @type {?{name: ?, email: ?, registered_on: ?}} + */ + _accountDetails: { + type: Object, + value: null, + }, - /** @type {?String} */ - _status: { - type: String, - value: null, - }, - }, + /** @type {?String} */ + _status: { + type: String, + value: null, + }, + }; + } _accountChanged(userId) { if (!userId) { @@ -65,19 +69,19 @@ this.$.restAPI.getAccountStatus(userId).then(status => { this._status = status; }); - }, + } _computeDisplayClass(status) { return status ? ' ' : 'hide'; - }, + } _computeDetail(accountDetails, name) { return accountDetails ? accountDetails[name] : ''; - }, + } _computeStatusClass(accountDetails) { return this._computeDetail(accountDetails, 'status') ? '' : 'hide'; - }, + } _computeDashboardUrl(accountDetails) { if (!accountDetails) { return null; } @@ -85,11 +89,13 @@ const email = accountDetails.email; if (!id && !email ) { return null; } return Gerrit.Nav.getUrlForUserDashboard(id ? id : email); - }, + } _computeDashboardLinkClass(showDashboardLink, loggedIn) { return showDashboardLink && loggedIn ? - 'dashboardLink' : 'dashboardLink hide'; - }, - }); + 'dashboardLink' : 'dashboardLink hide'; + } + } + + customElements.define(GrUserHeader.is, GrUserHeader); })();
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html index e837a5b..9e04ecd 100644 --- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-user-header</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html index 000756f..f4bd6a6 100644 --- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html +++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -36,6 +36,7 @@ <link rel="import" href="../gr-confirm-move-dialog/gr-confirm-move-dialog.html"> <link rel="import" href="../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html"> <link rel="import" href="../gr-confirm-revert-dialog/gr-confirm-revert-dialog.html"> +<link rel="import" href="../gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.html"> <link rel="import" href="../gr-confirm-submit-dialog/gr-confirm-submit-dialog.html"> <link rel="import" href="../../../styles/shared-styles.html"> @@ -192,12 +193,8 @@ hidden></gr-confirm-cherrypick-dialog> <gr-confirm-cherrypick-conflict-dialog id="confirmCherrypickConflict" class="confirmDialog" - change-status="[[changeStatus]]" - commit-message="[[commitMessage]]" - commit-num="[[commitNum]]" on-confirm="_handleCherrypickConflictConfirm" on-cancel="_handleConfirmDialogCancel" - project="[[change.project]]" hidden></gr-confirm-cherrypick-conflict-dialog> <gr-confirm-move-dialog id="confirmMove" class="confirmDialog" @@ -210,6 +207,12 @@ on-confirm="_handleRevertDialogConfirm" on-cancel="_handleConfirmDialogCancel" hidden></gr-confirm-revert-dialog> + <gr-confirm-revert-submission-dialog id="confirmRevertSubmissionDialog" + class="confirmDialog" + commit-message="[[commitMessage]]" + on-confirm="_handleRevertSubmissionDialogConfirm" + on-cancel="_handleConfirmDialogCancel" + hidden></gr-confirm-revert-submission-dialog> <gr-confirm-abandon-dialog id="confirmAbandonDialog" class="confirmDialog" on-confirm="_handleAbandonDialogConfirm"
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js index fe70239..53a9568 100644 --- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js +++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -64,6 +64,7 @@ REBASE_EDIT: 'rebaseEdit', RESTORE: 'restore', REVERT: 'revert', + REVERT_SUBMISSION: 'revert_submission', REVIEWED: 'reviewed', STOP_EDIT: 'stopEdit', UNIGNORE: 'unignore', @@ -86,6 +87,7 @@ rebase: 'Rebasing...', restore: 'Restoring...', revert: 'Reverting...', + revert_submission: 'Reverting Submission...', submit: 'Submitting...', }; @@ -180,6 +182,7 @@ ChangeActions.REBASE_EDIT, ChangeActions.RESTORE, ChangeActions.REVERT, + ChangeActions.REVERT_SUBMISSION, ChangeActions.STOP_EDIT, QUICK_APPROVE_ACTION.key, RevisionActions.REBASE, @@ -189,9 +192,19 @@ const AWAIT_CHANGE_ATTEMPTS = 5; const AWAIT_CHANGE_TIMEOUT_MS = 1000; - Polymer({ - is: 'gr-change-actions', - + /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.RESTClientMixin + */ + class GrChangeActions extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.PatchSetBehavior, + Gerrit.RESTClientBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-change-actions'; } /** * Fired when the change should be reloaded. * @@ -201,7 +214,7 @@ /** * Fired when an action is tapped. * - * @event <action key>-tap + * @event custom-tap - naming pattern: <action key>-tap */ /** @@ -216,7 +229,15 @@ * @event show-error */ - properties: { + constructor() { + super(); + this.ActionType = ActionType; + this.ChangeActions = ChangeActions; + this.RevisionActions = RevisionActions; + } + + static get properties() { + return { /** * @type {{ * _number: number, @@ -226,217 +247,214 @@ * subject: string, * }} */ - change: Object, - actions: { - type: Object, - value() { return {}; }, - }, - primaryActionKeys: { - type: Array, - value() { - return [ - RevisionActions.SUBMIT, - ]; + change: Object, + actions: { + type: Object, + value() { return {}; }, }, - }, - disableEdit: { - type: Boolean, - value: false, - }, - _hasKnownChainState: { - type: Boolean, - value: false, - }, - _hideQuickApproveAction: { - type: Boolean, - value: false, - }, - changeNum: String, - changeStatus: String, - commitNum: String, - hasParent: { - type: Boolean, - observer: '_computeChainState', - }, - latestPatchNum: String, - commitMessage: { - type: String, - value: '', - }, - /** @type {?} */ - revisionActions: { - type: Object, - notify: true, - value() { return {}; }, - }, - // If property binds directly to [[revisionActions.submit]] it is not - // updated when revisionActions doesn't contain submit action. - /** @type {?} */ - _revisionSubmitAction: { - type: Object, - computed: '_getSubmitAction(revisionActions)', - }, - // If property binds directly to [[revisionActions.rebase]] it is not - // updated when revisionActions doesn't contain rebase action. - /** @type {?} */ - _revisionRebaseAction: { - type: Object, - computed: '_getRebaseAction(revisionActions)', - }, - privateByDefault: String, + primaryActionKeys: { + type: Array, + value() { + return [ + RevisionActions.SUBMIT, + ]; + }, + }, + disableEdit: { + type: Boolean, + value: false, + }, + _hasKnownChainState: { + type: Boolean, + value: false, + }, + _hideQuickApproveAction: { + type: Boolean, + value: false, + }, + changeNum: String, + changeStatus: String, + commitNum: String, + hasParent: { + type: Boolean, + observer: '_computeChainState', + }, + latestPatchNum: String, + commitMessage: { + type: String, + value: '', + }, + /** @type {?} */ + revisionActions: { + type: Object, + notify: true, + value() { return {}; }, + }, + // If property binds directly to [[revisionActions.submit]] it is not + // updated when revisionActions doesn't contain submit action. + /** @type {?} */ + _revisionSubmitAction: { + type: Object, + computed: '_getSubmitAction(revisionActions)', + }, + // If property binds directly to [[revisionActions.rebase]] it is not + // updated when revisionActions doesn't contain rebase action. + /** @type {?} */ + _revisionRebaseAction: { + type: Object, + computed: '_getRebaseAction(revisionActions)', + }, + privateByDefault: String, - _loading: { - type: Boolean, - value: true, - }, - _actionLoadingMessage: { - type: String, - value: '', - }, - _allActionValues: { - type: Array, - computed: '_computeAllActions(actions.*, revisionActions.*,' + + _loading: { + type: Boolean, + value: true, + }, + _actionLoadingMessage: { + type: String, + value: '', + }, + _allActionValues: { + type: Array, + computed: '_computeAllActions(actions.*, revisionActions.*,' + 'primaryActionKeys.*, _additionalActions.*, change, ' + '_actionPriorityOverrides.*)', - }, - _topLevelActions: { - type: Array, - computed: '_computeTopLevelActions(_allActionValues.*, ' + - '_hiddenActions.*, _overflowActions.*)', - observer: '_filterPrimaryActions', - }, - _topLevelPrimaryActions: Array, - _topLevelSecondaryActions: Array, - _menuActions: { - type: Array, - computed: '_computeMenuActions(_allActionValues.*, _hiddenActions.*, ' + - '_overflowActions.*)', - }, - _overflowActions: { - type: Array, - value() { - const value = [ - { - type: ActionType.CHANGE, - key: ChangeActions.WIP, - }, - { - type: ActionType.CHANGE, - key: ChangeActions.DELETE, - }, - { - type: ActionType.REVISION, - key: RevisionActions.CHERRYPICK, - }, - { - type: ActionType.CHANGE, - key: ChangeActions.MOVE, - }, - { - type: ActionType.REVISION, - key: RevisionActions.DOWNLOAD, - }, - { - type: ActionType.CHANGE, - key: ChangeActions.IGNORE, - }, - { - type: ActionType.CHANGE, - key: ChangeActions.UNIGNORE, - }, - { - type: ActionType.CHANGE, - key: ChangeActions.REVIEWED, - }, - { - type: ActionType.CHANGE, - key: ChangeActions.UNREVIEWED, - }, - { - type: ActionType.CHANGE, - key: ChangeActions.PRIVATE, - }, - { - type: ActionType.CHANGE, - key: ChangeActions.PRIVATE_DELETE, - }, - { - type: ActionType.CHANGE, - key: ChangeActions.FOLLOW_UP, - }, - ]; - return value; }, - }, - _actionPriorityOverrides: { - type: Array, - value() { return []; }, - }, - _additionalActions: { - type: Array, - value() { return []; }, - }, - _hiddenActions: { - type: Array, - value() { return []; }, - }, - _disabledMenuActions: { - type: Array, - value() { return []; }, - }, - // editPatchsetLoaded == "does the current selected patch range have - // 'edit' as one of either basePatchNum or patchNum". - editPatchsetLoaded: { - type: Boolean, - value: false, - }, - // editMode == "is edit mode enabled in the file list". - editMode: { - type: Boolean, - value: false, - }, - editBasedOnCurrentPatchSet: { - type: Boolean, - value: true, - }, - }, + _topLevelActions: { + type: Array, + computed: '_computeTopLevelActions(_allActionValues.*, ' + + '_hiddenActions.*, _overflowActions.*)', + observer: '_filterPrimaryActions', + }, + _topLevelPrimaryActions: Array, + _topLevelSecondaryActions: Array, + _menuActions: { + type: Array, + computed: '_computeMenuActions(_allActionValues.*, ' + + '_hiddenActions.*, _overflowActions.*)', + }, + _overflowActions: { + type: Array, + value() { + const value = [ + { + type: ActionType.CHANGE, + key: ChangeActions.WIP, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.DELETE, + }, + { + type: ActionType.REVISION, + key: RevisionActions.CHERRYPICK, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.MOVE, + }, + { + type: ActionType.REVISION, + key: RevisionActions.DOWNLOAD, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.IGNORE, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.UNIGNORE, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.REVIEWED, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.UNREVIEWED, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.PRIVATE, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.PRIVATE_DELETE, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.FOLLOW_UP, + }, + ]; + return value; + }, + }, + _actionPriorityOverrides: { + type: Array, + value() { return []; }, + }, + _additionalActions: { + type: Array, + value() { return []; }, + }, + _hiddenActions: { + type: Array, + value() { return []; }, + }, + _disabledMenuActions: { + type: Array, + value() { return []; }, + }, + // editPatchsetLoaded == "does the current selected patch range have + // 'edit' as one of either basePatchNum or patchNum". + editPatchsetLoaded: { + type: Boolean, + value: false, + }, + // editMode == "is edit mode enabled in the file list". + editMode: { + type: Boolean, + value: false, + }, + editBasedOnCurrentPatchSet: { + type: Boolean, + value: true, + }, + }; + } - ActionType, - ChangeActions, - RevisionActions, - - behaviors: [ - Gerrit.FireBehavior, - Gerrit.PatchSetBehavior, - Gerrit.RESTClientBehavior, - ], - - observers: [ - '_actionsChanged(actions.*, revisionActions.*, _additionalActions.*)', - '_changeChanged(change)', - '_editStatusChanged(editMode, editPatchsetLoaded, ' + + static get observers() { + return [ + '_actionsChanged(actions.*, revisionActions.*, _additionalActions.*)', + '_changeChanged(change)', + '_editStatusChanged(editMode, editPatchsetLoaded, ' + 'editBasedOnCurrentPatchSet, disableEdit, actions.*, change.*)', - ], + ]; + } - listeners: { - 'fullscreen-overlay-opened': '_handleHideBackgroundContent', - 'fullscreen-overlay-closed': '_handleShowBackgroundContent', - }, + created() { + super.created(); + this.addEventListener('fullscreen-overlay-opened', + () => this._handleHideBackgroundContent()); + this.addEventListener('fullscreen-overlay-closed', + () => this._handleShowBackgroundContent()); + } ready() { + super.ready(); this.$.jsAPI.addElement(this.$.jsAPI.Element.CHANGE_ACTIONS, this); this._handleLoadingComplete(); - }, + } _getSubmitAction(revisionActions) { return this._getRevisionAction(revisionActions, 'submit', null); - }, + } _getRebaseAction(revisionActions) { return this._getRevisionAction(revisionActions, 'rebase', - {rebaseOnCurrent: null} + {rebaseOnCurrent: null} ); - }, + } _getRevisionAction(revisionActions, actionName, emptyActionValue) { if (!revisionActions) { @@ -448,7 +466,7 @@ return emptyActionValue; } return revisionActions[actionName]; - }, + } reload() { if (!this.changeNum || !this.latestPatchNum) { @@ -466,11 +484,11 @@ this._loading = false; throw err; }); - }, + } _handleLoadingComplete() { Gerrit.awaitPluginsLoaded().then(() => this._loading = false); - }, + } _updateRebaseAction(revisionActions) { if (revisionActions && revisionActions.rebase) { @@ -482,11 +500,11 @@ this._parentIsCurrent = true; } return revisionActions; - }, + } _changeChanged() { this.reload(); - }, + } addActionButton(type, label) { if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { @@ -501,7 +519,7 @@ }; this.push('_additionalActions', action); return action.__key; - }, + } removeActionButton(key) { const idx = this._indexOfActionButtonWithKey(key); @@ -509,7 +527,7 @@ return; } this.splice('_additionalActions', idx, 1); - }, + } setActionButtonProp(key, prop, value) { this.set([ @@ -517,7 +535,7 @@ this._indexOfActionButtonWithKey(key), prop, ], value); - }, + } setActionOverflow(type, key, overflow) { if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { @@ -534,7 +552,7 @@ } else if (overflow) { this.push('_overflowActions', action); } - }, + } setActionPriority(type, key, priority) { if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { @@ -553,7 +571,7 @@ } else { this.push('_actionPriorityOverrides', action); } - }, + } setActionHidden(type, key, hidden) { if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { @@ -566,7 +584,7 @@ } else if (!hidden && idx !== -1) { this.splice('_hiddenActions', idx, 1); } - }, + } getActionDetails(action) { if (this.revisionActions[action]) { @@ -574,7 +592,7 @@ } else if (this.actions[action]) { return this.actions[action]; } - }, + } _indexOfActionButtonWithKey(key) { for (let i = 0; i < this._additionalActions.length; i++) { @@ -583,20 +601,20 @@ } } return -1; - }, + } _getRevisionActions() { return this.$.restAPI.getChangeRevisionActions(this.changeNum, this.latestPatchNum); - }, + } _shouldHideActions(actions, loading) { return loading || !actions || !actions.base || !actions.base.length; - }, + } _keyCount(changeRecord) { return Object.keys((changeRecord && changeRecord.base) || {}).length; - }, + } _actionsChanged(actionsChangeRecord, revisionActionsChangeRecord, additionalActionsChangeRecord) { @@ -623,11 +641,11 @@ this.set('revisionActions.download', DOWNLOAD_ACTION); } } - }, + } - /** - * @param {string=} actionName - */ + /** + * @param {string=} actionName + */ _deleteAndNotify(actionName) { if (this.actions && this.actions[actionName]) { delete this.actions[actionName]; @@ -635,7 +653,7 @@ // see https://github.com/Polymer/polymer/issues/2631 this.notifyPath('actions.' + actionName, false); } - }, + } _editStatusChanged(editMode, editPatchsetLoaded, editBasedOnCurrentPatchSet, disableEdit) { @@ -702,13 +720,13 @@ // Remove edit button. this._deleteAndNotify('edit'); } - }, + } _getValuesFor(obj) { return Object.keys(obj).map(key => { return obj[key]; }); - }, + } _getLabelStatus(label) { if (label.approved) { @@ -720,7 +738,7 @@ } else { return LabelStatus.NEED; } - }, + } /** * Get highest score for last missing permitted label for current change. @@ -768,7 +786,7 @@ } } return null; - }, + } hideQuickApproveAction() { this._topLevelSecondaryActions = @@ -776,7 +794,7 @@ return sa.key !== QUICK_APPROVE_ACTION.key; }); this._hideQuickApproveAction = true; - }, + } _getQuickApproveAction() { if (this._hideQuickApproveAction) { @@ -795,7 +813,7 @@ review.labels[approval.label] = approval.score; action.payload = review; return action; - }, + } _getActionValues(actionsChangeRecord, primariesChangeRecord, additionalActionsChangeRecord, type) { @@ -840,7 +858,7 @@ return Object.assign({}, a); }); return result.concat(additionalActions).concat(pluginActions); - }, + } _populateActionUrl(action) { const patchNum = @@ -848,7 +866,7 @@ this.$.restAPI.getChangeActionURL( this.changeNum, patchNum, '/' + action.__key) .then(url => action.__url = url); - }, + } /** * Given a change action, return a display label that uses the appropriate @@ -864,26 +882,27 @@ } // Otherwise, just map the name to sentence case. return this._toSentenceCase(action.label); - }, + } /** * Capitalize the first letter and lowecase all others. + * * @param {string} s * @return {string} */ _toSentenceCase(s) { if (!s.length) { return ''; } return s[0].toUpperCase() + s.slice(1).toLowerCase(); - }, + } _computeLoadingLabel(action) { return ActionLoadingLabels[action] || 'Working...'; - }, + } _canSubmitChange() { return this.$.jsAPI.canSubmitChange(this.change, this._getRevision(this.change, this.latestPatchNum)); - }, + } _getRevision(change, patchNum) { for (const rev of Object.values(change.revisions)) { @@ -892,19 +911,30 @@ } } return null; - }, + } _modifyRevertMsg() { return this.$.jsAPI.modifyRevertMsg(this.change, this.$.confirmRevertDialog.message, this.commitMessage); - }, + } showRevertDialog() { this.$.confirmRevertDialog.populateRevertMessage( this.commitMessage, this.change.current_revision); this.$.confirmRevertDialog.message = this._modifyRevertMsg(); this._showActionDialog(this.$.confirmRevertDialog); - }, + } + + showRevertSubmissionDialog() { + const query = 'submissionid:' + this.change.submission_id; + this.$.restAPI.getChanges('', query) + .then(changes => { + this.$.confirmRevertSubmissionDialog. + populateRevertSubmissionMessage( + this.commitMessage, this.change, changes); + this._showActionDialog(this.$.confirmRevertSubmissionDialog); + }); + } _handleActionTap(e) { e.preventDefault(); @@ -922,7 +952,7 @@ } const type = el.getAttribute('data-action-type'); this._handleAction(type, key); - }, + } _handleOveflowItemTap(e) { e.preventDefault(); @@ -934,7 +964,7 @@ return; } this._handleAction(e.detail.action.__type, e.detail.action.__key); - }, + } _handleAction(type, key) { this.$.reporting.reportInteraction(`${type}-${key}`); @@ -948,7 +978,7 @@ default: this._fireAction(this._prependSlash(key), this.actions[key], false); } - }, + } _handleChangeAction(key) { let action; @@ -956,6 +986,9 @@ case ChangeActions.REVERT: this.showRevertDialog(); break; + case ChangeActions.REVERT_SUBMISSION: + this.showRevertSubmissionDialog(); + break; case ChangeActions.ABANDON: this._showActionDialog(this.$.confirmAbandonDialog); break; @@ -996,7 +1029,7 @@ default: this._fireAction(this._prependSlash(key), this.actions[key], false); } - }, + } _handleRevisionAction(key) { switch (key) { @@ -1018,11 +1051,11 @@ this._fireAction(this._prependSlash(key), this.revisionActions[key], true); } - }, + } _prependSlash(key) { return key === '/' ? key : `/${key}`; - }, + } /** * _hasKnownChainState set to true true if hasParent is defined (can be @@ -1030,25 +1063,25 @@ */ _computeChainState(hasParent) { this._hasKnownChainState = true; - }, + } _calculateDisabled(action, hasKnownChainState) { if (action.__key === 'rebase' && hasKnownChainState === false) { return true; } return !action.enabled; - }, + } _handleConfirmDialogCancel() { this._hideAllDialogs(); - }, + } _hideAllDialogs() { const dialogEls = Polymer.dom(this.root).querySelectorAll('.confirmDialog'); for (const dialogEl of dialogEls) { dialogEl.hidden = true; } this.$.overlay.close(); - }, + } _handleRebaseConfirm(e) { const el = this.$.confirmRebase; @@ -1056,15 +1089,15 @@ this.$.overlay.close(); el.hidden = true; this._fireAction('/rebase', this.revisionActions.rebase, true, payload); - }, + } _handleCherrypickConfirm() { this._handleCherryPickRestApi(false); - }, + } _handleCherrypickConflictConfirm() { this._handleCherryPickRestApi(true); - }, + } _handleCherryPickRestApi(conflicts) { const el = this.$.confirmCherrypick; @@ -1089,7 +1122,7 @@ allow_conflicts: conflicts, } ); - }, + } _handleMoveConfirm() { const el = this.$.confirmMove; @@ -1108,7 +1141,7 @@ message: el.message, } ); - }, + } _handleRevertDialogConfirm() { const el = this.$.confirmRevertDialog; @@ -1116,7 +1149,15 @@ el.hidden = true; this._fireAction('/revert', this.actions.revert, false, {message: el.message}); - }, + } + + _handleRevertSubmissionDialogConfirm() { + const el = this.$.confirmRevertSubmissionDialog; + this.$.overlay.close(); + el.hidden = true; + this._fireAction('/revert_submission', this.actions.revert_submission, + false, {message: el.message}); + } _handleAbandonDialogConfirm() { const el = this.$.confirmAbandonDialog; @@ -1124,38 +1165,38 @@ el.hidden = true; this._fireAction('/abandon', this.actions.abandon, false, {message: el.message}); - }, + } _handleCreateFollowUpChange() { this.$.createFollowUpChange.handleCreateChange(); this._handleCloseCreateFollowUpChange(); - }, + } _handleCloseCreateFollowUpChange() { this.$.overlay.close(); - }, + } _handleDeleteConfirm() { this._fireAction('/', this.actions[ChangeActions.DELETE], false); - }, + } _handleDeleteEditConfirm() { this._hideAllDialogs(); this._fireAction('/edit', this.actions.deleteEdit, false); - }, + } _handleSubmitConfirm() { if (!this._canSubmitChange()) { return; } this._hideAllDialogs(); this._fireAction('/submit', this.revisionActions.submit, true); - }, + } _getActionOverflowIndex(type, key) { return this._overflowActions.findIndex(action => { return action.type === type && action.key === key; }); - }, + } _setLoadingOnButtonWithKey(type, key) { this._actionLoadingMessage = this._computeLoadingLabel(key); @@ -1178,7 +1219,7 @@ buttonEl.removeAttribute('loading'); buttonEl.disabled = false; }.bind(this); - }, + } /** * @param {string} endpoint @@ -1192,7 +1233,7 @@ this._send(action.method, opt_payload, endpoint, revAction, cleanupFn, action).then(this._handleResponse.bind(this, action)); - }, + } _showActionDialog(dialog) { this._hideAllDialogs(); @@ -1203,7 +1244,7 @@ dialog.resetFocus(); } }); - }, + } // TODO(rmistry): Redo this after // https://bugs.chromium.org/p/gerrit/issues/detail?id=4671 is resolved. @@ -1211,7 +1252,7 @@ const labels = this.$.jsAPI.getLabelValuesPostRevert(this.change); if (!labels) { return Promise.resolve(); } return this.$.restAPI.saveChangeReview(newChangeId, 'current', {labels}); - }, + } _handleResponse(action, response) { if (!response) { return; } @@ -1240,13 +1281,24 @@ case ChangeActions.REBASE_EDIT: Gerrit.Nav.navigateToChange(this.change); break; + case ChangeActions.REVERT_SUBMISSION: + if (!obj.revert_changes || !obj.revert_changes.length) return; + /* If there is only 1 change then gerrit will automatically + redirect to that change */ + Gerrit.Nav.navigateToSearchQuery('topic: ' + + obj.revert_changes[0].topic); + break; default: this.dispatchEvent(new CustomEvent('reload-change', {detail: {action: action.__key}, bubbles: false})); break; } }); - }, + } + + _handleShowRevertSubmissionChangesConfirm() { + this._hideAllDialogs(); + } _handleResponseError(action, response, body) { if (action && action.__key === RevisionActions.CHERRYPICK) { @@ -1263,7 +1315,7 @@ throw Error(errText); } }); - }, + } /** * @param {string} method @@ -1306,62 +1358,63 @@ return response; }); }); - }, + } _handleAbandonTap() { this._showActionDialog(this.$.confirmAbandonDialog); - }, + } _handleCherrypickTap() { this.$.confirmCherrypick.branch = ''; this._showActionDialog(this.$.confirmCherrypick); - }, + } _handleMoveTap() { this.$.confirmMove.branch = ''; this.$.confirmMove.message = ''; this._showActionDialog(this.$.confirmMove); - }, + } _handleDownloadTap() { this.fire('download-tap', null, {bubbles: false}); - }, + } _handleDeleteTap() { this._showActionDialog(this.$.confirmDeleteDialog); - }, + } _handleDeleteEditTap() { this._showActionDialog(this.$.confirmDeleteEditDialog); - }, + } _handleFollowUpTap() { this._showActionDialog(this.$.createFollowUpDialog); - }, + } _handleWipTap() { this._fireAction('/wip', this.actions.wip, false); - }, + } _handlePublishEditTap() { this._fireAction('/edit:publish', this.actions.publishEdit, false); - }, + } _handleRebaseEditTap() { this._fireAction('/edit:rebase', this.actions.rebaseEdit, false); - }, + } _handleHideBackgroundContent() { this.$.mainContent.classList.add('overlayOpen'); - }, + } _handleShowBackgroundContent() { this.$.mainContent.classList.remove('overlayOpen'); - }, + } /** * Merge sources of change actions into a single ordered array of action * values. + * * @param {!Array} changeActionsRecord * @param {!Array} revisionActionsRecord * @param {!Array} primariesRecord @@ -1400,7 +1453,7 @@ } return action; }); - }, + } _getActionPriority(action) { if (action.__type && action.__key) { @@ -1422,7 +1475,7 @@ return ActionPriority.REVISION; } return ActionPriority.DEFAULT; - }, + } /** * Sort comparator to define the order of change actions. @@ -1436,7 +1489,7 @@ } else { return priorityDelta; } - }, + } _computeTopLevelActions(actionRecord, hiddenActionsRecord) { const hiddenActions = hiddenActionsRecord.base || []; @@ -1444,14 +1497,14 @@ const overflow = this._getActionOverflowIndex(a.__type, a.__key) !== -1; return !(overflow || hiddenActions.includes(a.__key)); }); - }, + } _filterPrimaryActions(_topLevelActions) { this._topLevelPrimaryActions = _topLevelActions.filter(action => - action.__primary); + action.__primary); this._topLevelSecondaryActions = _topLevelActions.filter(action => - !action.__primary); - }, + !action.__primary); + } _computeMenuActions(actionRecord, hiddenActionsRecord) { const hiddenActions = hiddenActionsRecord.base || []; @@ -1468,7 +1521,7 @@ tooltip: action.title, }; }); - }, + } /** * Occasionally, a change created by a change action is not yet knwon to the @@ -1502,22 +1555,24 @@ }; check(); }); - }, + } _handleEditTap() { this.dispatchEvent(new CustomEvent('edit-tap', {bubbles: false})); - }, + } _handleStopEditTap() { this.dispatchEvent(new CustomEvent('stop-edit-tap', {bubbles: false})); - }, + } _computeHasTooltip(title) { return !!title; - }, + } _computeHasIcon(action) { return action.icon ? '' : 'hidden'; - }, - }); + } + } + + customElements.define(GrChangeActions.is, GrChangeActions); })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html index 37201ac..4f1bfed 100644 --- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html +++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-actions</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -378,7 +378,7 @@ element._handleRebaseConfirm({detail: {base: '1234'}}); rebaseAction.rebaseOnCurrent = true; assert.deepEqual(fireActionStub.lastCall.args, - ['/rebase', rebaseAction, true, {base: '1234'}]); + ['/rebase', rebaseAction, true, {base: '1234'}]); done(); }); }); @@ -466,7 +466,9 @@ element._handleDeleteEditTap(); assert.isFalse(element.$.confirmDeleteEditDialog.hidden); MockInteractions.tap( - element.$$('#confirmDeleteEditDialog').$$('gr-button[primary]')); + element.shadowRoot + .querySelector('#confirmDeleteEditDialog') + .$$('gr-button[primary]')); flushAsynchronousOperations(); assert.equal(fireActionStub.lastCall.args[0], '/edit'); @@ -980,9 +982,12 @@ test('shows confirm dialog', () => { element._handleDeleteTap(); - assert.isFalse(element.$$('#confirmDeleteDialog').hidden); + assert.isFalse(element.shadowRoot + .querySelector('#confirmDeleteDialog').hidden); MockInteractions.tap( - element.$$('#confirmDeleteDialog').$$('gr-button[primary]')); + element.shadowRoot + .querySelector('#confirmDeleteDialog') + .$$('gr-button[primary]')); flushAsynchronousOperations(); assert.isTrue(fireActionStub.calledWith('/', deleteAction, false)); }); @@ -990,9 +995,12 @@ test('hides delete confirm on cancel', () => { element._handleDeleteTap(); MockInteractions.tap( - element.$$('#confirmDeleteDialog').$$('gr-button:not([primary])')); + element.shadowRoot + .querySelector('#confirmDeleteDialog') + .$$('gr-button:not([primary])')); flushAsynchronousOperations(); - assert.isTrue(element.$$('#confirmDeleteDialog').hidden); + assert.isTrue(element.shadowRoot + .querySelector('#confirmDeleteDialog').hidden); assert.isFalse(fireActionStub.called); }); }); @@ -1060,7 +1068,6 @@ element.reload().then(() => { flush(done); }); }); - test('unignore button is not outside of the overflow menu', () => { assert.isNotOk(element.$$('[data-action-key="unignore"]')); }); @@ -1140,7 +1147,6 @@ element.reload().then(() => { flush(done); }); }); - test('unreviewed button not outside of the overflow menu', () => { assert.isNotOk(element.$$('[data-action-key="unreviewed"]')); }); @@ -1424,6 +1430,7 @@ let payload; let onShowError; let onShowAlert; + let getResponseObjectStub; setup(() => { cleanup = sinon.stub(); @@ -1439,12 +1446,15 @@ suite('happy path', () => { let sendStub; - setup(() => { sandbox.stub(element, 'fetchChangeUpdates') .returns(Promise.resolve({isLatest: true})); sendStub = sandbox.stub(element.$.restAPI, 'executeChangeAction') .returns(Promise.resolve({})); + getResponseObjectStub = sandbox.stub(element.$.restAPI, + 'getResponseObject'); + sandbox.stub(Gerrit.Nav, + 'navigateToChange').returns(Promise.resolve(true)); }); test('change action', () => { @@ -1457,6 +1467,88 @@ }); }); + suite('show revert submission dialog', () => { + setup(() => { + element.change.submission_id = '199'; + element.change.current_revision = '2000'; + sandbox.stub(element.$.restAPI, 'getChanges') + .returns(Promise.resolve([ + {change_id: '12345678901234', topic: 'T', subject: 'random'}, + {change_id: '23456', topic: 'T', subject: 'a'.repeat(100)}, + ])); + }); + + test('revert submission shows submissionId', done => { + const expectedMsg = 'Revert submission 199' + '\n\n' + + 'Reason for revert: <INSERT REASONING HERE>' + '\n' + + 'Reverted Changes:' + '\n' + + '1234567890: random' + '\n' + + '23456: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' + + '\n'; + const modifiedMsg = expectedMsg + 'abcd'; + sandbox.stub(element.$.confirmRevertSubmissionDialog, + '_modifyRevertSubmissionMsg').returns(modifiedMsg); + element.showRevertSubmissionDialog(); + flush(() => { + const msg = element.$.confirmRevertSubmissionDialog.message; + assert.equal(msg, modifiedMsg); + done(); + }); + }); + }); + + suite('single changes revert', () => { + let navigateToSearchQueryStub; + setup(() => { + getResponseObjectStub + .returns(Promise.resolve({revert_changes: [ + {change_id: 12345}, + ]})); + showActionDialogStub = sandbox.stub(element, '_showActionDialog'); + navigateToSearchQueryStub = sandbox.stub(Gerrit.Nav, + 'navigateToSearchQuery'); + }); + + test('revert submission single change', done => { + element._send('POST', {message: 'Revert submission'}, + '/revert_submission', false, cleanup).then(res => { + element._handleResponse({__key: 'revert_submission'}, {}). + then(() => { + assert.isTrue(navigateToSearchQueryStub.called); + done(); + }); + }); + }); + }); + + suite('multiple changes revert', () => { + let showActionDialogStub; + let navigateToSearchQueryStub; + setup(() => { + getResponseObjectStub + .returns(Promise.resolve({revert_changes: [ + {change_id: 12345, topic: 'T'}, + {change_id: 23456, topic: 'T'}, + ]})); + showActionDialogStub = sandbox.stub(element, '_showActionDialog'); + navigateToSearchQueryStub = sandbox.stub(Gerrit.Nav, + 'navigateToSearchQuery'); + }); + + test('revert submission multiple change', done => { + element._send('POST', {message: 'Revert submission'}, + '/revert_submission', false, cleanup).then(res => { + element._handleResponse({__key: 'revert_submission'}, {}).then( + () => { + assert.isFalse(showActionDialogStub.called); + assert.isTrue(navigateToSearchQueryStub.calledWith( + 'topic: T')); + done(); + }); + }); + }); + }); + test('revision action', () => { return element._send('DELETE', payload, '/endpoint', true, cleanup) .then(() => { @@ -1541,7 +1633,6 @@ element.changeNum = '42'; element.latestPatchNum = '2'; - sandbox.stub(element.$.confirmCherrypick.$.restAPI, 'getRepoBranches').returns(Promise.resolve([])); sandbox.stub(element.$.confirmMove.$.restAPI,
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html index e60909c..8d85dce 100644 --- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html +++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-metadata</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -114,7 +114,7 @@ js_resource_paths: [], html_resource_paths: [ new URL('test/plugin.html?' + Math.random(), - window.location.href).toString(), + window.location.href).toString(), ], }, }; @@ -140,7 +140,7 @@ setup(() => { Gerrit.install(p => plugin = p, '0.1', new URL('test/plugin.html?' + Math.random(), - window.location.href).toString()); + window.location.href).toString()); sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true); Gerrit._loadPlugins([]); element = createElement();
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html index 6a92d96..539b1b6 100644 --- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html +++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -43,7 +43,9 @@ <dom-module id="gr-change-metadata"> <template> - <style include="gr-change-metadata-shared-styles"></style> + <style include="gr-change-metadata-shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="shared-styles"> :host { display: table; @@ -175,9 +177,10 @@ <span class="title">Assignee</span> <span class="value"> <gr-account-list - max-count="1" id="assigneeValue" placeholder="Set assignee..." + max-count="1" + skip-suggest-on-empty accounts="{{_assignee}}" readonly="[[_computeAssigneeReadOnly(_mutable, change)]]" suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]">
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js index 07eb59e..94519b0 100644 --- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js +++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -48,105 +48,111 @@ TRUSTED: 'TRUSTED', }; - Polymer({ - is: 'gr-change-metadata', - + /** + * @appliesMixin Gerrit.RESTClientMixin + */ + class GrChangeMetadata extends Polymer.mixinBehaviors( [ + Gerrit.RESTClientBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-change-metadata'; } /** * Fired when the change topic is changed. * * @event topic-changed */ - properties: { + static get properties() { + return { /** @type {?} */ - change: Object, - labels: { - type: Object, - notify: true, - }, - account: Object, - /** @type {?} */ - revision: Object, - commitInfo: Object, - _mutable: { - type: Boolean, - computed: '_computeIsMutable(account)', - }, - /** @type {?} */ - serverConfig: Object, - parentIsCurrent: Boolean, - _notCurrentMessage: { - type: String, - value: NOT_CURRENT_MESSAGE, - readOnly: true, - }, - _topicReadOnly: { - type: Boolean, - computed: '_computeTopicReadOnly(_mutable, change)', - }, - _hashtagReadOnly: { - type: Boolean, - computed: '_computeHashtagReadOnly(_mutable, change)', - }, - /** - * @type {Gerrit.PushCertificateValidation} - */ - _pushCertificateValidation: { - type: Object, - computed: '_computePushCertificateValidation(serverConfig, change)', - }, - _showRequirements: { - type: Boolean, - computed: '_computeShowRequirements(change)', - }, - - _assignee: Array, - _isWip: { - type: Boolean, - computed: '_computeIsWip(change)', - }, - _newHashtag: String, - - _settingTopic: { - type: Boolean, - value: false, - }, - - _currentParents: { - type: Array, - computed: '_computeParents(change)', - }, - - /** @type {?} */ - _CHANGE_ROLE: { - type: Object, - readOnly: true, - value: { - OWNER: 'owner', - UPLOADER: 'uploader', - AUTHOR: 'author', - COMMITTER: 'committer', + change: Object, + labels: { + type: Object, + notify: true, }, - }, - }, + account: Object, + /** @type {?} */ + revision: Object, + commitInfo: Object, + _mutable: { + type: Boolean, + computed: '_computeIsMutable(account)', + }, + /** @type {?} */ + serverConfig: Object, + parentIsCurrent: Boolean, + _notCurrentMessage: { + type: String, + value: NOT_CURRENT_MESSAGE, + readOnly: true, + }, + _topicReadOnly: { + type: Boolean, + computed: '_computeTopicReadOnly(_mutable, change)', + }, + _hashtagReadOnly: { + type: Boolean, + computed: '_computeHashtagReadOnly(_mutable, change)', + }, + /** + * @type {Gerrit.PushCertificateValidation} + */ + _pushCertificateValidation: { + type: Object, + computed: '_computePushCertificateValidation(serverConfig, change)', + }, + _showRequirements: { + type: Boolean, + computed: '_computeShowRequirements(change)', + }, - behaviors: [ - Gerrit.RESTClientBehavior, - ], + _assignee: Array, + _isWip: { + type: Boolean, + computed: '_computeIsWip(change)', + }, + _newHashtag: String, - observers: [ - '_changeChanged(change)', - '_labelsChanged(change.labels)', - '_assigneeChanged(_assignee.*)', - ], + _settingTopic: { + type: Boolean, + value: false, + }, + + _currentParents: { + type: Array, + computed: '_computeParents(change)', + }, + + /** @type {?} */ + _CHANGE_ROLE: { + type: Object, + readOnly: true, + value: { + OWNER: 'owner', + UPLOADER: 'uploader', + AUTHOR: 'author', + COMMITTER: 'committer', + }, + }, + }; + } + + static get observers() { + return [ + '_changeChanged(change)', + '_labelsChanged(change.labels)', + '_assigneeChanged(_assignee.*)', + ]; + } _labelsChanged(labels) { this.labels = Object.assign({}, labels) || null; - }, + } _changeChanged(change) { this._assignee = change.assignee ? [change.assignee] : []; - }, + } _assigneeChanged(assigneeRecord) { if (!this.change) { return; } @@ -162,11 +168,11 @@ this.set(['change', 'assignee'], undefined); this.$.restAPI.deleteAssignee(this.change._number); } - }, + } _computeHideStrategy(change) { return !this.changeIsOpen(change); - }, + } /** * @param {Object} commitInfo @@ -184,15 +190,15 @@ config: serverConfig, }); return weblinks.length ? weblinks : null; - }, + } _computeStrategy(change) { return SubmitTypeLabel[change.submit_type]; - }, + } _computeLabelNames(labels) { return Object.keys(labels).sort(); - }, + } _handleTopicChanged(e, topic) { const lastTopic = this.change.topic; @@ -207,19 +213,19 @@ 'topic-changed', {bubbles: true, composed: true})); } }); - }, + } _showAddTopic(changeRecord, settingTopic) { const hasTopic = !!changeRecord && !!changeRecord.base && !!changeRecord.base.topic; return !hasTopic && !settingTopic; - }, + } _showTopicChip(changeRecord, settingTopic) { const hasTopic = !!changeRecord && !!changeRecord.base && !!changeRecord.base.topic; return hasTopic && !settingTopic; - }, + } _handleHashtagChanged(e) { const lastHashtag = this.change.hashtag; @@ -228,13 +234,13 @@ this._newHashtag = ''; this.$.restAPI.setChangeHashtag( this.change._number, {add: [newHashtag]}).then(newHashtag => { - this.set(['change', 'hashtags'], newHashtag); - if (newHashtag !== lastHashtag) { - this.dispatchEvent(new CustomEvent( - 'hashtag-changed', {bubbles: true, composed: true})); - } - }); - }, + this.set(['change', 'hashtags'], newHashtag); + if (newHashtag !== lastHashtag) { + this.dispatchEvent(new CustomEvent( + 'hashtag-changed', {bubbles: true, composed: true})); + } + }); + } _computeTopicReadOnly(mutable, change) { return !mutable || @@ -242,7 +248,7 @@ !change.actions || !change.actions.topic || !change.actions.topic.enabled; - }, + } _computeHashtagReadOnly(mutable, change) { return !mutable || @@ -250,7 +256,7 @@ !change.actions || !change.actions.hashtags || !change.actions.hashtags.enabled; - }, + } _computeAssigneeReadOnly(mutable, change) { return !mutable || @@ -258,17 +264,17 @@ !change.actions || !change.actions.assignee || !change.actions.assignee.enabled; - }, + } _computeTopicPlaceholder(_topicReadOnly) { // Action items in Material Design are uppercase -- placeholder label text // is sentence case. return _topicReadOnly ? 'No topic' : 'ADD TOPIC'; - }, + } _computeHashtagPlaceholder(_hashtagReadOnly) { return _hashtagReadOnly ? '' : HASHTAG_ADD_MESSAGE; - }, + } _computeShowRequirements(change) { if (change.status !== this.ChangeStatus.NEW) { @@ -281,7 +287,7 @@ const hasLabels = !!change.labels && Object.keys(change.labels).length > 0; return hasRequirements || hasLabels || !!change.work_in_progress; - }, + } /** * @return {?Gerrit.PushCertificateValidation} object representing data for @@ -326,7 +332,7 @@ default: throw new Error(`unknown certificate status: ${key.status}`); } - }, + } _problems(msg, key) { if (!key || !key.problems || key.problems.length === 0) { @@ -334,26 +340,26 @@ } return [msg + ':'].concat(key.problems).join('\n'); - }, + } _computeProjectURL(project) { return Gerrit.Nav.getUrlForProjectChanges(project); - }, + } _computeBranchURL(project, branch) { if (!this.change || !this.change.status) return ''; return Gerrit.Nav.getUrlForBranch(branch, project, this.change.status == this.ChangeStatus.NEW ? 'open' : - this.change.status.toLowerCase()); - }, + this.change.status.toLowerCase()); + } _computeTopicURL(topic) { return Gerrit.Nav.getUrlForTopic(topic); - }, + } _computeHashtagURL(hashtag) { return Gerrit.Nav.getUrlForHashtag(hashtag); - }, + } _handleTopicRemoved(e) { const target = Polymer.dom(e).rootTarget; @@ -367,7 +373,7 @@ target.disabled = false; return; }); - }, + } _handleHashtagRemoved(e) { e.preventDefault(); @@ -382,19 +388,20 @@ target.disabled = false; return; }); - }, + } _computeIsWip(change) { return !!change.work_in_progress; - }, + } _computeShowRoleClass(change, role) { return this._getNonOwnerRole(change, role) ? '' : 'hideDisplay'; - }, + } /** * Get the user with the specified role on the change. Returns null if the * user with that role is the same as the owner. + * * @param {!Object} change * @param {string} role One of the values from _CHANGE_ROLE * @return {Object|null} either an accound or null. @@ -427,7 +434,7 @@ } return null; - }, + } _computeParents(change) { if (!change || !change.current_revision || @@ -436,11 +443,11 @@ return undefined; } return change.revisions[change.current_revision].commit.parents; - }, + } _computeParentsLabel(parents) { return parents && parents.length > 1 ? 'Parents' : 'Parent'; - }, + } _computeParentListClass(parents, parentIsCurrent) { // Undefined check for polymer 2 @@ -453,24 +460,26 @@ parents && parents.length > 1 ? 'merge' : 'nonMerge', parentIsCurrent ? 'current' : 'notCurrent', ].join(' '); - }, + } _computeIsMutable(account) { return !!Object.keys(account).length; - }, + } editTopic() { if (this._topicReadOnly || this.change.topic) { return; } // Cannot use `this.$.ID` syntax because the element exists inside of a // dom-if. this.$$('.topicEditableLabel').open(); - }, + } _getReviewerSuggestionsProvider(change) { const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI, change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY); provider.init(); return provider; - }, - }); + } + } + + customElements.define(GrChangeMetadata.is, GrChangeMetadata); })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html index caa38d9..148d917 100644 --- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html +++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-metadata</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -426,7 +426,7 @@ {current_revision: '789', revisions: {456: {commit: {parents}}}})); assert.equal(element._computeParents( {current_revision: '456', revisions: {456: {commit: {parents}}}}), - parents); + parents); }); test('_computeParentsLabel', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js index 8ec00dd..346fee3 100644 --- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js +++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
@@ -17,47 +17,54 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-change-requirements', + /** + * @appliesMixin Gerrit.RESTClientMixin + */ + class GrChangeRequirements extends Polymer.mixinBehaviors( [ + Gerrit.RESTClientBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-change-requirements'; } - properties: { + static get properties() { + return { /** @type {?} */ - change: Object, - account: Object, - mutable: Boolean, - _requirements: { - type: Array, - computed: '_computeRequirements(change)', - }, - _requiredLabels: { - type: Array, - value: () => [], - }, - _optionalLabels: { - type: Array, - value: () => [], - }, - _showWip: { - type: Boolean, - computed: '_computeShowWip(change)', - }, - _showOptionalLabels: { - type: Boolean, - value: true, - }, - }, + change: Object, + account: Object, + mutable: Boolean, + _requirements: { + type: Array, + computed: '_computeRequirements(change)', + }, + _requiredLabels: { + type: Array, + value: () => [], + }, + _optionalLabels: { + type: Array, + value: () => [], + }, + _showWip: { + type: Boolean, + computed: '_computeShowWip(change)', + }, + _showOptionalLabels: { + type: Boolean, + value: true, + }, + }; + } - behaviors: [ - Gerrit.RESTClientBehavior, - ], - - observers: [ - '_computeLabels(change.labels.*)', - ], + static get observers() { + return [ + '_computeLabels(change.labels.*)', + ]; + } _computeShowWip(change) { return change.work_in_progress; - }, + } _computeRequirements(change) { const _requirements = []; @@ -78,15 +85,15 @@ } return _requirements; - }, + } _computeRequirementClass(requirementStatus) { return requirementStatus ? 'approved' : ''; - }, + } _computeRequirementIcon(requirementStatus) { return requirementStatus ? 'gr-icons:check' : 'gr-icons:hourglass'; - }, + } _computeLabels(labelsRecord) { const labels = labelsRecord.base; @@ -103,7 +110,7 @@ this.push(path, {label, icon, style, labelInfo}); } - }, + } /** * @param {Object} labelInfo @@ -114,7 +121,7 @@ if (labelInfo.approved) { return 'gr-icons:check'; } if (labelInfo.rejected) { return 'gr-icons:close'; } return 'gr-icons:hourglass'; - }, + } /** * @param {Object} labelInfo @@ -123,28 +130,30 @@ if (labelInfo.approved) { return 'approved'; } if (labelInfo.rejected) { return 'rejected'; } return ''; - }, + } _computeShowOptional(optionalFieldsRecord) { return optionalFieldsRecord.base.length ? '' : 'hidden'; - }, + } _computeLabelValue(value) { return (value > 0 ? '+' : '') + value; - }, + } _computeShowHideIcon(showOptionalLabels) { return showOptionalLabels ? - 'gr-icons:expand-less' : - 'gr-icons:expand-more'; - }, + 'gr-icons:expand-less' : + 'gr-icons:expand-more'; + } _computeSectionClass(show) { return show ? '' : 'hidden'; - }, + } _handleShowHide(e) { this._showOptionalLabels = !this._showOptionalLabels; - }, - }); + } + } + + customElements.define(GrChangeRequirements.is, GrChangeRequirements); })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html index 2ceac39..242fe2c 100644 --- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html +++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-requirements</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html index 45df471..c9f881f 100644 --- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html +++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -51,6 +51,7 @@ <link rel="import" href="../gr-included-in-dialog/gr-included-in-dialog.html"> <link rel="import" href="../gr-messages-list/gr-messages-list.html"> <link rel="import" href="../gr-related-changes-list/gr-related-changes-list.html"> +<link rel="import" href="../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog.html"> <link rel="import" href="../gr-reply-dialog/gr-reply-dialog.html"> <link rel="import" href="../gr-thread-list/gr-thread-list.html"> <link rel="import" href="../gr-upload-help-dialog/gr-upload-help-dialog.html"> @@ -408,8 +409,6 @@ change-status="[[_change.status]]" commit-num="[[_commitInfo.commit]]" latest-patch-num="[[computeLatestPatchNum(_allPatchSets)]]" - reply-disabled="[[_replyDisabled]]" - reply-button-label="[[_replyButtonLabel]]" commit-message="[[_latestCommitMessage]]" edit-patchset-loaded="[[hasEditPatchsetLoaded(_patchRange.*)]]" edit-mode="[[_editMode]]" @@ -637,6 +636,11 @@ on-thread-list-modified="_handleReloadDiffComments"></gr-thread-list> </template> </div> + <gr-apply-fix-dialog + id="applyFixDialog" + prefs="[[_diffPrefs]]" + change="[[_change]]" + change-num="[[_changeNum]]"></gr-apply-fix-dialog> <gr-overlay id="downloadOverlay" with-backdrop> <gr-download-dialog id="downloadDialog"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js index e6ceea2..fa84b95 100644 --- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js +++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -63,9 +63,21 @@ const CHANGE_RELOAD_TIMING_LABEL = 'ChangeReloaded'; const SEND_REPLY_TIMING_LABEL = 'SendReply'; - Polymer({ - is: 'gr-change-view', - + /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.RESTClientMixin + */ + class GrChangeView extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + Gerrit.PatchSetBehavior, + Gerrit.RESTClientBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-change-view'; } /** * Fired when the title of the page should change. * @@ -84,230 +96,215 @@ * @event show-auth-required */ - properties: { + static get properties() { + return { /** * URL params passed from the router. */ - params: { - type: Object, - observer: '_paramsChanged', - }, - /** @type {?} */ - viewState: { - type: Object, - notify: true, - value() { return {}; }, - observer: '_viewStateChanged', - }, - backPage: String, - hasParent: Boolean, - keyEventTarget: { - type: Object, - value() { return document.body; }, - }, - disableEdit: { - type: Boolean, - value: false, - }, - disableDiffPrefs: { - type: Boolean, - value: false, - }, - _diffPrefsDisabled: { - type: Boolean, - computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)', - }, - _commentThreads: Array, - /** @type {?} */ - _serverConfig: { - type: Object, - observer: '_startUpdateCheckTimer', - }, - _diffPrefs: Object, - _numFilesShown: { - type: Number, - value: DEFAULT_NUM_FILES_SHOWN, - observer: '_numFilesShownChanged', - }, - _account: { - type: Object, - value: {}, - }, - _prefs: Object, - /** @type {?} */ - _changeComments: Object, - _canStartReview: { - type: Boolean, - computed: '_computeCanStartReview(_change)', - }, - _comments: Object, - /** @type {?} */ - _change: { - type: Object, - observer: '_changeChanged', - }, - _revisionInfo: { - type: Object, - computed: '_getRevisionInfo(_change)', - }, - /** @type {?} */ - _commitInfo: Object, - _currentRevision: { - type: Object, - computed: '_computeCurrentRevision(_change.current_revision, ' + - '_change.revisions)', - }, - _files: Object, - _changeNum: String, - _diffDrafts: { - type: Object, - value() { return {}; }, - }, - _editingCommitMessage: { - type: Boolean, - value: false, - }, - _hideEditCommitMessage: { - type: Boolean, - computed: '_computeHideEditCommitMessage(_loggedIn, ' + - '_editingCommitMessage, _change, _editMode)', - }, - _diffAgainst: String, - /** @type {?string} */ - _latestCommitMessage: { - type: String, - value: '', - }, - _lineHeight: Number, - _changeIdCommitMessageError: { - type: String, - computed: - '_computeChangeIdCommitMessageError(_latestCommitMessage, _change)', - }, + params: { + type: Object, + observer: '_paramsChanged', + }, /** @type {?} */ - _patchRange: { - type: Object, - }, - _filesExpanded: String, - _basePatchNum: String, - _selectedRevision: Object, - _currentRevisionActions: Object, - _allPatchSets: { - type: Array, - computed: 'computeAllPatchSets(_change, _change.revisions.*)', - }, - _loggedIn: { - type: Boolean, - value: false, - }, - _loading: Boolean, - /** @type {?} */ - _projectConfig: Object, - _rebaseOnCurrent: Boolean, - _replyButtonLabel: { - type: String, - value: 'Reply', - computed: '_computeReplyButtonLabel(_diffDrafts.*, _canStartReview)', - }, - _selectedPatchSet: String, - _shownFileCount: Number, - _initialLoadComplete: { - type: Boolean, - value: false, - }, - _replyDisabled: { - type: Boolean, - value: true, - computed: '_computeReplyDisabled(_serverConfig)', - }, - _changeStatus: { - type: String, - computed: 'changeStatusString(_change)', - }, - _changeStatuses: { - type: String, - computed: + viewState: { + type: Object, + notify: true, + value() { return {}; }, + observer: '_viewStateChanged', + }, + backPage: String, + hasParent: Boolean, + keyEventTarget: { + type: Object, + value() { return document.body; }, + }, + disableEdit: { + type: Boolean, + value: false, + }, + disableDiffPrefs: { + type: Boolean, + value: false, + }, + _diffPrefsDisabled: { + type: Boolean, + computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)', + }, + _commentThreads: Array, + /** @type {?} */ + _serverConfig: { + type: Object, + observer: '_startUpdateCheckTimer', + }, + _diffPrefs: Object, + _numFilesShown: { + type: Number, + value: DEFAULT_NUM_FILES_SHOWN, + observer: '_numFilesShownChanged', + }, + _account: { + type: Object, + value: {}, + }, + _prefs: Object, + /** @type {?} */ + _changeComments: Object, + _canStartReview: { + type: Boolean, + computed: '_computeCanStartReview(_change)', + }, + _comments: Object, + /** @type {?} */ + _change: { + type: Object, + observer: '_changeChanged', + }, + _revisionInfo: { + type: Object, + computed: '_getRevisionInfo(_change)', + }, + /** @type {?} */ + _commitInfo: Object, + _currentRevision: { + type: Object, + computed: '_computeCurrentRevision(_change.current_revision, ' + + '_change.revisions)', + }, + _files: Object, + _changeNum: String, + _diffDrafts: { + type: Object, + value() { return {}; }, + }, + _editingCommitMessage: { + type: Boolean, + value: false, + }, + _hideEditCommitMessage: { + type: Boolean, + computed: '_computeHideEditCommitMessage(_loggedIn, ' + + '_editingCommitMessage, _change, _editMode)', + }, + _diffAgainst: String, + /** @type {?string} */ + _latestCommitMessage: { + type: String, + value: '', + }, + _lineHeight: Number, + _changeIdCommitMessageError: { + type: String, + computed: + '_computeChangeIdCommitMessageError(_latestCommitMessage, _change)', + }, + /** @type {?} */ + _patchRange: { + type: Object, + }, + _filesExpanded: String, + _basePatchNum: String, + _selectedRevision: Object, + _currentRevisionActions: Object, + _allPatchSets: { + type: Array, + computed: 'computeAllPatchSets(_change, _change.revisions.*)', + }, + _loggedIn: { + type: Boolean, + value: false, + }, + _loading: Boolean, + /** @type {?} */ + _projectConfig: Object, + _rebaseOnCurrent: Boolean, + _replyButtonLabel: { + type: String, + value: 'Reply', + computed: '_computeReplyButtonLabel(_diffDrafts.*, _canStartReview)', + }, + _selectedPatchSet: String, + _shownFileCount: Number, + _initialLoadComplete: { + type: Boolean, + value: false, + }, + _replyDisabled: { + type: Boolean, + value: true, + computed: '_computeReplyDisabled(_serverConfig)', + }, + _changeStatus: { + type: String, + computed: 'changeStatusString(_change)', + }, + _changeStatuses: { + type: String, + computed: '_computeChangeStatusChips(_change, _mergeable, _submitEnabled)', - }, - _commitCollapsed: { - type: Boolean, - value: true, - }, - _relatedChangesCollapsed: { - type: Boolean, - value: true, - }, - /** @type {?number} */ - _updateCheckTimerHandle: Number, - _editMode: { - type: Boolean, - computed: '_computeEditMode(_patchRange.*, params.*)', - }, - _showRelatedToggle: { - type: Boolean, - value: false, - observer: '_updateToggleContainerClass', - }, - _parentIsCurrent: Boolean, - _submitEnabled: { - type: Boolean, - computed: '_isSubmitEnabled(_currentRevisionActions)', - }, + }, + _commitCollapsed: { + type: Boolean, + value: true, + }, + _relatedChangesCollapsed: { + type: Boolean, + value: true, + }, + /** @type {?number} */ + _updateCheckTimerHandle: Number, + _editMode: { + type: Boolean, + computed: '_computeEditMode(_patchRange.*, params.*)', + }, + _showRelatedToggle: { + type: Boolean, + value: false, + observer: '_updateToggleContainerClass', + }, + _parentIsCurrent: Boolean, + _submitEnabled: { + type: Boolean, + computed: '_isSubmitEnabled(_currentRevisionActions)', + }, - /** @type {?} */ - _mergeable: { - type: Boolean, - value: undefined, - }, - _showMessagesView: { - type: Boolean, - value: true, - }, - _showFileTabContent: { - type: Boolean, - value: true, - }, - /** @type {Array<string>} */ - _dynamicTabHeaderEndpoints: { - type: Array, - }, - _showPrimaryTabs: { - type: Boolean, - computed: '_computeShowPrimaryTabs(_dynamicTabHeaderEndpoints)', - }, - /** @type {Array<string>} */ - _dynamicTabContentEndpoints: { - type: Array, - }, - _selectedFilesTabPluginEndpoint: { - type: String, - }, - }, + /** @type {?} */ + _mergeable: { + type: Boolean, + value: undefined, + }, + _showMessagesView: { + type: Boolean, + value: true, + }, + _showFileTabContent: { + type: Boolean, + value: true, + }, + /** @type {Array<string>} */ + _dynamicTabHeaderEndpoints: { + type: Array, + }, + _showPrimaryTabs: { + type: Boolean, + computed: '_computeShowPrimaryTabs(_dynamicTabHeaderEndpoints)', + }, + /** @type {Array<string>} */ + _dynamicTabContentEndpoints: { + type: Array, + }, + _selectedFilesTabPluginEndpoint: { + type: String, + }, + }; + } - behaviors: [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - Gerrit.PatchSetBehavior, - Gerrit.RESTClientBehavior, - ], - - listeners: { - 'topic-changed': '_handleTopicChanged', - // When an overlay is opened in a mobile viewport, the overlay has a full - // screen view. When it has a full screen view, we do not want the - // background to be scrollable. This will eliminate background scroll by - // hiding most of the contents on the screen upon opening, and showing - // again upon closing. - 'fullscreen-overlay-opened': '_handleHideBackgroundContent', - 'fullscreen-overlay-closed': '_handleShowBackgroundContent', - 'diff-comments-modified': '_handleReloadCommentThreads', - }, - - observers: [ - '_labelsChanged(_change.labels.*)', - '_paramsAndChangeChanged(params, _change)', - '_patchNumChanged(_patchRange.patchNum)', - ], + static get observers() { + return [ + '_labelsChanged(_change.labels.*)', + '_paramsAndChangeChanged(params, _change)', + '_patchNumChanged(_patchRange.patchNum)', + ]; + } keyboardShortcuts() { return { @@ -325,9 +322,32 @@ [this.Shortcut.OPEN_DIFF_PREFS]: '_handleOpenDiffPrefsShortcut', [this.Shortcut.EDIT_TOPIC]: '_handleEditTopic', }; - }, + } + + created() { + super.created(); + + this.addEventListener('topic-changed', + () => this._handleTopicChanged()); + + this.addEventListener( + // When an overlay is opened in a mobile viewport, the overlay has a full + // screen view. When it has a full screen view, we do not want the + // background to be scrollable. This will eliminate background scroll by + // hiding most of the contents on the screen upon opening, and showing + // again upon closing. + 'fullscreen-overlay-opened', + () => this._handleHideBackgroundContent()); + + this.addEventListener('fullscreen-overlay-closed', + () => this._handleShowBackgroundContent()); + + this.addEventListener('diff-comments-modified', + () => this._handleReloadCommentThreads()); + } attached() { + super.attached(); this._getServerConfig().then(config => { this._serverConfig = config; }); @@ -347,8 +367,8 @@ Gerrit._endpoints.getDynamicEndpoints('change-view-tab-header'); this._dynamicTabContentEndpoints = Gerrit._endpoints.getDynamicEndpoints('change-view-tab-content'); - if (this._dynamicTabContentEndpoints.length - !== this._dynamicTabHeaderEndpoints.length) { + if (this._dynamicTabContentEndpoints.length !== + this._dynamicTabHeaderEndpoints.length) { console.warn('Different number of tab headers and tab content.'); } }).then(() => this._setPrimaryTab()); @@ -361,26 +381,31 @@ this._handleCommitMessageSave.bind(this)); this.addEventListener('editable-content-cancel', this._handleCommitMessageCancel.bind(this)); + this.addEventListener('open-fix-preview', + this._onOpenFixPreview.bind(this)); + this.addEventListener('close-fix-preview', + this._onCloseFixPreview.bind(this)); this.listen(window, 'scroll', '_handleScroll'); this.listen(document, 'visibilitychange', '_handleVisibilityChange'); - }, + } detached() { + super.detached(); this.unlisten(window, 'scroll', '_handleScroll'); this.unlisten(document, 'visibilitychange', '_handleVisibilityChange'); if (this._updateCheckTimerHandle) { this._cancelUpdateCheckTimer(); } - }, + } get messagesList() { return this.$$('gr-messages-list'); - }, + } get threadList() { return this.$$('gr-thread-list'); - }, + } /** * @param {boolean=} opt_reset @@ -397,7 +422,15 @@ this.set('viewState.diffMode', 'SIDE_BY_SIDE'); } }); - }, + } + + _onOpenFixPreview(e) { + this.$.applyFixDialog.open(e); + } + + _onCloseFixPreview(e) { + this._reload(); + } _handleToggleDiffMode(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -409,14 +442,15 @@ } else { this.$.fileListHeader.setDiffViewMode(DiffViewMode.SIDE_BY_SIDE); } - }, + } _handleCommentTabChange() { this._showMessagesView = this.$.commentTabs.selected === 0; - }, + } _handleFileTabChange(e) { - const selectedIndex = this.$$('#primaryTabs').selected; + const selectedIndex = this.shadowRoot + .querySelector('#primaryTabs').selected; this._showFileTabContent = selectedIndex === 0; // Initial tab is the static files list. const newSelectedTab = @@ -429,7 +463,7 @@ this.$.reporting.reportInteraction('tab-changed', `tabname: ${tabName}, source: ${source}`); } - }, + } _handleShowTab(e) { const idx = this._dynamicTabContentEndpoints.indexOf(e.detail.tab); @@ -437,15 +471,15 @@ console.warn(e.detail.tab + ' tab not found'); return; } - this.$$('#primaryTabs').selected = idx + 1; - this.$$('#primaryTabs').scrollIntoView(); + this.shadowRoot.querySelector('#primaryTabs').selected = idx + 1; + this.shadowRoot.querySelector('#primaryTabs').scrollIntoView(); this.$.reporting.reportInteraction('show-tab', e.detail.tab); - }, + } _handleEditCommitMessage(e) { this._editingCommitMessage = true; this.$.commitMessageEditor.focusTextarea(); - }, + } _handleCommitMessageSave(e) { // Trim trailing whitespace from each line. @@ -456,25 +490,25 @@ this.$.commitMessageEditor.disabled = true; this.$.restAPI.putChangeCommitMessage( this._changeNum, message).then(resp => { - this.$.commitMessageEditor.disabled = false; - if (!resp.ok) { return; } + this.$.commitMessageEditor.disabled = false; + if (!resp.ok) { return; } - this._latestCommitMessage = this._prepareCommitMsgForLinkify( - message); - this._editingCommitMessage = false; - this._reloadWindow(); - }).catch(err => { - this.$.commitMessageEditor.disabled = false; - }); - }, + this._latestCommitMessage = this._prepareCommitMsgForLinkify( + message); + this._editingCommitMessage = false; + this._reloadWindow(); + }).catch(err => { + this.$.commitMessageEditor.disabled = false; + }); + } _reloadWindow() { window.location.reload(); - }, + } _handleCommitMessageCancel(e) { this._editingCommitMessage = false; - }, + } _computeChangeStatusChips(change, mergeable, submitEnabled) { // Polymer 2: check for undefined @@ -498,7 +532,7 @@ submitEnabled: !!submitEnabled, }; return this.changeStatuses(change, options); - }, + } _computeHideEditCommitMessage(loggedIn, editing, change, editMode) { if (!loggedIn || editing || @@ -508,7 +542,7 @@ } return false; - }, + } _handleReloadCommentThreads() { // Get any new drafts that have been saved in the diff view and show @@ -518,7 +552,7 @@ .map(c => Object.assign({}, c)); Polymer.dom.flush(); }); - }, + } _handleReloadDiffComments(e) { // Keeps the file list counts updated. @@ -529,7 +563,7 @@ e.detail.path); Polymer.dom.flush(); }); - }, + } _computeTotalCommentCounts(unresolvedCount, changeComments) { if (!changeComments) return undefined; @@ -543,7 +577,7 @@ // Add a comma and space if both unresolved and draft comments exist. (unresolvedString && draftString ? ', ' : '') + draftString; - }, + } _handleCommentSave(e) { const draft = e.detail.comment; @@ -575,7 +609,7 @@ return (c1.line || -1) - (c2.line || -1); }); this._diffDrafts = diffDrafts; - }, + } _handleCommentDiscard(e) { const draft = e.detail.comment; @@ -609,16 +643,16 @@ delete diffDrafts[draft.path]; } this._diffDrafts = diffDrafts; - }, + } _handleReplyTap(e) { e.preventDefault(); this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY); - }, + } _handleOpenDiffPrefs() { this.$.fileList.openDiffPrefs(); - }, + } _handleOpenIncludedInDialog() { this.$.includedInDialog.loadData().then(() => { @@ -626,11 +660,11 @@ this.$.includedInOverlay.refit(); }); this.$.includedInOverlay.open(); - }, + } _handleIncludedInDialogClose(e) { this.$.includedInOverlay.close(); - }, + } _handleOpenDownloadDialog() { this.$.downloadOverlay.open().then(() => { @@ -638,19 +672,19 @@ .setFocusStops(this.$.downloadDialog.getFocusStops()); this.$.downloadDialog.focus(); }); - }, + } _handleDownloadDialogClose(e) { this.$.downloadOverlay.close(); - }, + } _handleOpenUploadHelpDialog(e) { this.$.uploadHelpOverlay.open(); - }, + } _handleCloseUploadHelpDialog(e) { this.$.uploadHelpOverlay.close(); - }, + } _handleMessageReply(e) { const msg = e.detail.message.message; @@ -658,33 +692,33 @@ line => { return '> ' + line; }).join('\n') + '\n\n'; this.$.replyDialog.quote = quoteStr; this._openReplyDialog(this.$.replyDialog.FocusTarget.BODY); - }, + } _handleHideBackgroundContent() { this.$.mainContent.classList.add('overlayOpen'); - }, + } _handleShowBackgroundContent() { this.$.mainContent.classList.remove('overlayOpen'); - }, + } _handleReplySent(e) { this.$.replyOverlay.close(); this._reload().then(() => { this.$.reporting.timeEnd(SEND_REPLY_TIMING_LABEL); }); - }, + } _handleReplyCancel(e) { this.$.replyOverlay.close(); - }, + } _handleReplyAutogrow(e) { // If the textarea resizes, we need to re-fit the overlay. this.debounce('reply-overlay-refit', () => { this.$.replyOverlay.refit(); }, REPLY_REFIT_DEBOUNCE_INTERVAL_MS); - }, + } _handleShowReplyDialog(e) { let target = this.$.replyDialog.FocusTarget.REVIEWERS; @@ -692,25 +726,25 @@ target = this.$.replyDialog.FocusTarget.CCS; } this._openReplyDialog(target); - }, + } _handleScroll() { this.debounce('scroll', () => { this.viewState.scrollTop = document.body.scrollTop; }, 150); - }, + } _setShownFiles(e) { this._shownFileCount = e.detail.length; - }, + } _expandAllDiffs() { this.$.fileList.expandAllDiffs(); - }, + } _collapseAllDiffs() { this.$.fileList.collapseAllDiffs(); - }, + } _paramsChanged(value) { // Change the content of the comment tabs back to messages list, but @@ -764,7 +798,7 @@ this._reload(true).then(() => { this._performPostLoadTasks(); }); - }, + } _sendShowChangeEvent() { this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, { @@ -772,15 +806,15 @@ patchNum: this._patchRange.patchNum, info: {mergeable: this._mergeable}, }); - }, + } _setPrimaryTab() { // Selected has to be set after the paper-tabs are visible because // the selected underline depends on calculations made by the browser. this.$.commentTabs.selected = 0; - const primaryTabs = this.$$('#primaryTabs'); + const primaryTabs = this.shadowRoot.querySelector('#primaryTabs'); if (primaryTabs) primaryTabs.selected = 0; - }, + } _performPostLoadTasks() { this._maybeShowReplyDialog(); @@ -799,7 +833,7 @@ } this._initialLoadComplete = true; }); - }, + } _paramsAndChangeChanged(value, change) { // Polymer 2: check for undefined @@ -815,16 +849,16 @@ patchRangeState.patchNum !== this._patchRange.patchNum) { this._resetFileListViewState(); } - }, + } _viewStateChanged(viewState) { this._numFilesShown = viewState.numFilesShown ? - viewState.numFilesShown : DEFAULT_NUM_FILES_SHOWN; - }, + viewState.numFilesShown : DEFAULT_NUM_FILES_SHOWN; + } _numFilesShownChanged(numFilesShown) { this.viewState.numFilesShown = numFilesShown; - }, + } _handleMessageAnchorTap(e) { const hash = MSG_PREFIX + e.detail.id; @@ -832,18 +866,18 @@ this._patchRange.patchNum, this._patchRange.basePatchNum, this._editMode, hash); history.replaceState(null, '', url); - }, + } _maybeScrollToMessage(hash) { if (hash.startsWith(MSG_PREFIX)) { this.messagesList.scrollToMessage(hash.substr(MSG_PREFIX.length)); } - }, + } _getLocationSearch() { // Not inlining to make it easier to test. return window.location.search; - }, + } _getUrlParameter(param) { const pageURL = this._getLocationSearch().substring(1); @@ -855,7 +889,7 @@ } } return null; - }, + } _maybeShowRevertDialog() { Gerrit.awaitPluginsLoaded() @@ -871,7 +905,7 @@ this.$.actions.showRevertDialog(); } }); - }, + } _maybeShowReplyDialog() { this._getLoggedIn().then(loggedIn => { @@ -885,7 +919,7 @@ this.set('viewState.showReplyDialog', false); } }); - }, + } _resetFileListViewState() { this.set('viewState.selectedFileIndex', 0); @@ -899,7 +933,7 @@ } this.set('viewState.changeNum', this._changeNum); this.set('viewState.patchRange', this._patchRange); - }, + } _changeChanged(change) { if (!change || !this._patchRange || !this._allPatchSets) { return; } @@ -915,11 +949,12 @@ const title = change.subject + ' (' + change.change_id.substr(0, 9) + ')'; this.fire('title-change', {title}); - }, + } /** * Gets base patch number, if it is a parent try and decide from * preference whether to default to `auto merge`, `Parent 1` or `PARENT`. + * * @param {Object} change * @param {Object} patchRange * @return {number|string} @@ -937,7 +972,7 @@ // check that there is at least 2 parents otherwise fall back to 1, // which means there is only one parent. const parentCount = parentCounts.hasOwnProperty(1) ? - parentCounts[1] : 1; + parentCounts[1] : 1; const preferFirst = this._prefs && this._prefs.default_base_for_merges === 'FIRST_PARENT'; @@ -947,19 +982,19 @@ } return 'PARENT'; - }, + } _computeShowPrimaryTabs(dynamicTabHeaderEndpoints) { return dynamicTabHeaderEndpoints && dynamicTabHeaderEndpoints.length > 0; - }, + } _computeChangeUrl(change) { return Gerrit.Nav.getUrlForChange(change); - }, + } _computeShowCommitInfo(changeStatus, current_revision) { return changeStatus === 'Merged' && current_revision; - }, + } _computeMergedCommitInfo(current_revision, revisions) { const rev = revisions[current_revision]; @@ -968,11 +1003,11 @@ // in <gr-commit-info>. @see Issue 5337 if (!rev.commit.commit) { rev.commit.commit = current_revision; } return rev.commit; - }, + } _computeChangeIdClass(displayChangeId) { return displayChangeId === CHANGE_ID_ERROR.MISMATCH ? 'warning' : ''; - }, + } _computeTitleAttributeWarning(displayChangeId) { if (displayChangeId === CHANGE_ID_ERROR.MISMATCH) { @@ -980,7 +1015,7 @@ } else if (displayChangeId === CHANGE_ID_ERROR.MISSING) { return 'No Change-Id in commit message'; } - }, + } _computeChangeIdCommitMessageError(commitMessage, change) { // Polymer 2: check for undefined @@ -1010,11 +1045,11 @@ } // There is no change-id in the commit message. return CHANGE_ID_ERROR.MISSING; - }, + } _computeLabelNames(labels) { return Object.keys(labels).sort(); - }, + } _computeLabelValues(labelName, labels) { const result = []; @@ -1039,7 +1074,7 @@ } } return result; - }, + } _computeReplyButtonLabel(changeRecord, canStartReview) { // Polymer 2: check for undefined @@ -1061,7 +1096,7 @@ label += ' (' + draftCount + ')'; } return label; - }, + } _handleOpenReplyDialog(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -1077,7 +1112,7 @@ e.preventDefault(); this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY); }); - }, + } _handleOpenDownloadDialogShortcut(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -1085,7 +1120,7 @@ e.preventDefault(); this.$.downloadOverlay.open(); - }, + } _handleEditTopic(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -1093,13 +1128,13 @@ e.preventDefault(); this.$.metadata.editTopic(); - }, + } _handleRefreshChange(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } e.preventDefault(); Gerrit.Nav.navigateToChange(this._change); - }, + } _handleToggleChangeStar(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -1107,7 +1142,7 @@ e.preventDefault(); this.$.changeStar.toggleStar(); - }, + } _handleUpToDashboard(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -1115,7 +1150,7 @@ e.preventDefault(); this._determinePageBack(); - }, + } _handleExpandAllMessages(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -1123,7 +1158,7 @@ e.preventDefault(); this.messagesList.handleExpandCollapse(true); - }, + } _handleCollapseAllMessages(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -1131,7 +1166,7 @@ e.preventDefault(); this.messagesList.handleExpandCollapse(false); - }, + } _handleOpenDiffPrefsShortcut(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -1141,14 +1176,14 @@ e.preventDefault(); this.$.fileList.openDiffPrefs(); - }, + } _determinePageBack() { // Default backPage to root if user came to change view page // via an email link, etc. Gerrit.Nav.navigateToRelativeUrl(this.backPage || Gerrit.Nav.getUrlForRoot()); - }, + } _handleLabelRemoved(splices, path) { for (const splice of splices) { @@ -1163,7 +1198,7 @@ } } } - }, + } _labelsChanged(changeRecord) { if (!changeRecord) { return; } @@ -1174,7 +1209,7 @@ this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.LABEL_CHANGE, { change: this._change, }); - }, + } /** * @param {string=} opt_section @@ -1186,7 +1221,7 @@ Polymer.dom.flush(); this.$.replyOverlay.center(); }); - }, + } _handleReloadChange(e) { return this._reload().then(() => { @@ -1197,19 +1232,19 @@ Gerrit.Nav.navigateToChange(this._change); } }); - }, + } _handleGetChangeDetailError(response) { this.fire('page-error', {response}); - }, + } _getLoggedIn() { return this.$.restAPI.getLoggedIn(); - }, + } _getServerConfig() { return this.$.restAPI.getConfig(); - }, + } _getProjectConfig() { if (!this._change) return; @@ -1217,18 +1252,18 @@ config => { this._projectConfig = config; }); - }, + } _getPreferences() { return this.$.restAPI.getPreferences(); - }, + } _prepareCommitMsgForLinkify(msg) { // TODO(wyatta) switch linkify sequence, see issue 5526. // This is a zero-with space. It is added to prevent the linkify library // from including R= or CC= as part of the email address. return msg.replace(REVIEWERS_REGEX, '$1=\u200B'); - }, + } /** * Utility function to make the necessary modifications to a change in the @@ -1258,7 +1293,7 @@ change.revisions[edit.commit.commit].actions = change.revisions[edit.base_revision].actions; } - }, + } _getChangeDetail() { const detailCompletes = this.$.restAPI.getChangeDetail( @@ -1288,7 +1323,6 @@ this._latestCommitMessage = null; } - const lineHeight = getComputedStyle(this).lineHeight; // Slice returns a number as a string, convert to an int. @@ -1313,25 +1347,25 @@ parseInt(this._patchRange.patchNum, 10)); } }); - }, + } _isSubmitEnabled(revisionActions) { return !!(revisionActions && revisionActions.submit && revisionActions.submit.enabled); - }, + } _getEdit() { return this.$.restAPI.getChangeEdit(this._changeNum, true); - }, + } _getLatestCommitMessage() { return this.$.restAPI.getChangeCommitInfo(this._changeNum, this.computeLatestPatchNum(this._allPatchSets)).then(commitInfo => { - if (!commitInfo) return Promise.resolve(); - this._latestCommitMessage = + if (!commitInfo) return Promise.resolve(); + this._latestCommitMessage = this._prepareCommitMsgForLinkify(commitInfo.message); - }); - }, + }); + } _getLatestRevisionSHA(change) { if (change.current_revision) { @@ -1350,7 +1384,7 @@ } } return latestRev; - }, + } _getCommitInfo() { return this.$.restAPI.getChangeCommitInfo( @@ -1358,13 +1392,13 @@ commitInfo => { this._commitInfo = commitInfo; }); - }, + } _reloadDraftsWithCallback(e) { return this._reloadDrafts().then(() => { return e.detail.resolve(); }); - }, + } /** * Fetches a new changeComment object, and data for all types of comments @@ -1376,9 +1410,9 @@ this._changeComments = comments; this._diffDrafts = Object.assign({}, this._changeComments.drafts); this._commentThreads = this._changeComments.getAllThreadsForChange() - .map(c => Object.assign({}, c)); + .map(c => Object.assign({}, c)); }); - }, + } /** * Fetches a new changeComment object, but only updated data for drafts is @@ -1390,10 +1424,11 @@ this._changeComments = comments; this._diffDrafts = Object.assign({}, this._changeComments.drafts); }); - }, + } /** * Reload the change. + * * @param {boolean=} opt_isLocationChange Reloads the related changes * when true and ends reporting events that started on location change. * @return {Promise} A promise that resolves when the core data has loaded. @@ -1438,7 +1473,7 @@ let coreDataPromise; // If the patch number is specified - if (this._patchRange.patchNum) { + if (this._patchRange && this._patchRange.patchNum) { // Because a specific patchset is specified, reload the resources that // are keyed by patch number or patch range. const patchResourcesLoaded = this._reloadPatchNumDependentResources(); @@ -1497,7 +1532,7 @@ }); return coreDataPromise; - }, + } /** * Kicks off requests for resources that rely on the patch range @@ -1508,7 +1543,7 @@ this._getCommitInfo(), this.$.fileList.reload(), ]); - }, + } _getMergeability() { if (!this._change) { @@ -1528,59 +1563,59 @@ return this.$.restAPI.getMergeable(this._changeNum).then(m => { this._mergeable = m.mergeable; }); - }, + } _computeCanStartReview(change) { return !!(change.actions && change.actions.ready && change.actions.ready.enabled); - }, + } - _computeReplyDisabled() { return false; }, + _computeReplyDisabled() { return false; } _computeChangePermalinkAriaLabel(changeNum) { return 'Change ' + changeNum; - }, + } _computeCommitClass(collapsed, commitMessage) { if (this._computeCommitToggleHidden(commitMessage)) { return ''; } return collapsed ? 'collapsed' : ''; - }, + } _computeRelatedChangesClass(collapsed) { return collapsed ? 'collapsed' : ''; - }, + } _computeCollapseText(collapsed) { // Symbols are up and down triangles. return collapsed ? '\u25bc Show more' : '\u25b2 Show less'; - }, + } _toggleCommitCollapsed() { this._commitCollapsed = !this._commitCollapsed; if (this._commitCollapsed) { window.scrollTo(0, 0); } - }, + } _toggleRelatedChangesCollapsed() { this._relatedChangesCollapsed = !this._relatedChangesCollapsed; if (this._relatedChangesCollapsed) { window.scrollTo(0, 0); } - }, + } _computeCommitToggleHidden(commitMessage) { if (!commitMessage) { return true; } return commitMessage.split('\n').length < MIN_LINES_FOR_COMMIT_COLLAPSE; - }, + } _getOffsetHeight(element) { return element.offsetHeight; - }, + } _getScrollHeight(element) { return element.scrollHeight; - }, + } /** * Get the line height of an element to the nearest integer. @@ -1588,7 +1623,7 @@ _getLineHeight(element) { const lineHeightStr = getComputedStyle(element).lineHeight; return Math.round(lineHeightStr.slice(0, lineHeightStr.length - 2)); - }, + } /** * New max height for the related changes section, shorter than the existing @@ -1647,7 +1682,7 @@ } this.updateStyles(stylesToUpdate); - }, + } _computeShowRelatedToggle() { // Make sure the max height has been applied, since there is now content @@ -1667,7 +1702,7 @@ return this._showRelatedToggle = true; } this._showRelatedToggle = false; - }, + } _updateToggleContainerClass(showRelatedToggle) { if (showRelatedToggle) { @@ -1675,7 +1710,7 @@ } else { this.$.relatedChangesToggle.classList.remove('showToggle'); } - }, + } _startUpdateCheckTimer() { if (!this._serverConfig || @@ -1718,14 +1753,14 @@ }); }); }, this._serverConfig.change.update_delay * 1000); - }, + } _cancelUpdateCheckTimer() { if (this._updateCheckTimerHandle) { this.cancelAsync(this._updateCheckTimerHandle); } this._updateCheckTimerHandle = null; - }, + } _handleVisibilityChange() { if (document.hidden && this._updateCheckTimerHandle) { @@ -1733,17 +1768,17 @@ } else if (!this._updateCheckTimerHandle) { this._startUpdateCheckTimer(); } - }, + } _handleTopicChanged() { this.$.relatedChanges.reload(); - }, + } _computeHeaderClass(editMode) { const classes = ['header']; if (editMode) { classes.push('editMode'); } return classes.join(' '); - }, + } _computeEditMode(patchRangeRecord, paramsRecord) { if ([patchRangeRecord, paramsRecord].some(arg => arg === undefined)) { @@ -1754,7 +1789,7 @@ const patchRange = patchRangeRecord.base || {}; return this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME); - }, + } _handleFileActionTap(e) { e.preventDefault(); @@ -1776,23 +1811,24 @@ controls.openRestoreDialog(path); break; } - }, + } _computeCommitMessageKey(number, revision) { return `c${number}_rev${revision}`; - }, + } _patchNumChanged(patchNumStr) { if (!this._selectedRevision) { return; } - const patchNum = parseInt(patchNumStr, 10); + // If patchNumStr is"edit", then patchNum is undefined hence an OR + const patchNum = parseInt(patchNumStr, 10) || patchNumStr; if (patchNum === this._selectedRevision._number) { return; } this._selectedRevision = Object.values(this._change.revisions).find( revision => revision._number === patchNum); - }, + } /** * If an edit exists already, load it. Otherwise, toggle edit mode via the @@ -1800,7 +1836,7 @@ */ _handleEditTap() { const editInfo = Object.values(this._change.revisions).find(info => - info._number === this.EDIT_NAME); + info._number === this.EDIT_NAME); if (editInfo) { Gerrit.Nav.navigateToChange(this._change, this.EDIT_NAME); @@ -1815,31 +1851,33 @@ patchNum = this._patchRange.patchNum; } Gerrit.Nav.navigateToChange(this._change, patchNum, null, true); - }, + } _handleStopEditTap() { Gerrit.Nav.navigateToChange(this._change, this._patchRange.patchNum); - }, + } _resetReplyOverlayFocusStops() { this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops()); - }, + } _handleToggleStar(e) { this.$.restAPI.saveChangeStarred(e.detail.change._number, e.detail.starred); - }, + } _getRevisionInfo(change) { return new Gerrit.RevisionInfo(change); - }, + } _computeCurrentRevision(currentRevision, revisions) { return currentRevision && revisions && revisions[currentRevision]; - }, + } _computeDiffPrefsDisabled(disableDiffPrefs, loggedIn) { return disableDiffPrefs || !loggedIn; - }, - }); + } + } + + customElements.define(GrChangeView.is, GrChangeView); })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html index f7d1a18..b80b51e 100644 --- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html +++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-view</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -176,7 +176,7 @@ assert.isFalse(element.$.replyOverlay.opened); assert(openSpy.lastCall.calledWithExactly( element.$.replyDialog.FocusTarget.ANY), - '_openReplyDialog should have been passed ANY'); + '_openReplyDialog should have been passed ANY'); assert.equal(openSpy.callCount, 1); done(); }); @@ -1046,7 +1046,7 @@ MockInteractions.tap(element.$.replyBtn); assert(openStub.lastCall.calledWithExactly( element.$.replyDialog.FocusTarget.ANY), - '_openReplyDialog should have been passed ANY'); + '_openReplyDialog should have been passed ANY'); assert.equal(openStub.callCount, 1); }); @@ -1058,7 +1058,7 @@ {message: {message: 'text'}}); assert(openStub.lastCall.calledWithExactly( element.$.replyDialog.FocusTarget.BODY), - '_openReplyDialog should have been passed BODY'); + '_openReplyDialog should have been passed BODY'); assert.equal(openStub.callCount, 1); done(); }); @@ -1362,7 +1362,6 @@ '96px'); }); - suite('update checks', () => { setup(() => { sandbox.spy(element, '_startUpdateCheckTimer'); @@ -1483,7 +1482,7 @@ test('_computeEditMode', () => { const callCompute = (range, params) => - element._computeEditMode({base: range}, {base: params}); + element._computeEditMode({base: range}, {base: params}); assert.isFalse(callCompute({}, {})); assert.isTrue(callCompute({}, {edit: true})); assert.isFalse(callCompute({basePatchNum: 'PARENT', patchNum: 1}, {})); @@ -1618,6 +1617,30 @@ }); }); + test('_selectedRevision is assigned when patchNum is edit', () => { + const revision1 = {_number: 1, commit: {parents: []}}; + const revision2 = {_number: 2, commit: {parents: []}}; + const revision3 = {_number: 'edit', commit: {parents: []}}; + sandbox.stub(element.$.restAPI, 'getChangeDetail').returns( + Promise.resolve({ + revisions: { + aaa: revision1, + bbb: revision2, + ccc: revision3, + }, + labels: {}, + actions: {}, + current_revision: 'ccc', + change_id: 'loremipsumdolorsitamet', + })); + sandbox.stub(element, '_getEdit').returns(Promise.resolve()); + sandbox.stub(element, '_getPreferences').returns(Promise.resolve({})); + element._patchRange = {patchNum: 'edit'}; + return element._getChangeDetail().then(() => { + assert.strictEqual(element._selectedRevision, revision3); + }); + }); + test('_sendShowChangeEvent', () => { element._change = {labels: {}}; element._patchRange = {patchNum: 4}; @@ -1703,7 +1726,7 @@ element._patchRange = {patchNum: 1}; element.$.actions.dispatchEvent(new CustomEvent('stop-edit-tap', - {bubbles: false})); + {bubbles: false})); }); suite('plugin endpoints', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js index cbc7e42..3de51a7 100644 --- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js +++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -16,46 +16,54 @@ */ (function() { 'use strict'; - Polymer({ - is: 'gr-comment-list', - behaviors: [ - Gerrit.BaseUrlBehavior, - Gerrit.PathListBehavior, - Gerrit.URLEncodingBehavior, - ], + /** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.PathListMixin + * @appliesMixin Gerrit.URLEncodingMixin + */ + class GrCommentList extends Polymer.mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.PathListBehavior, + Gerrit.URLEncodingBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-comment-list'; } - properties: { - changeNum: Number, - comments: Object, - patchNum: Number, - projectName: String, - /** @type {?} */ - projectConfig: Object, - }, + static get properties() { + return { + changeNum: Number, + comments: Object, + patchNum: Number, + projectName: String, + /** @type {?} */ + projectConfig: Object, + }; + } _computeFilesFromComments(comments) { const arr = Object.keys(comments || {}); return arr.sort(this.specialFilePathCompare); - }, + } _isOnParent(comment) { return comment.side === 'PARENT'; - }, + } _computeDiffLineURL(file, changeNum, patchNum, comment) { const basePatchNum = comment.hasOwnProperty('parent') ? - -comment.parent : null; + -comment.parent : null; return Gerrit.Nav.getUrlForDiffById(this.changeNum, this.projectName, file, patchNum, basePatchNum, comment.line, this._isOnParent(comment)); - }, + } _computeCommentsForFile(comments, file) { // Changes are not picked up by the dom-repeat due to the array instance // identity not changing even when it has elements added/removed from it. return (comments[file] || []).slice(); - }, + } _computePatchDisplayName(comment) { if (this._isOnParent(comment)) { @@ -65,6 +73,8 @@ return `PS${comment.patch_set}, `; } return ''; - }, - }); + } + } + + customElements.define(GrCommentList.is, GrCommentList); })();
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html index c18ae8d..97be9b7 100644 --- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-comment-list</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js index e2fcdff..2a3fe89 100644 --- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js +++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
@@ -17,23 +17,27 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-commit-info', + class GrCommitInfo extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-commit-info'; } - properties: { - change: Object, - /** @type {?} */ - commitInfo: Object, - serverConfig: Object, - _showWebLink: { - type: Boolean, - computed: '_computeShowWebLink(change, commitInfo, serverConfig)', - }, - _webLink: { - type: String, - computed: '_computeWebLink(change, commitInfo, serverConfig)', - }, - }, + static get properties() { + return { + change: Object, + /** @type {?} */ + commitInfo: Object, + serverConfig: Object, + _showWebLink: { + type: Boolean, + computed: '_computeShowWebLink(change, commitInfo, serverConfig)', + }, + _webLink: { + type: String, + computed: '_computeWebLink(change, commitInfo, serverConfig)', + }, + }; + } _getWeblink(change, commitInfo, config) { return Gerrit.Nav.getPatchSetWeblink( @@ -43,7 +47,7 @@ weblinks: commitInfo.web_links, config, }); - }, + } _computeShowWebLink(change, commitInfo, serverConfig) { // Polymer 2: check for undefined @@ -53,7 +57,7 @@ const weblink = this._getWeblink(change, commitInfo, serverConfig); return !!weblink && !!weblink.url; - }, + } _computeWebLink(change, commitInfo, serverConfig) { // Polymer 2: check for undefined @@ -63,12 +67,14 @@ const {url} = this._getWeblink(change, commitInfo, serverConfig) || {}; return url; - }, + } _computeShortHash(commitInfo) { const {name} = this._getWeblink(this.change, commitInfo, this.serverConfig) || {}; return name; - }, - }); + } + } + + customElements.define(GrCommitInfo.is, GrCommitInfo); })();
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html index f271a70..2c5b431 100644 --- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html +++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-commit-info</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -101,7 +101,6 @@ element.serverConfig), 'https://link-url'); }); - test('ignore web links that are neither gitweb nor gitiles', () => { const router = document.createElement('gr-router'); sandbox.stub(Gerrit.Nav, '_generateWeblinks',
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js index 524876e..eb6f5f3 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
@@ -17,9 +17,17 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-confirm-abandon-dialog', - + /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + */ + class GrConfirmAbandonDialog extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-confirm-abandon-dialog'; } /** * Fired when the confirm button is pressed. * @@ -32,41 +40,42 @@ * @event cancel */ - properties: { - message: String, - }, + static get properties() { + return { + message: String, + }; + } - behaviors: [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - ], - - keyBindings: { - 'ctrl+enter meta+enter': '_handleEnterKey', - }, + get keyBindings() { + return { + 'ctrl+enter meta+enter': '_handleEnterKey', + }; + } resetFocus() { this.$.messageInput.textarea.focus(); - }, + } _handleEnterKey(e) { this._confirm(); - }, + } _handleConfirmTap(e) { e.preventDefault(); e.stopPropagation(); this._confirm(); - }, + } _confirm() { this.fire('confirm', {reason: this.message}, {bubbles: false}); - }, + } _handleCancelTap(e) { e.preventDefault(); e.stopPropagation(); this.fire('cancel', null, {bubbles: false}); - }, - }); + } + } + + customElements.define(GrConfirmAbandonDialog.is, GrConfirmAbandonDialog); })();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html index cc4b80e..e9964c5 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-confirm-abandon-dialog</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js index a0da331..0b3639f 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
@@ -17,12 +17,15 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-confirm-cherrypick-conflict-dialog', - - behaviors: [ - Gerrit.FireBehavior, - ], + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrConfirmCherrypickConflictDialog extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-confirm-cherrypick-conflict-dialog'; } /** * Fired when the confirm button is pressed. @@ -40,12 +43,15 @@ e.preventDefault(); e.stopPropagation(); this.fire('confirm', null, {bubbles: false}); - }, + } _handleCancelTap(e) { e.preventDefault(); e.stopPropagation(); this.fire('cancel', null, {bubbles: false}); - }, - }); + } + } + + customElements.define(GrConfirmCherrypickConflictDialog.is, + GrConfirmCherrypickConflictDialog); })();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html index f411de4..557972a 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-confirm-cherrypick-conflict-dialog</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js index bf84003..6b01465 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
@@ -19,9 +19,15 @@ const SUGGESTIONS_LIMIT = 15; - Polymer({ - is: 'gr-confirm-cherrypick-dialog', - + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrConfirmCherrypickDialog extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-confirm-cherrypick-dialog'; } /** * Fired when the confirm button is pressed. * @@ -34,29 +40,29 @@ * @event cancel */ - properties: { - branch: String, - baseCommit: String, - changeStatus: String, - commitMessage: String, - commitNum: String, - message: String, - project: String, - _query: { - type: Function, - value() { - return this._getProjectBranchesSuggestions.bind(this); + static get properties() { + return { + branch: String, + baseCommit: String, + changeStatus: String, + commitMessage: String, + commitNum: String, + message: String, + project: String, + _query: { + type: Function, + value() { + return this._getProjectBranchesSuggestions.bind(this); + }, }, - }, - }, + }; + } - behaviors: [ - Gerrit.FireBehavior, - ], - - observers: [ - '_computeMessage(changeStatus, commitNum, commitMessage)', - ], + static get observers() { + return [ + '_computeMessage(changeStatus, commitNum, commitMessage)', + ]; + } _computeMessage(changeStatus, commitNum, commitMessage) { // Polymer 2: check for undefined @@ -74,23 +80,23 @@ newMessage += '(cherry picked from commit ' + commitNum + ')'; } this.message = newMessage; - }, + } _handleConfirmTap(e) { e.preventDefault(); e.stopPropagation(); this.fire('confirm', null, {bubbles: false}); - }, + } _handleCancelTap(e) { e.preventDefault(); e.stopPropagation(); this.fire('cancel', null, {bubbles: false}); - }, + } resetFocus() { this.$.branchInput.focus(); - }, + } _getProjectBranchesSuggestions(input) { if (input.startsWith('refs/heads/')) { @@ -98,21 +104,24 @@ } return this.$.restAPI.getRepoBranches( input, this.project, SUGGESTIONS_LIMIT).then(response => { - const branches = []; - let branch; - for (const key in response) { - if (!response.hasOwnProperty(key)) { continue; } - if (response[key].ref.startsWith('refs/heads/')) { - branch = response[key].ref.substring('refs/heads/'.length); - } else { - branch = response[key].ref; - } - branches.push({ - name: branch, - }); - } - return branches; + const branches = []; + let branch; + for (const key in response) { + if (!response.hasOwnProperty(key)) { continue; } + if (response[key].ref.startsWith('refs/heads/')) { + branch = response[key].ref.substring('refs/heads/'.length); + } else { + branch = response[key].ref; + } + branches.push({ + name: branch, }); - }, - }); + } + return branches; + }); + } + } + + customElements.define(GrConfirmCherrypickDialog.is, + GrConfirmCherrypickDialog); })();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html index 22a2aba..42310fb 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-confirm-cherrypick-dialog</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js index 23123c3..d595f48 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
@@ -19,9 +19,15 @@ const SUGGESTIONS_LIMIT = 15; - Polymer({ - is: 'gr-confirm-move-dialog', - + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrConfirmMoveDialog extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-confirm-move-dialog'; } /** * Fired when the confirm button is pressed. * @@ -34,33 +40,31 @@ * @event cancel */ - properties: { - branch: String, - message: String, - project: String, - _query: { - type: Function, - value() { - return this._getProjectBranchesSuggestions.bind(this); + static get properties() { + return { + branch: String, + message: String, + project: String, + _query: { + type: Function, + value() { + return this._getProjectBranchesSuggestions.bind(this); + }, }, - }, - }, - - behaviors: [ - Gerrit.FireBehavior, - ], + }; + } _handleConfirmTap(e) { e.preventDefault(); e.stopPropagation(); this.fire('confirm', null, {bubbles: false}); - }, + } _handleCancelTap(e) { e.preventDefault(); e.stopPropagation(); this.fire('cancel', null, {bubbles: false}); - }, + } _getProjectBranchesSuggestions(input) { if (input.startsWith('refs/heads/')) { @@ -68,21 +72,23 @@ } return this.$.restAPI.getRepoBranches( input, this.project, SUGGESTIONS_LIMIT).then(response => { - const branches = []; - let branch; - for (const key in response) { - if (!response.hasOwnProperty(key)) { continue; } - if (response[key].ref.startsWith('refs/heads/')) { - branch = response[key].ref.substring('refs/heads/'.length); - } else { - branch = response[key].ref; - } - branches.push({ - name: branch, - }); - } - return branches; + const branches = []; + let branch; + for (const key in response) { + if (!response.hasOwnProperty(key)) { continue; } + if (response[key].ref.startsWith('refs/heads/')) { + branch = response[key].ref.substring('refs/heads/'.length); + } else { + branch = response[key].ref; + } + branches.push({ + name: branch, }); - }, - }); + } + return branches; + }); + } + } + + customElements.define(GrConfirmMoveDialog.is, GrConfirmMoveDialog); })();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html index 8d6e029..036950b 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-confirm-move-dialog</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js index 1b884e7..7169437 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
@@ -17,9 +17,10 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-confirm-rebase-dialog', - + class GrConfirmRebaseDialog extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-confirm-rebase-dialog'; } /** * Fired when the confirm button is pressed. * @@ -32,24 +33,28 @@ * @event cancel */ - properties: { - branch: String, - changeNumber: Number, - hasParent: Boolean, - rebaseOnCurrent: Boolean, - _text: String, - _query: { - type: Function, - value() { - return this._getChangeSuggestions.bind(this); + static get properties() { + return { + branch: String, + changeNumber: Number, + hasParent: Boolean, + rebaseOnCurrent: Boolean, + _text: String, + _query: { + type: Function, + value() { + return this._getChangeSuggestions.bind(this); + }, }, - }, - _recentChanges: Array, - }, + _recentChanges: Array, + }; + } - observers: [ - '_updateSelectedOption(rebaseOnCurrent, hasParent)', - ], + static get observers() { + return [ + '_updateSelectedOption(rebaseOnCurrent, hasParent)', + ]; + } // This is called by gr-change-actions every time the rebase dialog is // re-opened. Unlike other autocompletes that make a request with each @@ -71,36 +76,36 @@ this._recentChanges = changes; return this._recentChanges; }); - }, + } _getRecentChanges() { if (this._recentChanges) { return Promise.resolve(this._recentChanges); } return this.fetchRecentChanges(); - }, + } _getChangeSuggestions(input) { return this._getRecentChanges().then(changes => - this._filterChanges(input, changes)); - }, + this._filterChanges(input, changes)); + } _filterChanges(input, changes) { return changes.filter(change => change.name.includes(input) && change.value !== this.changeNumber); - }, + } _displayParentOption(rebaseOnCurrent, hasParent) { return hasParent && rebaseOnCurrent; - }, + } _displayParentUpToDateMsg(rebaseOnCurrent, hasParent) { return hasParent && !rebaseOnCurrent; - }, + } _displayTipOption(rebaseOnCurrent, hasParent) { return !(!rebaseOnCurrent && !hasParent); - }, + } /** * There is a subtle but important difference between setting the base to an @@ -115,7 +120,7 @@ // Change numbers will have their description appended by the // autocomplete. return this._text.split(':')[0]; - }, + } _handleConfirmTap(e) { e.preventDefault(); @@ -123,22 +128,22 @@ this.dispatchEvent(new CustomEvent('confirm', {detail: {base: this._getSelectedBase()}})); this._text = ''; - }, + } _handleCancelTap(e) { e.preventDefault(); e.stopPropagation(); this.dispatchEvent(new CustomEvent('cancel')); this._text = ''; - }, + } _handleRebaseOnOther() { this.$.parentInput.focus(); - }, + } _handleEnterChangeNumberClick() { this.$.rebaseOnOtherInput.checked = true; - }, + } /** * Sets the default radio button based on the state of the app and @@ -157,6 +162,8 @@ } else { this.$.rebaseOnOtherInput.checked = true; } - }, - }); + } + } + + customElements.define(GrConfirmRebaseDialog.is, GrConfirmRebaseDialog); })();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html index cd5b130..d801638 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-confirm-rebase-dialog</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -182,6 +182,11 @@ test('input text change triggers function', () => { sandbox.spy(element, '_getRecentChanges'); element.$.parentInput.noDebounce = true; + MockInteractions.pressAndReleaseKeyOn( + element.$.parentInput.$.input, + 13, + null, + 'enter'); element._text = '1'; assert.isTrue(element._getRecentChanges.calledOnce); element._text = '12';
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js index 662b64c..379226a 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
@@ -20,9 +20,15 @@ const ERR_COMMIT_NOT_FOUND = 'Unable to find the commit hash of this change.'; - Polymer({ - is: 'gr-confirm-revert-dialog', - + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrConfirmRevertDialog extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-confirm-revert-dialog'; } /** * Fired when the confirm button is pressed. * @@ -35,13 +41,11 @@ * @event cancel */ - properties: { - message: String, - }, - - behaviors: [ - Gerrit.FireBehavior, - ], + static get properties() { + return { + message: String, + }; + } populateRevertMessage(message, commitHash) { // Figure out what the revert title should be. @@ -55,18 +59,20 @@ this.message = `${revertTitle}\n\n${revertCommitText}\n\n` + `Reason for revert: <INSERT REASONING HERE>\n`; - }, + } _handleConfirmTap(e) { e.preventDefault(); e.stopPropagation(); this.fire('confirm', null, {bubbles: false}); - }, + } _handleCancelTap(e) { e.preventDefault(); e.stopPropagation(); this.fire('cancel', null, {bubbles: false}); - }, - }); + } + } + + customElements.define(GrConfirmRevertDialog.is, GrConfirmRevertDialog); })();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html index 6e41555..dbdfba2 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-confirm-revert-dialog</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.html new file mode 100644 index 0000000..0107c33 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.html
@@ -0,0 +1,73 @@ +<!-- +@license +Copyright (C) 2019 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> +<link rel="import" href="/bower_components/polymer/polymer.html"> +<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> +<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> +<link rel="import" href="../../../styles/shared-styles.html"> +<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> + +<dom-module id="gr-confirm-revert-submission-dialog"> + <template> + <!-- TODO(taoalpha): move all shared styles to a style module. --> + <style include="shared-styles"> + :host { + display: block; + } + :host([disabled]) { + opacity: .5; + pointer-events: none; + } + label { + cursor: pointer; + display: block; + width: 100%; + } + iron-autogrow-textarea { + font-family: var(--monospace-font-family); + padding: 0; + width: 73ch; /* Add a char to account for the border. */ + + --iron-autogrow-textarea { + border: 1px solid var(--border-color); + box-sizing: border-box; + font-family: var(--monospace-font-family); + } + } + </style> + <gr-dialog + confirm-label="Revert Submission" + on-confirm="_handleConfirmTap" + on-cancel="_handleCancelTap"> + <div class="header" slot="header">Revert Submission</div> + <div class="main" slot="main"> + <label for="messageInput"> + Revert Commit Message + </label> + <iron-autogrow-textarea + id="messageInput" + class="message" + autocomplete="on" + max-rows="15" + bind-value="{{message}}"></iron-autogrow-textarea> + </div> + </gr-dialog> + <gr-js-api-interface id="jsAPI"></gr-js-api-interface> + </template> + <script src="gr-confirm-revert-submission-dialog.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js new file mode 100644 index 0000000..51698be --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js
@@ -0,0 +1,99 @@ +/** + * @license + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +(function() { + 'use strict'; + + const ERR_COMMIT_NOT_FOUND = + 'Unable to find the commit hash of this change.'; + const CHANGE_SUBJECT_LIMIT = 50; + + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrConfirmRevertSubmissionDialog extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-confirm-revert-submission-dialog'; } + /** + * Fired when the confirm button is pressed. + * + * @event confirm + */ + + /** + * Fired when the cancel button is pressed. + * + * @event cancel + */ + + static get properties() { + return { + message: String, + commitMessage: String, + }; + } + + getTrimmedChangeSubject(subject) { + if (!subject) return ''; + if (subject.length < CHANGE_SUBJECT_LIMIT) return subject; + return subject.substring(0, CHANGE_SUBJECT_LIMIT) + '...'; + } + + _modifyRevertSubmissionMsg(change) { + return this.$.jsAPI.modifyRevertSubmissionMsg(change, + this.message, this.commitMessage); + } + + populateRevertSubmissionMessage(message, change, changes) { + // Follow the same convention of the revert + const commitHash = change.current_revision; + if (!commitHash) { + this.fire('show-alert', {message: ERR_COMMIT_NOT_FOUND}); + return; + } + const submissionId = change.submission_id; + const revertTitle = 'Revert submission ' + submissionId; + this.changes = changes; + this.message = revertTitle + '\n\n' + + 'Reason for revert: <INSERT REASONING HERE>\n'; + this.message += 'Reverted Changes:\n'; + changes = changes || []; + changes.forEach(change => { + this.message += change.change_id.substring(0, 10) + ': ' + + this.getTrimmedChangeSubject(change.subject) + '\n'; + }); + this.message = this._modifyRevertSubmissionMsg(change); + } + + _handleConfirmTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('confirm', null, {bubbles: false}); + } + + _handleCancelTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('cancel', null, {bubbles: false}); + } + } + + customElements.define(GrConfirmRevertSubmissionDialog.is, + GrConfirmRevertSubmissionDialog); +})();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_test.html new file mode 100644 index 0000000..cc4bd54 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_test.html
@@ -0,0 +1,100 @@ +<!DOCTYPE html> +<!-- +@license +Copyright (C) 2019 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-confirm-revert-submission-dialog</title> + +<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> + +<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="gr-confirm-revert-submission-dialog.html"> + +<script>void(0);</script> + +<test-fixture id="basic"> + <template> + <gr-confirm-revert-submission-dialog> + </gr-confirm-revert-submission-dialog> + </template> +</test-fixture> + +<script> + suite('gr-confirm-revert-submission-dialog tests', () => { + let element; + let sandbox; + + setup(() => { + element = fixture('basic'); + sandbox =sinon.sandbox.create(); + }); + + teardown(() => sandbox.restore()); + + test('no match', () => { + assert.isNotOk(element.message); + const alertStub = sandbox.stub(); + element.addEventListener('show-alert', alertStub); + element.populateRevertSubmissionMessage( + 'not a commitHash in sight' + ); + assert.isTrue(alertStub.calledOnce); + }); + + test('single line', () => { + assert.isNotOk(element.message); + element.populateRevertSubmissionMessage( + 'one line commit\n\nChange-Id: abcdefg\n', + 'abcd123'); + const expected = 'Revert submission\n\n' + + 'Reason for revert: <INSERT REASONING HERE>\n'; + assert.equal(element.message, expected); + }); + + test('multi line', () => { + assert.isNotOk(element.message); + element.populateRevertSubmissionMessage( + 'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n', + 'abcd123'); + const expected = 'Revert submission\n\n' + + 'Reason for revert: <INSERT REASONING HERE>\n'; + assert.equal(element.message, expected); + }); + + test('issue above change id', () => { + assert.isNotOk(element.message); + element.populateRevertSubmissionMessage( + 'test \nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n', + 'abcd123'); + const expected = 'Revert submission\n\n' + + 'Reason for revert: <INSERT REASONING HERE>\n'; + assert.equal(element.message, expected); + }); + + test('revert a revert', () => { + assert.isNotOk(element.message); + element.populateRevertSubmissionMessage( + 'Revert "one line commit"\n\nChange-Id: abcdefg\n', + 'abcd123'); + const expected = 'Revert submission\n\n' + + 'Reason for revert: <INSERT REASONING HERE>\n'; + assert.equal(element.message, expected); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js index e86e21c..b4d3595 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
@@ -17,9 +17,10 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-confirm-submit-dialog', - + class GrConfirmSubmitDialog extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-confirm-submit-dialog'; } /** * Fired when the confirm button is pressed. * @@ -32,37 +33,41 @@ * @event cancel */ - properties: { + static get properties() { + return { /** * @type {{ * is_private: boolean, * subject: string, * }} */ - change: Object, + change: Object, - /** - * @type {{ - * label: string, - * }} - */ - action: Object, - }, + /** + * @type {{ + * label: string, + * }} + */ + action: Object, + }; + } resetFocus(e) { this.$.dialog.resetFocus(); - }, + } _handleConfirmTap(e) { e.preventDefault(); e.stopPropagation(); this.dispatchEvent(new CustomEvent('confirm', {bubbles: false})); - }, + } _handleCancelTap(e) { e.preventDefault(); e.stopPropagation(); this.dispatchEvent(new CustomEvent('cancel', {bubbles: false})); - }, - }); + } + } + + customElements.define(GrConfirmSubmitDialog.is, GrConfirmSubmitDialog); })();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html index 40fa29a..515147f 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-confirm-submit-dialog</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html index e20bbd7..4ddc876 100644 --- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html +++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
@@ -27,16 +27,13 @@ <template> <style include="shared-styles"> :host { - background-color: var(--dialog-background-color); display: block; + padding: var(--spacing-m) 0; } section { display: flex; padding: var(--spacing-m) var(--spacing-xl); } - section:not(:first-of-type) { - border-top: 1px solid var(--border-color); - } .flexContainer { display: flex; justify-content: space-between; @@ -76,9 +73,9 @@ } </style> <section> - <span class="title"> + <h3 class="title"> Patch set [[patchNum]] of [[_computePatchSetQuantity(change.revisions)]] - </span> + </h3> </section> <section class$="[[_computeShowDownloadCommands(_schemes)]]"> <gr-download-commands
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js index 90cc60f..72ad5d2 100644 --- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
@@ -17,40 +17,47 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-download-dialog', - + /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.RESTClientMixin + */ + class GrDownloadDialog extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.PatchSetBehavior, + Gerrit.RESTClientBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-download-dialog'; } /** * Fired when the user presses the close button. * * @event close */ - properties: { + static get properties() { + return { /** @type {{ revisions: Array }} */ - change: Object, - patchNum: String, - /** @type {?} */ - config: Object, + change: Object, + patchNum: String, + /** @type {?} */ + config: Object, - _schemes: { - type: Array, - value() { return []; }, - computed: '_computeSchemes(change, patchNum)', - observer: '_schemesChanged', - }, - _selectedScheme: String, - }, + _schemes: { + type: Array, + value() { return []; }, + computed: '_computeSchemes(change, patchNum)', + observer: '_schemesChanged', + }, + _selectedScheme: String, + }; + } - hostAttributes: { - role: 'dialog', - }, - - behaviors: [ - Gerrit.FireBehavior, - Gerrit.PatchSetBehavior, - Gerrit.RESTClientBehavior, - ], + ready() { + super.ready(); + this._ensureAttribute('role', 'dialog'); + } focus() { if (this._schemes.length) { @@ -58,15 +65,16 @@ } else { this.$.download.focus(); } - }, + } getFocusStops() { - const links = this.$$('#archives').querySelectorAll('a'); + const links = this.shadowRoot + .querySelector('#archives').querySelectorAll('a'); return { start: this.$.closeButton, end: links[links.length - 1], }; - }, + } _computeDownloadCommands(change, patchNum, _selectedScheme) { let commandObj; @@ -87,7 +95,7 @@ }); } return commands; - }, + } /** * @param {!Object} change @@ -97,7 +105,7 @@ */ _computeZipDownloadLink(change, patchNum) { return this._computeDownloadLink(change, patchNum, true); - }, + } /** * @param {!Object} change @@ -107,7 +115,7 @@ */ _computeZipDownloadFilename(change, patchNum) { return this._computeDownloadFilename(change, patchNum, true); - }, + } /** * @param {!Object} change @@ -123,8 +131,7 @@ } return this.changeBaseURL(change.project, change._number, patchNum) + '/patch?' + (opt_zip ? 'zip' : 'download'); - }, - + } /** * @param {!Object} change @@ -147,7 +154,7 @@ } } return shortRev + '.diff.' + (opt_zip ? 'zip' : 'base64'); - }, + } _computeHidePatchFile(change, patchNum) { // Polymer 2: check for undefined @@ -157,12 +164,12 @@ for (const rev of Object.values(change.revisions || {})) { if (this.patchNumEquals(rev._number, patchNum)) { const parentLength = rev.commit && rev.commit.parents ? - rev.commit.parents.length : 0; + rev.commit.parents.length : 0; return parentLength == 0; } } return false; - }, + } _computeArchiveDownloadLink(change, patchNum, format) { // Polymer 2: check for undefined @@ -171,7 +178,7 @@ } return this.changeBaseURL(change.project, change._number, patchNum) + '/archive?format=' + format; - }, + } _computeSchemes(change, patchNum) { // Polymer 2: check for undefined @@ -189,28 +196,30 @@ } } return []; - }, + } _computePatchSetQuantity(revisions) { if (!revisions) { return 0; } return Object.keys(revisions).length; - }, + } _handleCloseTap(e) { e.preventDefault(); e.stopPropagation(); this.fire('close', null, {bubbles: false}); - }, + } _schemesChanged(schemes) { if (schemes.length === 0) { return; } if (!schemes.includes(this._selectedScheme)) { this._selectedScheme = schemes.sort()[0]; } - }, + } _computeShowDownloadCommands(schemes) { return schemes.length ? '' : 'hidden'; - }, - }); + } + } + + customElements.define(GrDownloadDialog.is, GrDownloadDialog); })();
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html index de02923..0f1260f 100644 --- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-download-dialog</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -182,7 +182,7 @@ test('computed fields', () => { assert.equal(element._computeArchiveDownloadLink( {project: 'test/project', _number: 123}, 2, 'tgz'), - '/changes/test%2Fproject~123/revisions/2/archive?format=tgz'); + '/changes/test%2Fproject~123/revisions/2/archive?format=tgz'); }); test('close event', done => {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js index 2126433..46463d2 100644 --- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js +++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
@@ -21,9 +21,17 @@ const PATCH_DESC_MAX_LENGTH = 500; const MERGED_STATUS = 'MERGED'; - Polymer({ - is: 'gr-file-list-header', - + /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.PatchSetMixin + */ + class GrFileListHeader extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.PatchSetBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-file-list-header'; } /** * @event expand-diffs */ @@ -48,72 +56,71 @@ * @event open-upload-help-dialog */ - properties: { - account: Object, - allPatchSets: Array, - /** @type {?} */ - change: Object, - changeNum: String, - changeUrl: String, - changeComments: Object, - commitInfo: Object, - editMode: Boolean, - loggedIn: Boolean, - serverConfig: Object, - shownFileCount: Number, - diffPrefs: Object, - diffPrefsDisabled: Boolean, - diffViewMode: { - type: String, - notify: true, - }, - patchNum: String, - basePatchNum: String, - filesExpanded: String, - // Caps the number of files that can be shown and have the 'show diffs' / - // 'hide diffs' buttons still be functional. - _maxFilesForBulkActions: { - type: Number, - readOnly: true, - value: 225, - }, - _patchsetDescription: { - type: String, - value: '', - }, - showTitle: { - type: Boolean, - value: true, - }, - _descriptionReadOnly: { - type: Boolean, - computed: '_computeDescriptionReadOnly(loggedIn, change, account)', - }, - revisionInfo: Object, - }, + static get properties() { + return { + account: Object, + allPatchSets: Array, + /** @type {?} */ + change: Object, + changeNum: String, + changeUrl: String, + changeComments: Object, + commitInfo: Object, + editMode: Boolean, + loggedIn: Boolean, + serverConfig: Object, + shownFileCount: Number, + diffPrefs: Object, + diffPrefsDisabled: Boolean, + diffViewMode: { + type: String, + notify: true, + }, + patchNum: String, + basePatchNum: String, + filesExpanded: String, + // Caps the number of files that can be shown and have the 'show diffs' / + // 'hide diffs' buttons still be functional. + _maxFilesForBulkActions: { + type: Number, + readOnly: true, + value: 225, + }, + _patchsetDescription: { + type: String, + value: '', + }, + showTitle: { + type: Boolean, + value: true, + }, + _descriptionReadOnly: { + type: Boolean, + computed: '_computeDescriptionReadOnly(loggedIn, change, account)', + }, + revisionInfo: Object, + }; + } - behaviors: [ - Gerrit.FireBehavior, - Gerrit.PatchSetBehavior, - ], - - observers: [ - '_computePatchSetDescription(change, patchNum)', - ], + static get observers() { + return [ + '_computePatchSetDescription(change, patchNum)', + ]; + } setDiffViewMode(mode) { this.$.modeSelect.setMode(mode); - }, + } _expandAllDiffs() { this._expanded = true; this.fire('expand-diffs'); - }, + } _collapseAllDiffs() { this._expanded = false; this.fire('collapse-diffs'); - }, + } _computeExpandedClass(filesExpanded) { const classes = []; @@ -125,11 +132,11 @@ classes.push('openFile'); } return classes.join(' '); - }, + } _computeDescriptionPlaceholder(readOnly) { return (readOnly ? 'No' : 'Add') + ' patchset description'; - }, + } _computeDescriptionReadOnly(loggedIn, change, account) { // Polymer 2: check for undefined @@ -138,7 +145,7 @@ } return !(loggedIn && (account._account_id === change.owner._account_id)); - }, + } _computePatchSetDescription(change, patchNum) { // Polymer 2: check for undefined @@ -148,12 +155,12 @@ const rev = this.getRevisionByPatchNum(change.revisions, patchNum); this._patchsetDescription = (rev && rev.description) ? - rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : ''; - }, + rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : ''; + } _handleDescriptionRemoved(e) { return this._updateDescription('', e); - }, + } /** * @param {!Object} revisions The revisions object keyed by revision hashes @@ -167,15 +174,16 @@ return rev; } } - }, + } _handleDescriptionChanged(e) { const desc = e.detail.trim(); this._updateDescription(desc, e); - }, + } /** * Update the patchset description with the rest API. + * * @param {string} desc * @param {?(Event|Node)} e * @return {!Promise} @@ -197,43 +205,43 @@ if (target) { target.disabled = false; } return; }); - }, + } _computePrefsButtonHidden(prefs, diffPrefsDisabled) { return diffPrefsDisabled || !prefs; - }, + } _fileListActionsVisible(shownFileCount, maxFilesForBulkActions) { return shownFileCount <= maxFilesForBulkActions; - }, + } _handlePatchChange(e) { const {basePatchNum, patchNum} = e.detail; if (this.patchNumEquals(basePatchNum, this.basePatchNum) && this.patchNumEquals(patchNum, this.patchNum)) { return; } Gerrit.Nav.navigateToChange(this.change, patchNum, basePatchNum); - }, + } _handlePrefsTap(e) { e.preventDefault(); this.fire('open-diff-prefs'); - }, + } _handleIncludedInTap(e) { e.preventDefault(); this.fire('open-included-in-dialog'); - }, + } _handleDownloadTap(e) { e.preventDefault(); e.stopPropagation(); this.dispatchEvent( new CustomEvent('open-download-dialog', {bubbles: false})); - }, + } _computeEditModeClass(editMode) { return editMode ? 'editMode' : ''; - }, + } _computePatchInfoClass(patchNum, allPatchSets) { const latestNum = this.computeLatestPatchNum(allPatchSets); @@ -241,27 +249,29 @@ return ''; } return 'patchInfoOldPatchSet'; - }, + } _hideIncludedIn(change) { return change && change.status === MERGED_STATUS ? '' : 'hide'; - }, + } _handleUploadTap(e) { e.preventDefault(); e.stopPropagation(); this.dispatchEvent( new CustomEvent('open-upload-help-dialog', {bubbles: false})); - }, + } _computeUploadHelpContainerClass(change, account) { const changeIsMerged = change && change.status === MERGED_STATUS; const ownerId = change && change.owner && change.owner._account_id ? - change.owner._account_id : null; + change.owner._account_id : null; const userId = account && account._account_id; const userIsOwner = ownerId && userId && ownerId === userId; const hideContainer = !userIsOwner || changeIsMerged; return 'uploadContainer desktop' + (hideContainer ? ' hide' : ''); - }, - }); + } + } + + customElements.define(GrFileListHeader.is, GrFileListHeader); })();
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html index ac626ab..10efaff 100644 --- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html +++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-file-list-header</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -216,8 +216,8 @@ test('expand/collapse buttons are toggled correctly', () => { element.shownFileCount = 10; flushAsynchronousOperations(); - const expandBtn = element.$$('#expandBtn'); - const collapseBtn = element.$$('#collapseBtn'); + const expandBtn = element.shadowRoot.querySelector('#expandBtn'); + const collapseBtn = element.shadowRoot.querySelector('#collapseBtn'); assert.notEqual(getComputedStyle(expandBtn).display, 'none'); assert.equal(getComputedStyle(collapseBtn).display, 'none'); element.filesExpanded = GrFileListConstants.FilesExpandedState.SOME;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html index 0668e14..0a7f1ce 100644 --- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html +++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -116,6 +116,9 @@ cursor: pointer; flex: 1; text-decoration: none; + /* Wrap it into multiple lines if too long. */ + white-space: normal; + word-break: break-word; } .path:hover :first-child { text-decoration: underline; @@ -197,10 +200,6 @@ .truncatedFileName { display: none; } - .expanded .fullFileName { - white-space: normal; - word-wrap: break-word; - } .mobile { display: none; } @@ -247,7 +246,9 @@ margin: -2px 0; padding: var(--spacing-s) 0; } - @media screen and (max-width: 50em) { + + /** small screen breakpoint: 768px */ + @media screen and (max-width: 55em) { .desktop { display: none; } @@ -433,7 +434,6 @@ no-auto-render show-load-failure display-line="[[_displayLine]]" - inline-index=[[index]] hidden="[[!_isFileExpanded(file.__path, _expandedFilePaths.*)]]" change-num="[[changeNum]]" patch-range="[[patchRange]]"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js index 4903467..5bec680 100644 --- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js +++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -41,162 +41,175 @@ U: 'Unchanged', }; - Polymer({ - is: 'gr-file-list', - + /** + * @appliesMixin Gerrit.AsyncForeachMixin + * @appliesMixin Gerrit.DomUtilMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.PathListMixin + */ + class GrFileList extends Polymer.mixinBehaviors( [ + Gerrit.AsyncForeachBehavior, + Gerrit.DomUtilBehavior, + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + Gerrit.PatchSetBehavior, + Gerrit.PathListBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-file-list'; } /** * Fired when a draft refresh should get triggered * * @event reload-drafts */ - properties: { + static get properties() { + return { /** @type {?} */ - patchRange: Object, - patchNum: String, - changeNum: String, - /** @type {?} */ - changeComments: Object, - drafts: Object, - revisions: Array, - projectConfig: Object, - selectedIndex: { - type: Number, - notify: true, - }, - keyEventTarget: { - type: Object, - value() { return document.body; }, - }, - /** @type {?} */ - change: Object, - diffViewMode: { - type: String, - notify: true, - observer: '_updateDiffPreferences', - }, - editMode: { - type: Boolean, - observer: '_editModeChanged', - }, - filesExpanded: { - type: String, - value: GrFileListConstants.FilesExpandedState.NONE, - notify: true, - }, - _filesByPath: Object, - _files: { - type: Array, - observer: '_filesChanged', - value() { return []; }, - }, - _loggedIn: { - type: Boolean, - value: false, - }, - _reviewed: { - type: Array, - value() { return []; }, - }, - diffPrefs: { - type: Object, - notify: true, - observer: '_updateDiffPreferences', - }, - /** @type {?} */ - _userPrefs: Object, - _showInlineDiffs: Boolean, - numFilesShown: { - type: Number, - notify: true, - }, - /** @type {?} */ - _patchChange: { - type: Object, - computed: '_calculatePatchChange(_files)', - }, - fileListIncrement: Number, - _hideChangeTotals: { - type: Boolean, - computed: '_shouldHideChangeTotals(_patchChange)', - }, - _hideBinaryChangeTotals: { - type: Boolean, - computed: '_shouldHideBinaryChangeTotals(_patchChange)', - }, + patchRange: Object, + patchNum: String, + changeNum: String, + /** @type {?} */ + changeComments: Object, + drafts: Object, + revisions: Array, + projectConfig: Object, + selectedIndex: { + type: Number, + notify: true, + }, + keyEventTarget: { + type: Object, + value() { return document.body; }, + }, + /** @type {?} */ + change: Object, + diffViewMode: { + type: String, + notify: true, + observer: '_updateDiffPreferences', + }, + editMode: { + type: Boolean, + observer: '_editModeChanged', + }, + filesExpanded: { + type: String, + value: GrFileListConstants.FilesExpandedState.NONE, + notify: true, + }, + _filesByPath: Object, + _files: { + type: Array, + observer: '_filesChanged', + value() { return []; }, + }, + _loggedIn: { + type: Boolean, + value: false, + }, + _reviewed: { + type: Array, + value() { return []; }, + }, + diffPrefs: { + type: Object, + notify: true, + observer: '_updateDiffPreferences', + }, + /** @type {?} */ + _userPrefs: Object, + _showInlineDiffs: Boolean, + numFilesShown: { + type: Number, + notify: true, + }, + /** @type {?} */ + _patchChange: { + type: Object, + computed: '_calculatePatchChange(_files)', + }, + fileListIncrement: Number, + _hideChangeTotals: { + type: Boolean, + computed: '_shouldHideChangeTotals(_patchChange)', + }, + _hideBinaryChangeTotals: { + type: Boolean, + computed: '_shouldHideBinaryChangeTotals(_patchChange)', + }, - _shownFiles: { - type: Array, - computed: '_computeFilesShown(numFilesShown, _files)', - }, + _shownFiles: { + type: Array, + computed: '_computeFilesShown(numFilesShown, _files)', + }, - /** - * The amount of files added to the shown files list the last time it was - * updated. This is used for reporting the average render time. - */ - _reportinShownFilesIncrement: Number, + /** + * The amount of files added to the shown files list the last time it was + * updated. This is used for reporting the average render time. + */ + _reportinShownFilesIncrement: Number, - _expandedFilePaths: { - type: Array, - value() { return []; }, - }, - _displayLine: Boolean, - _loading: { - type: Boolean, - observer: '_loadingChanged', - }, - /** @type {Gerrit.LayoutStats|undefined} */ - _sizeBarLayout: { - type: Object, - computed: '_computeSizeBarLayout(_shownFiles.*)', - }, + _expandedFilePaths: { + type: Array, + value() { return []; }, + }, + _displayLine: Boolean, + _loading: { + type: Boolean, + observer: '_loadingChanged', + }, + /** @type {Gerrit.LayoutStats|undefined} */ + _sizeBarLayout: { + type: Object, + computed: '_computeSizeBarLayout(_shownFiles.*)', + }, - _showSizeBars: { - type: Boolean, - value: true, - computed: '_computeShowSizeBars(_userPrefs)', - }, + _showSizeBars: { + type: Boolean, + value: true, + computed: '_computeShowSizeBars(_userPrefs)', + }, - /** @type {Function} */ - _cancelForEachDiff: Function, + /** @type {Function} */ + _cancelForEachDiff: Function, - _showDynamicColumns: { - type: Boolean, - computed: '_computeShowDynamicColumns(_dynamicHeaderEndpoints, ' + + _showDynamicColumns: { + type: Boolean, + computed: '_computeShowDynamicColumns(_dynamicHeaderEndpoints, ' + '_dynamicContentEndpoints, _dynamicSummaryEndpoints)', - }, - /** @type {Array<string>} */ - _dynamicHeaderEndpoints: { - type: Array, - }, - /** @type {Array<string>} */ - _dynamicContentEndpoints: { - type: Array, - }, - /** @type {Array<string>} */ - _dynamicSummaryEndpoints: { - type: Array, - }, - }, + }, + /** @type {Array<string>} */ + _dynamicHeaderEndpoints: { + type: Array, + }, + /** @type {Array<string>} */ + _dynamicContentEndpoints: { + type: Array, + }, + /** @type {Array<string>} */ + _dynamicSummaryEndpoints: { + type: Array, + }, + }; + } - behaviors: [ - Gerrit.AsyncForeachBehavior, - Gerrit.DomUtilBehavior, - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - Gerrit.PatchSetBehavior, - Gerrit.PathListBehavior, - ], - - observers: [ - '_expandedPathsChanged(_expandedFilePaths.splices)', - '_computeFiles(_filesByPath, changeComments, patchRange, _reviewed, ' + + static get observers() { + return [ + '_expandedPathsChanged(_expandedFilePaths.splices)', + '_computeFiles(_filesByPath, changeComments, patchRange, _reviewed, ' + '_loading)', - ], + ]; + } - keyBindings: { - esc: '_handleEscKey', - }, + get keyBindings() { + return { + esc: '_handleEscKey', + }; + } keyboardShortcuts() { return { @@ -221,12 +234,16 @@ [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null, [this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null, }; - }, - listeners: { - keydown: '_scopedKeydownHandler', - }, + } + + created() { + super.created(); + this.addEventListener('keydown', + e => this._scopedKeydownHandler(e)); + } attached() { + super.attached(); Gerrit.awaitPluginsLoaded().then(() => { this._dynamicHeaderEndpoints = Gerrit._endpoints.getDynamicEndpoints( 'change-view-file-list-header'); @@ -246,11 +263,12 @@ 'Different number of dynamic file-list headers and summary.'); } }); - }, + } detached() { + super.detached(); this._cancelDiffs(); - }, + } /** * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard @@ -264,7 +282,7 @@ // Enter. this._handleOpenFile(e); } - }, + } reload() { if (!this.changeNum || !this.patchRange.patchNum) { @@ -302,28 +320,28 @@ this._detectChromiteButler(); this.$.reporting.fileListDisplayed(); }); - }, + } _detectChromiteButler() { const hasButler = !!document.getElementById('butler-suggested-owners'); if (hasButler) { this.$.reporting.reportExtension('butler'); } - }, + } get diffs() { return Array.from( Polymer.dom(this.root).querySelectorAll('gr-diff-host')); - }, + } openDiffPrefs() { this.$.diffPreferencesDialog.open(); - }, + } _calculatePatchChange(files) { - const magicFilesExcluded = files.filter(files => { - return files.__path !== '/COMMIT_MSG' && files.__path !== '/MERGE_LIST'; - }); + const magicFilesExcluded = files.filter(files => + !this.isMagicPath(files.__path) + ); return magicFilesExcluded.reduce((acc, obj) => { const inserted = obj.lines_inserted ? obj.lines_inserted : 0; @@ -343,15 +361,15 @@ }; }, {inserted: 0, deleted: 0, size_delta_inserted: 0, size_delta_deleted: 0, total_size: 0}); - }, + } _getDiffPreferences() { return this.$.restAPI.getDiffPreferences(); - }, + } _getPreferences() { return this.$.restAPI.getPreferences(); - }, + } _togglePathExpanded(path) { // Is the path in the list of expanded diffs? IF so remove it, otherwise @@ -362,11 +380,11 @@ } else { this.splice('_expandedFilePaths', pathIndex, 1); } - }, + } _togglePathExpandedByIndex(index) { this._togglePathExpanded(this._files[index].__path); - }, + } _updateDiffPreferences() { if (!this.diffs.length) { return; } @@ -374,14 +392,14 @@ this.$.reporting.time(EXPAND_ALL_TIMING_LABEL); this._renderInOrder(this._expandedFilePaths, this.diffs, this._expandedFilePaths.length); - }, + } _forEachDiff(fn) { const diffs = this.diffs; for (let i = 0; i < diffs.length; i++) { fn(diffs[i]); } - }, + } expandAllDiffs() { this._showInlineDiffs = true; @@ -398,7 +416,7 @@ } this.splice(...['_expandedFilePaths', 0, 0].concat(newPaths)); - }, + } collapseAllDiffs() { this._showInlineDiffs = false; @@ -406,7 +424,7 @@ this.filesExpanded = this._computeExpandedFiles( this._expandedFilePaths.length, this._files.length); this.$.diffCursor.handleDiffUpdate(); - }, + } /** * Computes a string with the number of comments and unresolved comments. @@ -433,7 +451,7 @@ (commentString && unresolvedString ? ' ' : '') + // Add parentheses around unresolved if it exists. (unresolvedString ? `(${unresolvedString})` : ''); - }, + } /** * Computes a string with the number of drafts. @@ -448,7 +466,7 @@ changeComments.computeDraftCount(patchRange.basePatchNum, path) + changeComments.computeDraftCount(patchRange.patchNum, path); return GrCountStringFormatter.computePluralString(draftCount, 'draft'); - }, + } /** * Computes a shortened string with the number of drafts. @@ -463,7 +481,7 @@ changeComments.computeDraftCount(patchRange.basePatchNum, path) + changeComments.computeDraftCount(patchRange.patchNum, path); return GrCountStringFormatter.computeShortString(draftCount, 'd'); - }, + } /** * Computes a shortened string with the number of comments. @@ -478,7 +496,7 @@ changeComments.computeCommentCount(patchRange.basePatchNum, path) + changeComments.computeCommentCount(patchRange.patchNum, path); return GrCountStringFormatter.computeShortString(commentCount, 'c'); - }, + } /** * @param {string} path @@ -495,31 +513,32 @@ } this._saveReviewedState(path, reviewed); - }, + } _saveReviewedState(path, reviewed) { return this.$.restAPI.saveFileReviewed(this.changeNum, this.patchRange.patchNum, path, reviewed); - }, + } _getLoggedIn() { return this.$.restAPI.getLoggedIn(); - }, + } _getReviewedFiles() { if (this.editMode) { return Promise.resolve([]); } return this.$.restAPI.getReviewedFiles(this.changeNum, this.patchRange.patchNum); - }, + } _getFiles() { return this.$.restAPI.getChangeOrEditFiles( this.changeNum, this.patchRange); - }, + } /** * The closure compiler doesn't realize this.specialFilePathCompare is * valid. + * * @suppress {checkTypes} */ _normalizeChangeFilesResponse(response) { @@ -534,7 +553,7 @@ files.push(info); } return files; - }, + } /** * Handle all events from the file list dom-repeat so event handleers don't @@ -564,7 +583,7 @@ e.preventDefault(); this._togglePathExpanded(path); - }, + } _handleLeftPane(e) { if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) { @@ -573,7 +592,7 @@ e.preventDefault(); this.$.diffCursor.moveLeft(); - }, + } _handleRightPane(e) { if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) { @@ -582,7 +601,7 @@ e.preventDefault(); this.$.diffCursor.moveRight(); - }, + } _handleToggleInlineDiff(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -591,14 +610,14 @@ e.preventDefault(); this._togglePathExpandedByIndex(this.$.fileCursor.index); - }, + } _handleToggleAllInlineDiffs(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } e.preventDefault(); this._toggleInlineDiffs(); - }, + } _handleCursorNext(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { @@ -616,7 +635,7 @@ this.$.fileCursor.next(); this.selectedIndex = this.$.fileCursor.index; } - }, + } _handleCursorPrev(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { @@ -634,20 +653,14 @@ this.$.fileCursor.previous(); this.selectedIndex = this.$.fileCursor.index; } - }, + } _handleNewComment(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { return; } - - const isRangeSelected = this.diffs.some(diff => { - return diff.isRangeSelected(); - }, this); - if (!isRangeSelected) { - e.preventDefault(); - this._addDraftAtTarget(); - } - }, + e.preventDefault(); + this.$.diffCursor.createCommentInPlace(); + } _handleOpenLastFile(e) { // Check for meta key to avoid overriding native chrome shortcut. @@ -656,7 +669,7 @@ e.preventDefault(); this._openSelectedFile(this._files.length - 1); - }, + } _handleOpenFirstFile(e) { // Check for meta key to avoid overriding native chrome shortcut. @@ -665,7 +678,7 @@ e.preventDefault(); this._openSelectedFile(0); - }, + } _handleOpenFile(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -678,7 +691,7 @@ } this._openSelectedFile(); - }, + } _handleNextChunk(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -693,7 +706,7 @@ } else { this.$.diffCursor.moveToNextChunk(); } - }, + } _handlePrevChunk(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -708,7 +721,7 @@ } else { this.$.diffCursor.moveToPreviousChunk(); } - }, + } _handleToggleFileReviewed(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { @@ -718,7 +731,7 @@ e.preventDefault(); if (!this._files[this.$.fileCursor.index]) { return; } this._reviewFile(this._files[this.$.fileCursor.index].__path); - }, + } _handleToggleLeftPane(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } @@ -727,7 +740,7 @@ this._forEachDiff(diff => { diff.toggleLeftDiff(); }); - }, + } _toggleInlineDiffs() { if (this._showInlineDiffs) { @@ -735,13 +748,13 @@ } else { this.expandAllDiffs(); } - }, + } _openCursorFile() { const diff = this.$.diffCursor.getTargetDiffElement(); Gerrit.Nav.navigateToDiff(this.change, diff.path, diff.patchRange.patchNum, this.patchRange.basePatchNum); - }, + } /** * @param {number=} opt_index @@ -754,7 +767,7 @@ Gerrit.Nav.navigateToDiff(this.change, this._files[this.$.fileCursor.index].__path, this.patchRange.patchNum, this.patchRange.basePatchNum); - }, + } _addDraftAtTarget() { const diff = this.$.diffCursor.getTargetDiffElement(); @@ -762,20 +775,20 @@ if (diff && target) { diff.addDraftAtLine(target); } - }, + } _shouldHideChangeTotals(_patchChange) { return _patchChange.inserted === 0 && _patchChange.deleted === 0; - }, + } _shouldHideBinaryChangeTotals(_patchChange) { return _patchChange.size_delta_inserted === 0 && _patchChange.size_delta_deleted === 0; - }, + } _computeFileStatus(status) { return status || 'M'; - }, + } _computeDiffURL(change, patchNum, basePatchNum, path, editMode) { // Polymer 2: check for undefined @@ -790,7 +803,7 @@ basePatchNum); } return Gerrit.Nav.getUrlForDiff(change, path, patchNum, basePatchNum); - }, + } _formatBytes(bytes) { if (bytes == 0) return '+/-0 B'; @@ -802,7 +815,7 @@ const prepend = bytes > 0 ? '+' : ''; return prepend + parseFloat((bytes / Math.pow(bits, exponent)) .toFixed(decimals)) + ' ' + sizes[exponent]; - }, + } _formatPercentage(size, delta) { const oldSize = size - delta; @@ -811,12 +824,12 @@ const percentage = Math.round(Math.abs(delta * 100 / oldSize)); return '(' + (delta > 0 ? '+' : '-') + percentage + '%)'; - }, + } _computeBinaryClass(delta) { if (delta === 0) { return; } return delta >= 0 ? 'added' : 'removed'; - }, + } /** * @param {string} baseClass @@ -831,16 +844,16 @@ classes.push('invisible'); } return classes.join(' '); - }, + } _computePathClass(path, expandedFilesRecord) { return this._isFileExpanded(path, expandedFilesRecord) ? 'expanded' : ''; - }, + } _computeShowHideIcon(path, expandedFilesRecord) { return this._isFileExpanded(path, expandedFilesRecord) ? - 'gr-icons:expand-less' : 'gr-icons:expand-more'; - }, + 'gr-icons:expand-less' : 'gr-icons:expand-more'; + } _computeFiles(filesByPath, changeComments, patchRange, reviewed, loading) { // Polymer 2: check for undefined @@ -870,7 +883,7 @@ } this._files = this._normalizeChangeFilesResponse(files); - }, + } _computeFilesShown(numFilesShown, files) { // Polymer 2: check for undefined @@ -879,7 +892,7 @@ } const previousNumFilesShown = this._shownFiles ? - this._shownFiles.length : 0; + this._shownFiles.length : 0; const filesShown = files.slice(0, numFilesShown); this.fire('files-shown-changed', {length: filesShown.length}); @@ -894,13 +907,13 @@ Math.max(0, filesShown.length - previousNumFilesShown); return filesShown; - }, + } _updateDiffCursor() { // Overwrite the cursor's list of diffs: this.$.diffCursor.splice( ...['diffs', 0, this.$.diffCursor.diffs.length].concat(this.diffs)); - }, + } _filesChanged() { if (this._files && this._files.length > 0) { @@ -910,41 +923,41 @@ this.$.fileCursor.stops = files; this.$.fileCursor.setCursorAtIndex(this.selectedIndex, true); } - }, + } _incrementNumFilesShown() { this.numFilesShown += this.fileListIncrement; - }, + } _computeFileListControlClass(numFilesShown, files) { return numFilesShown >= files.length ? 'invisible' : ''; - }, + } _computeIncrementText(numFilesShown, files) { if (!files) { return ''; } const text = Math.min(this.fileListIncrement, files.length - numFilesShown); return 'Show ' + text + ' more'; - }, + } _computeShowAllText(files) { if (!files) { return ''; } return 'Show all ' + files.length + ' files'; - }, + } _computeWarnShowAll(files) { return files.length > WARN_SHOW_ALL_THRESHOLD; - }, + } _computeShowAllWarning(files) { if (!this._computeWarnShowAll(files)) { return ''; } return 'Warning: showing all ' + files.length + ' files may take several seconds.'; - }, + } _showAllFiles() { this.numFilesShown = this._files.length; - }, + } _computePatchSetDescription(revisions, patchNum) { // Polymer 2: check for undefined @@ -954,29 +967,30 @@ const rev = this.getRevisionByPatchNum(revisions, patchNum); return (rev && rev.description) ? - rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : ''; - }, + rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : ''; + } /** * Get a descriptive label for use in the status indicator's tooltip and * ARIA label. + * * @param {string} status * @return {string} */ _computeFileStatusLabel(status) { const statusCode = this._computeFileStatus(status); return FileStatus.hasOwnProperty(statusCode) ? - FileStatus[statusCode] : 'Status Unknown'; - }, + FileStatus[statusCode] : 'Status Unknown'; + } _isFileExpanded(path, expandedFilesRecord) { return expandedFilesRecord.base.includes(path); - }, + } _onLineSelected(e, detail) { this.$.diffCursor.moveToLineNumber(detail.number, detail.side, detail.path); - }, + } _computeExpandedFiles(expandedCount, totalCount) { if (expandedCount === 0) { @@ -985,20 +999,21 @@ return GrFileListConstants.FilesExpandedState.ALL; } return GrFileListConstants.FilesExpandedState.SOME; - }, + } /** * Handle splices to the list of expanded file paths. If there are any new * entries in the expanded list, then render each diff corresponding in * order by waiting for the previous diff to finish before starting the next * one. + * * @param {!Array} record The splice record in the expanded paths list. */ _expandedPathsChanged(record) { // Clear content for any diffs that are not open so if they get re-opened // the stale content does not flash before it is cleared and reloaded. const collapsedDiffs = this.diffs.filter(diff => - this._expandedFilePaths.indexOf(diff.path) === -1); + this._expandedFilePaths.indexOf(diff.path) === -1); this._clearCollapsedDiffs(collapsedDiffs); if (!record) { return; } // Happens after "Collapse all" clicked. @@ -1008,9 +1023,9 @@ // Find the paths introduced by the new index splices: const newPaths = record.indexSplices - .map(splice => splice.object.slice( - splice.index, splice.index + splice.addedCount)) - .reduce((acc, paths) => acc.concat(paths), []); + .map(splice => splice.object.slice( + splice.index, splice.index + splice.addedCount)) + .reduce((acc, paths) => acc.concat(paths), []); // Required so that the newly created diff view is included in this.diffs. Polymer.dom.flush(); @@ -1023,19 +1038,20 @@ this._updateDiffCursor(); this.$.diffCursor.handleDiffUpdate(); - }, + } _clearCollapsedDiffs(collapsedDiffs) { for (const diff of collapsedDiffs) { diff.cancel(); diff.clearDiffContent(); } - }, + } /** * Given an array of paths and a NodeList of diff elements, render the diff * for each path in order, awaiting the previous render to complete before * continung. + * * @param {!Array<string>} paths * @param {!NodeList<!Object>} diffElements (GrDiffHostElement) * @param {number} initialCount The total number of paths in the pass. This @@ -1071,16 +1087,17 @@ this.$.diffCursor.handleDiffUpdate(); }); }); - }, + } /** Cancel the rendering work of every diff in the list */ _cancelDiffs() { if (this._cancelForEachDiff) { this._cancelForEachDiff(); } this._forEachDiff(d => d.cancel()); - }, + } /** * In the given NodeList of diff elements, find the diff for the given path. + * * @param {string} path * @param {!NodeList<!Object>} diffElements (GrDiffElement) * @return {!Object|undefined} (GrDiffElement) @@ -1091,10 +1108,11 @@ return diffElements[i]; } } - }, + } /** * Reset the comments of a modified thread + * * @param {string} rootId * @param {string} path */ @@ -1128,19 +1146,20 @@ }); Polymer.dom.flush(); return; - }, + } _handleEscKey(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { return; } e.preventDefault(); this._displayLine = false; - }, + } /** * Update the loading class for the file list rows. The update is inside a * debouncer so that the file list doesn't flash gray when the API requests * are reasonably fast. + * * @param {boolean} loading */ _loadingChanged(loading) { @@ -1149,32 +1168,34 @@ // this way, the gray loading style is not shown on initial loads. this.classList.toggle('loading', loading && !!this._files.length); }, LOADING_DEBOUNCE_INTERVAL); - }, + } _editModeChanged(editMode) { this.classList.toggle('editMode', editMode); - }, + } _computeReviewedClass(isReviewed) { return isReviewed ? 'isReviewed' : ''; - }, + } _computeReviewedText(isReviewed) { return isReviewed ? 'MARK UNREVIEWED' : 'MARK REVIEWED'; - }, + } /** * Given a file path, return whether that path should have visible size bars * and be included in the size bars calculation. + * * @param {string} path * @return {boolean} */ _showBarsForPath(path) { return path !== this.COMMIT_MESSAGE_PATH && path !== this.MERGE_LIST_PATH; - }, + } /** * Compute size bar layout values from the file list. + * * @return {Gerrit.LayoutStats|undefined} */ _computeSizeBarLayout(shownFilesRecord) { @@ -1205,10 +1226,11 @@ stats.deletionOffset = stats.maxAdditionWidth + SIZE_BAR_GAP_WIDTH; } return stats; - }, + } /** * Get the width of the addition bar for a file. + * * @param {Object} file * @param {Gerrit.LayoutStats} stats * @return {number} @@ -1222,10 +1244,11 @@ const width = stats.maxAdditionWidth * file.lines_inserted / stats.maxInserted; return width === 0 ? 0 : Math.max(SIZE_BAR_MIN_WIDTH, width); - }, + } /** * Get the x-offset of the addition bar for a file. + * * @param {Object} file * @param {Gerrit.LayoutStats} stats * @return {number} @@ -1233,10 +1256,11 @@ _computeBarAdditionX(file, stats) { return stats.maxAdditionWidth - this._computeBarAdditionWidth(file, stats); - }, + } /** * Get the width of the deletion bar for a file. + * * @param {Object} file * @param {Gerrit.LayoutStats} stats * @return {number} @@ -1250,20 +1274,21 @@ const width = stats.maxDeletionWidth * file.lines_deleted / stats.maxDeleted; return width === 0 ? 0 : Math.max(SIZE_BAR_MIN_WIDTH, width); - }, + } /** * Get the x-offset of the deletion bar for a file. + * * @param {Gerrit.LayoutStats} stats * @return {number} */ _computeBarDeletionX(stats) { return stats.deletionOffset; - }, + } _computeShowSizeBars(userPrefs) { return !!userPrefs.size_bar_in_change_table; - }, + } _computeSizeBarsClass(showSizeBars, path) { let hideClass = ''; @@ -1273,7 +1298,7 @@ hideClass = 'invisible'; } return `sizeBars desktop ${hideClass}`; - }, + } /** * Shows registered dynamic columns iff the 'header', 'content' and @@ -1286,20 +1311,22 @@ return headerEndpoints && contentEndpoints && summaryEndpoints && headerEndpoints.length === contentEndpoints.length && headerEndpoints.length === summaryEndpoints.length; - }, + } /** * Returns true if none of the inline diffs have been expanded. + * * @return {boolean} */ _noDiffsExpanded() { return this.filesExpanded === GrFileListConstants.FilesExpandedState.NONE; - }, + } /** * Method to call via binding when each file list row is rendered. This * allows approximate detection of when the dom-repeat has completed * rendering. + * * @param {number} index The index of the row being rendered. * @return {string} an empty string. */ @@ -1311,7 +1338,7 @@ }, 1); } return ''; - }, + } _reviewedTitle(reviewed) { if (reviewed) { @@ -1319,12 +1346,14 @@ } return 'Mark as reviewed (shortcut: r)'; - }, + } _handleReloadingDiffPreference() { this._getDiffPreferences().then(prefs => { this.diffPrefs = prefs; }); - }, - }); + } + } + + customElements.define(GrFileList.is, GrFileList); })();
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html index 122291e..0d25846 100644 --- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-file-list</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -440,10 +440,10 @@ '/COMMIT_MSG', 'comment'), '3 comments (1 unresolved)'); assert.equal( element._computeCommentsStringMobile(element.changeComments, parentTo1 - , '/COMMIT_MSG'), '2c'); + , '/COMMIT_MSG'), '2c'); assert.equal( element._computeCommentsStringMobile(element.changeComments, _1To2 - , '/COMMIT_MSG'), '3c'); + , '/COMMIT_MSG'), '3c'); assert.equal( element._computeDraftsString(element.changeComments, parentTo1, 'unresolved.file'), '1 draft'); @@ -639,7 +639,7 @@ assert(navStub.lastCall.calledWith(element.change, 'file_added_in_rev2.txt', '2'), - 'Should navigate to /c/42/2/file_added_in_rev2.txt'); + 'Should navigate to /c/42/2/file_added_in_rev2.txt'); MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); @@ -647,9 +647,10 @@ assert.equal(element.$.fileCursor.index, 0); assert.equal(element.selectedIndex, 0); - sandbox.stub(element, '_addDraftAtTarget'); + const createCommentInPlaceStub = sandbox.stub(element.$.diffCursor, + 'createCommentInPlace'); MockInteractions.pressAndReleaseKeyOn(element, 67, null, 'c'); - assert.isTrue(element._addDraftAtTarget.called); + assert.isTrue(createCommentInPlaceStub.called); }); test('i key shows/hides selected inline diff', () => { @@ -1639,7 +1640,7 @@ element.set('_filesByPath', _filesByPath); flushAsynchronousOperations(); - // Navigates when a file is selected. + // Navigates when a file is selected. element._openSelectedFile(); assert.isTrue(navStub.called); }); @@ -1701,7 +1702,7 @@ const editControls = Array.from( Polymer.dom(element.root) - .querySelectorAll('.row:not(.header-row)')) + .querySelectorAll('.row:not(.header-row)')) .map(row => row.querySelector('gr-edit-file-controls')); assert.isTrue(editControls[0].classList.contains('invisible')); });
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js index 4b8ce22..134ce10 100644 --- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
@@ -17,36 +17,40 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-included-in-dialog', - + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrIncludedInDialog extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-included-in-dialog'; } /** * Fired when the user presses the close button. * * @event close */ - properties: { + static get properties() { + return { /** @type {?} */ - changeNum: { - type: Object, - observer: '_resetData', - }, - /** @type {?} */ - _includedIn: Object, - _loaded: { - type: Boolean, - value: false, - }, - _filterText: { - type: String, - value: '', - }, - }, - - behaviors: [ - Gerrit.FireBehavior, - ], + changeNum: { + type: Object, + observer: '_resetData', + }, + /** @type {?} */ + _includedIn: Object, + _loaded: { + type: Boolean, + value: false, + }, + _filterText: { + type: String, + value: '', + }, + }; + } loadData() { if (!this.changeNum) { return; } @@ -57,12 +61,12 @@ this._includedIn = configs; this._loaded = true; }); - }, + } _resetData() { this._includedIn = null; this._loaded = false; - }, + } _computeGroups(includedIn, filterText) { if (!includedIn) { return []; } @@ -83,22 +87,24 @@ } } return groups.filter(g => g.items.length); - }, + } _handleCloseTap(e) { e.preventDefault(); e.stopPropagation(); this.fire('close', null, {bubbles: false}); - }, + } _computeLoadingClass(loaded) { return loaded ? 'loading loaded' : 'loading'; - }, + } _onFilterChanged() { this.debounce('filter-change', () => { this._filterText = this.$.filterInput.bindValue; }, 100); - }, - }); + } + } + + customElements.define(GrIncludedInDialog.is, GrIncludedInDialog); })();
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html index 68c77e6..b97e0b4 100644 --- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-included-in-dialog</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html index 220546b..46fd227 100644 --- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html +++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
@@ -23,7 +23,9 @@ <dom-module id="gr-label-score-row"> <template> - <style include="gr-voting-styles"></style> + <style include="gr-voting-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="shared-styles"> .labelContainer { align-items: center; @@ -109,7 +111,7 @@ </template> <iron-selector id="labelSelector" - attr-for-selected="value" + attr-for-selected="data-value" selected="[[_computeLabelValue(labels, permittedLabels, label)]]" hidden$="[[!_computeAnyPermittedLabelValues(permittedLabels, label.name)]]" on-selected-item-changed="_setSelectedValueText"> @@ -119,8 +121,8 @@ <gr-button class$="[[_computeButtonClass(value, index, _items.length)]]" has-tooltip - name="[[label.name]]" - value$="[[value]]" + data-name$="[[label.name]]" + data-value$="[[value]]" title$="[[_computeLabelValueTitle(labels, label.name, value)]]"> [[value]]</gr-button> </template>
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js index 0b888c4..abfe469 100644 --- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js +++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
@@ -17,56 +17,59 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-label-score-row', - + class GrLabelScoreRow extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-label-score-row'; } /** * Fired when any label is changed. * * @event labels-changed */ - properties: { + static get properties() { + return { /** * @type {{ name: string }} */ - label: Object, - labels: Object, - name: { - type: String, - reflectToAttribute: true, - }, - permittedLabels: Object, - labelValues: Object, - _selectedValueText: { - type: String, - value: 'No value selected', - }, - _items: { - type: Array, - computed: '_computePermittedLabelValues(permittedLabels, label.name)', - }, - }, + label: Object, + labels: Object, + name: { + type: String, + reflectToAttribute: true, + }, + permittedLabels: Object, + labelValues: Object, + _selectedValueText: { + type: String, + value: 'No value selected', + }, + _items: { + type: Array, + computed: '_computePermittedLabelValues(permittedLabels, label.name)', + }, + }; + } get selectedItem() { if (!this._ironSelector) { return undefined; } return this._ironSelector.selectedItem; - }, + } get selectedValue() { if (!this._ironSelector) { return undefined; } return this._ironSelector.selected; - }, + } setSelectedValue(value) { // The selector may not be present if it’s not at the latest patch set. if (!this._ironSelector) { return; } this._ironSelector.select(value); - }, + } get _ironSelector() { return this.$ && this.$.labelSelector; - }, + } _computeBlankItems(permittedLabels, label, side) { if (!permittedLabels || !permittedLabels[label] || @@ -82,7 +85,7 @@ const endPosition = this.labelValues[parseInt( permittedLabels[label][permittedLabels[label].length - 1], 10)]; return new Array(Object.keys(this.labelValues).length - endPosition - 1); - }, + } _getLabelValue(labels, permittedLabels, label) { if (label.value) { @@ -93,7 +96,7 @@ return permittedLabels[label.name].find( value => parseInt(value, 10) === labels[label.name].default_value); } - }, + } _computeButtonClass(value, index, totalItems) { const classes = []; @@ -114,7 +117,7 @@ } return classes.join(' '); - }, + } _computeLabelValue(labels, permittedLabels, label) { if ([labels, permittedLabels, label].some(arg => arg === undefined)) { @@ -123,7 +126,7 @@ if (!labels[label.name]) { return null; } const labelValue = this._getLabelValue(labels, permittedLabels, label); const len = permittedLabels[label.name] != null ? - permittedLabels[label.name].length : 0; + permittedLabels[label.name].length : 0; for (let i = 0; i < len; i++) { const val = permittedLabels[label.name][i]; if (val === labelValue) { @@ -131,7 +134,7 @@ } } return null; - }, + } _setSelectedValueText(e) { // Needed because when the selected item changes, it first changes to @@ -140,22 +143,22 @@ this._selectedValueText = e.target.selectedItem.getAttribute('title'); // Needed to update the style of the selected button. this.updateStyles(); - const name = e.target.selectedItem.name; - const value = e.target.selectedItem.getAttribute('value'); + const name = e.target.selectedItem.dataset.name; + const value = e.target.selectedItem.dataset.value; this.dispatchEvent(new CustomEvent( 'labels-changed', {detail: {name, value}, bubbles: true, composed: true})); - }, + } _computeAnyPermittedLabelValues(permittedLabels, label) { return permittedLabels && permittedLabels.hasOwnProperty(label) && permittedLabels[label].length; - }, + } _computeHiddenClass(permittedLabels, label) { return !this._computeAnyPermittedLabelValues(permittedLabels, label) ? - 'hidden' : ''; - }, + 'hidden' : ''; + } _computePermittedLabelValues(permittedLabels, label) { // Polymer 2: check for undefined @@ -164,12 +167,14 @@ } return permittedLabels[label]; - }, + } _computeLabelValueTitle(labels, label, value) { return labels[label] && labels[label].values && labels[label].values[value]; - }, - }); + } + } + + customElements.define(GrLabelScoreRow.is, GrLabelScoreRow); })();
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html index 519fbb8..e9c21bc 100644 --- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html +++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-label-score-row</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -110,7 +110,7 @@ element.addEventListener('labels-changed', labelsChangedHandler); assert.ok(element.$.labelSelector); MockInteractions.tap(element.$$( - 'gr-button[value="-1"]')); + 'gr-button[data-value="-1"]')); flushAsynchronousOperations(); assert.strictEqual(element.selectedValue, '-1'); assert.strictEqual(element.selectedItem @@ -167,7 +167,7 @@ test('do not display tooltips on touch devices', () => { const verifiedBtn = element.$$( - 'iron-selector > gr-button[value="-1"]'); + 'iron-selector > gr-button[data-value="-1"]'); // On touch devices, tooltips should not be shown. verifiedBtn._isTouchDevice = true;
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js index 5b498c2..d2488e4 100644 --- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js +++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
@@ -17,22 +17,29 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-label-scores', + class GrLabelScores extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-label-scores'; } - properties: { - _labels: { - type: Array, - computed: '_computeLabels(change.labels.*, account)', - }, - permittedLabels: { - type: Object, - observer: '_computeColumns', - }, - /** @type {?} */ - change: Object, - _labelValues: Object, - }, + static get properties() { + return { + _labels: { + type: Array, + computed: '_computeLabels(change.labels.*, account)', + }, + permittedLabels: { + type: Object, + observer: '_computeColumns', + }, + /** @type {?} */ + change: Object, + /** @type {?} */ + account: Object, + + _labelValues: Object, + }; + } getLabelValues() { const labels = {}; @@ -58,7 +65,7 @@ } } return labels; - }, + } _getStringLabelValue(labels, labelName, numberValue) { for (const k in labels[labelName].values) { @@ -67,7 +74,7 @@ } } return numberValue; - }, + } _getVoteForAccount(labels, labelName, account) { const votes = labels[labelName]; @@ -80,7 +87,7 @@ } } return null; - }, + } _computeLabels(labelRecord, account) { // Polymer 2: check for undefined @@ -96,7 +103,7 @@ value: this._getVoteForAccount(labelsObj, key, this.account), }; }); - }, + } _computeColumns(permittedLabels) { const labels = Object.keys(permittedLabels); @@ -115,15 +122,15 @@ values[orderedValues[i]] = i; } this._labelValues = values; - }, + } _changeIsMerged(changeStatus) { return changeStatus === 'MERGED'; - }, + } /** - * @param label {string|undefined} - * @param permittedLabels {Object|undefined} + * @param {string|undefined} label + * @param {Object|undefined} permittedLabels * @return {string} */ _computeLabelAccessClass(label, permittedLabels) { @@ -133,6 +140,8 @@ return permittedLabels.hasOwnProperty(label) && permittedLabels[label].length ? 'access' : 'no-access'; - }, - }); + } + } + + customElements.define(GrLabelScores.is, GrLabelScores); })();
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html index f986a58..4c489b7 100644 --- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html +++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-label-scores</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -123,7 +123,7 @@ const labelName = 'Code-Review'; assert.strictEqual(element._getVoteForAccount( element.change.labels, labelName, element.account), - '+1'); + '+1'); }); test('_computeColumns', () => { @@ -187,10 +187,10 @@ {name: 'Verified', value: null} ]); element.set(['change', 'labels', 'Verified', 'all'], - [{_account_id: 123, value: 1}]); + [{_account_id: 123, value: 1}]); assert.deepEqual(element._labels, [ - {name: 'Code-Review', value: null}, - {name: 'Verified', value: '+1'}, + {name: 'Code-Review', value: null}, + {name: 'Verified', value: '+1'}, ]); }); });
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html index da5eb39..fbd5d68d 100644 --- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html +++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -31,7 +31,9 @@ <dom-module id="gr-message"> <template> - <style include="gr-voting-styles"></style> + <style include="gr-voting-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="shared-styles"> :host { border-bottom: 1px solid var(--border-color);
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js index 26e0cd3..39438b5 100644 --- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js +++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -20,9 +20,15 @@ const PATCH_SET_PREFIX_PATTERN = /^Patch Set \d+: /; const LABEL_TITLE_SCORE_PATTERN = /^([A-Za-z0-9-]+)([+-]\d+)$/; - Polymer({ - is: 'gr-message', - + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrMessage extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-message'; } /** * Fired when this message's reply link is tapped. * @@ -35,90 +41,93 @@ * @event message-anchor-tap */ - listeners: { - click: '_handleClick', - }, + static get properties() { + return { + changeNum: Number, + /** @type {?} */ + message: Object, + author: { + type: Object, + computed: '_computeAuthor(message)', + }, + comments: { + type: Object, + observer: '_commentsChanged', + }, + config: Object, + hideAutomated: { + type: Boolean, + value: false, + }, + hidden: { + type: Boolean, + computed: '_computeIsHidden(hideAutomated, isAutomated)', + reflectToAttribute: true, + }, + isAutomated: { + type: Boolean, + computed: '_computeIsAutomated(message)', + }, + showAvatar: { + type: Boolean, + computed: '_computeShowAvatar(author, config)', + }, + showOnBehalfOf: { + type: Boolean, + computed: '_computeShowOnBehalfOf(message)', + }, + showReplyButton: { + type: Boolean, + computed: '_computeShowReplyButton(message, _loggedIn)', + }, + projectName: { + type: String, + observer: '_projectNameChanged', + }, - properties: { - changeNum: Number, - /** @type {?} */ - message: Object, - author: { - type: Object, - computed: '_computeAuthor(message)', - }, - comments: { - type: Object, - observer: '_commentsChanged', - }, - config: Object, - hideAutomated: { - type: Boolean, - value: false, - }, - hidden: { - type: Boolean, - computed: '_computeIsHidden(hideAutomated, isAutomated)', - reflectToAttribute: true, - }, - isAutomated: { - type: Boolean, - computed: '_computeIsAutomated(message)', - }, - showAvatar: { - type: Boolean, - computed: '_computeShowAvatar(author, config)', - }, - showOnBehalfOf: { - type: Boolean, - computed: '_computeShowOnBehalfOf(message)', - }, - showReplyButton: { - type: Boolean, - computed: '_computeShowReplyButton(message, _loggedIn)', - }, - projectName: { - type: String, - observer: '_projectNameChanged', - }, + /** + * A mapping from label names to objects representing the minimum and + * maximum possible values for that label. + */ + labelExtremes: Object, - /** - * A mapping from label names to objects representing the minimum and - * maximum possible values for that label. - */ - labelExtremes: Object, + /** + * @type {{ commentlinks: Array }} + */ + _projectConfig: Object, + // Computed property needed to trigger Polymer value observing. + _expanded: { + type: Object, + computed: '_computeExpanded(message.expanded)', + }, + _loggedIn: { + type: Boolean, + value: false, + }, + }; + } - /** - * @type {{ commentlinks: Array }} - */ - _projectConfig: Object, - // Computed property needed to trigger Polymer value observing. - _expanded: { - type: Object, - computed: '_computeExpanded(message.expanded)', - }, - _loggedIn: { - type: Boolean, - value: false, - }, - }, + static get observers() { + return [ + '_updateExpandedClass(message.expanded)', + ]; + } - behaviors: [ - Gerrit.FireBehavior, - ], - - observers: [ - '_updateExpandedClass(message.expanded)', - ], + created() { + super.created(); + this.addEventListener('click', + e => this._handleClick(e)); + } ready() { + super.ready(); this.$.restAPI.getConfig().then(config => { this.config = config; }); this.$.restAPI.getLoggedIn().then(loggedIn => { this._loggedIn = loggedIn; }); - }, + } _updateExpandedClass(expanded) { if (expanded) { @@ -126,30 +135,30 @@ } else { this.classList.remove('expanded'); } - }, + } _computeAuthor(message) { return message.author || message.updated_by; - }, + } _computeShowAvatar(author, config) { return !!(author && config && config.plugin && config.plugin.has_avatars); - }, + } _computeShowOnBehalfOf(message) { const author = message.author || message.updated_by; return !!(author && message.real_author && author._account_id != message.real_author._account_id); - }, + } _computeShowReplyButton(message, loggedIn) { return message && !!message.message && loggedIn && !this._computeIsAutomated(message); - }, + } _computeExpanded(expanded) { return expanded; - }, + } /** * If there is no value set on the message object as to whether _expanded @@ -160,33 +169,33 @@ if (this.message && this.message.expanded === undefined) { this.set('message.expanded', Object.keys(value || {}).length > 0); } - }, + } _handleClick(e) { if (this.message.expanded) { return; } e.stopPropagation(); this.set('message.expanded', true); - }, + } _handleAuthorClick(e) { if (!this.message.expanded) { return; } e.stopPropagation(); this.set('message.expanded', false); - }, + } _computeIsAutomated(message) { return !!(message.reviewer || this._computeIsReviewerUpdate(message) || (message.tag && message.tag.startsWith('autogenerated'))); - }, + } _computeIsHidden(hideAutomated, isAutomated) { return hideAutomated && isAutomated; - }, + } _computeIsReviewerUpdate(event) { return event.type === 'REVIEWER_UPDATE'; - }, + } _getScores(message) { if (!message.message) { return []; } @@ -199,7 +208,7 @@ .map(s => s.match(LABEL_TITLE_SCORE_PATTERN)) .filter(ms => ms && ms.length === 3) .map(ms => ({label: ms[1], value: ms[2]})); - }, + } _computeScoreClass(score, labelExtremes) { // Polymer 2: check for undefined @@ -222,14 +231,14 @@ } } return classes.join(' '); - }, + } _computeClass(expanded, showAvatar, message) { const classes = []; classes.push(expanded ? 'expanded' : 'collapsed'); classes.push(showAvatar ? 'showAvatar' : 'hideAvatar'); return classes.join(' '); - }, + } _handleAnchorClick(e) { e.preventDefault(); @@ -238,26 +247,28 @@ composed: true, detail: {id: this.message.id}, })); - }, + } _handleReplyTap(e) { e.preventDefault(); this.fire('reply', {message: this.message}); - }, + } _projectNameChanged(name) { this.$.restAPI.getProjectConfig(name).then(config => { this._projectConfig = config; }); - }, + } _computeExpandToggleIcon(expanded) { return expanded ? 'gr-icons:expand-less' : 'gr-icons:expand-more'; - }, + } _toggleExpanded(e) { e.stopPropagation(); this.set('message.expanded', !this.message.expanded); - }, - }); + } + } + + customElements.define(GrMessage.is, GrMessage); })();
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html index ef5a756..01bc691 100644 --- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html +++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-message</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js index ba4a1a9..9afb334 100644 --- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js +++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -25,57 +25,61 @@ SHOW_MORE: 'show-more-messages', }; - Polymer({ - is: 'gr-messages-list', + class GrMessagesList extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-messages-list'; } - properties: { - changeNum: Number, - messages: { - type: Array, - value() { return []; }, - }, - reviewerUpdates: { - type: Array, - value() { return []; }, - }, - changeComments: Object, - projectName: String, - showReplyButtons: { - type: Boolean, - value: false, - }, - labels: Object, + static get properties() { + return { + changeNum: Number, + messages: { + type: Array, + value() { return []; }, + }, + reviewerUpdates: { + type: Array, + value() { return []; }, + }, + changeComments: Object, + projectName: String, + showReplyButtons: { + type: Boolean, + value: false, + }, + labels: Object, - _expanded: { - type: Boolean, - value: false, - observer: '_expandedChanged', - }, - _hideAutomated: { - type: Boolean, - value: false, - }, - /** - * The messages after processing and including merged reviewer updates. - */ - _processedMessages: { - type: Array, - computed: '_computeItems(messages, reviewerUpdates)', - observer: '_processedMessagesChanged', - }, - /** - * The subset of _processedMessages that is visible to the user. - */ - _visibleMessages: { - type: Array, - value() { return []; }, - }, + _expanded: { + type: Boolean, + value: false, + observer: '_expandedChanged', + }, + _hideAutomated: { + type: Boolean, + value: false, + }, + /** + * The messages after processing and including merged reviewer updates. + */ + _processedMessages: { + type: Array, + computed: '_computeItems(messages, reviewerUpdates)', + observer: '_processedMessagesChanged', + }, + /** + * The subset of _processedMessages that is visible to the user. + */ + _visibleMessages: { + type: Array, + value() { return []; }, + }, - _labelExtremes: { - type: Object, - computed: '_computeLabelExtremes(labels.*)', - }, - }, + _labelExtremes: { + type: Object, + computed: '_computeLabelExtremes(labels.*)', + }, + }; + } scrollToMessage(messageID) { let el = this.$$('[data-message-id="' + messageID + '"]'); @@ -102,18 +106,18 @@ el.set('message.expanded', true); let top = el.offsetTop; for (let offsetParent = el.offsetParent; - offsetParent; - offsetParent = offsetParent.offsetParent) { + offsetParent; + offsetParent = offsetParent.offsetParent) { top += offsetParent.offsetTop; } window.scrollTo(0, top); this._highlightEl(el); - }, + } _isAutomated(message) { return !!(message.reviewer || (message.tag && message.tag.startsWith('autogenerated'))); - }, + } _computeItems(messages, reviewerUpdates) { // Polymer 2: check for undefined @@ -152,7 +156,7 @@ } } return result; - }, + } _expandedChanged(exp) { if (this._processedMessages) { @@ -169,7 +173,7 @@ this.notifyPath(`_visibleMessages.${i}.expanded`); } } - }, + } _highlightEl(el) { const highlightedEls = @@ -183,23 +187,23 @@ } el.addEventListener('animationend', handleAnimationEnd); el.classList.add('highlighted'); - }, + } /** * @param {boolean} expand */ handleExpandCollapse(expand) { this._expanded = expand; - }, + } _handleExpandCollapseTap(e) { e.preventDefault(); this.handleExpandCollapse(!this._expanded); - }, + } _handleAnchorClick(e) { this.scrollToMessage(e.detail.id); - }, + } _hasAutomatedMessages(messages) { if (!messages) { return false; } @@ -209,16 +213,17 @@ } } return false; - }, + } _computeExpandCollapseMessage(expanded) { return expanded ? 'Collapse all' : 'Expand all'; - }, + } /** * Computes message author's file comments for change's message. * Method uses this.messages to find next message and relies on messages * to be sorted by date field descending. + * * @param {!Object} changeComments changeComment object, which includes * a method to get all published comments (including robot comments), * which returns a Hash of arrays of comments, filename as key. @@ -268,7 +273,7 @@ } } return msgComments; - }, + } /** * Returns the number of messages to splice to the beginning of @@ -293,7 +298,7 @@ delta = msgsRemaining - i; } return Math.min(msgsRemaining, delta); - }, + } /** * Gets the number of messages that would be visible, but do not currently @@ -309,20 +314,20 @@ this._getHumanMessages(visibleMessages).length; } return messages.length - visibleMessages.length; - }, + } _computeIncrementText(visibleMessages, messages, hideAutomated) { let delta = this._getDelta(visibleMessages, messages, hideAutomated); delta = Math.min( this._numRemaining(visibleMessages, messages, hideAutomated), delta); return 'Show ' + Math.min(MESSAGES_INCREMENT, delta) + ' more'; - }, + } _getHumanMessages(messages) { return messages.filter(msg => { return !this._isAutomated(msg); }); - }, + } _computeShowHideTextHidden(visibleMessages, messages, hideAutomated) { @@ -335,12 +340,12 @@ visibleMessages = this._getHumanMessages(visibleMessages); } return visibleMessages.length >= messages.length; - }, + } _handleShowAllTap() { this._visibleMessages = this._processedMessages; this.$.reporting.reportInteraction(ReportingEvent.SHOW_ALL); - }, + } _handleIncrementShownMessages() { const delta = this._getDelta(this._visibleMessages, @@ -350,27 +355,27 @@ // Add newMessages to the beginning of _visibleMessages this.splice(...['_visibleMessages', 0, 0].concat(newMessages)); this.$.reporting.reportInteraction(ReportingEvent.SHOW_MORE); - }, + } _processedMessagesChanged(messages) { if (messages) { this._visibleMessages = messages.slice(-MAX_INITIAL_SHOWN_MESSAGES); } - }, + } _computeNumMessagesText(visibleMessages, messages, hideAutomated) { const total = this._numRemaining(visibleMessages, messages, hideAutomated); return total === 1 ? 'Show 1 message' : 'Show all ' + total + ' messages'; - }, + } _computeIncrementHidden(visibleMessages, messages, hideAutomated) { const total = this._numRemaining(visibleMessages, messages, hideAutomated); return total <= this._getDelta(visibleMessages, messages, hideAutomated); - }, + } /** * Compute a mapping from label name to objects representing the minimum and @@ -389,6 +394,8 @@ extremes[key] = {min: values[0], max: values[values.length - 1]}; } return extremes; - }, - }); + } + } + + customElements.define(GrMessagesList.is, GrMessagesList); })();
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html index 315403e..b0747f4 100644 --- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-messages-list</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -49,7 +49,6 @@ </test-fixture> <script> - const randomMessage = function(opt_params) { const params = opt_params || {}; const author1 = { @@ -232,7 +231,8 @@ .concat(_.times(11, randomMessage)); flushAsynchronousOperations(); - MockInteractions.tap(element.$$('#collapse-messages')); // Expand all. + MockInteractions.tap(element.shadowRoot + .querySelector('#collapse-messages')); // Expand all. flushAsynchronousOperations(); let messages = getMessages(); @@ -256,8 +256,10 @@ .concat(_.times(11, randomMessage)); flushAsynchronousOperations(); - MockInteractions.tap(element.$$('#collapse-messages')); // Expand all. - MockInteractions.tap(element.$$('#collapse-messages')); // Collapse all. + MockInteractions.tap(element.shadowRoot + .querySelector('#collapse-messages')); // Expand all. + MockInteractions.tap(element.shadowRoot + .querySelector('#collapse-messages')); // Collapse all. flushAsynchronousOperations(); let messages = getMessages(); @@ -284,13 +286,15 @@ MockInteractions.tap(allMessageEls[1]); assert.isTrue(allMessageEls[1]._expanded); - MockInteractions.tap(element.$$('#collapse-messages')); + MockInteractions.tap(element.shadowRoot + .querySelector('#collapse-messages')); allMessageEls = getMessages(); for (const message of allMessageEls) { assert.isTrue(message._expanded); } - MockInteractions.tap(element.$$('#collapse-messages')); + MockInteractions.tap(element.shadowRoot + .querySelector('#collapse-messages')); allMessageEls = getMessages(); for (const message of allMessageEls) { assert.isFalse(message._expanded); @@ -298,28 +302,33 @@ }); test('expand/collapse from external keypress', () => { - MockInteractions.tap(element.$$('#collapse-messages')); + MockInteractions.tap(element.shadowRoot + .querySelector('#collapse-messages')); let allMessageEls = getMessages(); for (const message of allMessageEls) { assert.isTrue(message._expanded); } // Expand/collapse all text also changes. - assert.equal(element.$$('#collapse-messages').textContent.trim(), - 'Collapse all'); + assert.equal(element.shadowRoot + .querySelector('#collapse-messages').textContent.trim(), + 'Collapse all'); - MockInteractions.tap(element.$$('#collapse-messages')); + MockInteractions.tap(element.shadowRoot + .querySelector('#collapse-messages')); allMessageEls = getMessages(); for (const message of allMessageEls) { assert.isFalse(message._expanded); } // Expand/collapse all text also changes. - assert.equal(element.$$('#collapse-messages').textContent.trim(), - 'Expand all'); + assert.equal(element.shadowRoot + .querySelector('#collapse-messages').textContent.trim(), + 'Expand all'); }); test('hide messages does not appear when no automated messages', () => { - assert.isOk(element.$$('#automatedMessageToggleContainer[hidden]')); + assert.isOk(element.shadowRoot + .querySelector('#automatedMessageToggleContainer[hidden]')); }); test('scroll to message', () => { @@ -477,7 +486,8 @@ }); test('hide autogenerated button is not hidden', () => { - assert.isNotOk(element.$$('#automatedMessageToggle[hidden]')); + assert.isNotOk(element.shadowRoot + .querySelector('#automatedMessageToggle[hidden]')); }); test('autogenerated messages are not hidden initially', () => { @@ -517,7 +527,6 @@ assert.equal(element._getDelta([], messages, false), 1); assert.equal(element._getDelta([], messages, true), 1); - messages = _.times(7, randomMessage); assert.equal(element._getDelta([], messages, false), 5); assert.equal(element._getDelta([], messages, true), 5);
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js index 76c5980..4035115 100644 --- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js +++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -17,9 +17,19 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-related-changes-list', - + /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.RESTClientMixin + */ + class GrRelatedChangesList extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.PatchSetBehavior, + Gerrit.RESTClientBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-related-changes-list'; } /** * Fired when a new section is loaded so that the change view can determine * a show more button is needed, sometimes before all the sections finish @@ -28,64 +38,62 @@ * @event new-section-loaded */ - properties: { - change: Object, - hasParent: { - type: Boolean, - notify: true, - value: false, - }, - patchNum: String, - parentChange: Object, - hidden: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - loading: { - type: Boolean, - notify: true, - }, - mergeable: Boolean, - _connectedRevisions: { - type: Array, - computed: '_computeConnectedRevisions(change, patchNum, ' + + static get properties() { + return { + change: Object, + hasParent: { + type: Boolean, + notify: true, + value: false, + }, + patchNum: String, + parentChange: Object, + hidden: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + loading: { + type: Boolean, + notify: true, + }, + mergeable: Boolean, + _connectedRevisions: { + type: Array, + computed: '_computeConnectedRevisions(change, patchNum, ' + '_relatedResponse.changes)', - }, - /** @type {?} */ - _relatedResponse: { - type: Object, - value() { return {changes: []}; }, - }, - /** @type {?} */ - _submittedTogether: { - type: Object, - value() { return {changes: []}; }, - }, - _conflicts: { - type: Array, - value() { return []; }, - }, - _cherryPicks: { - type: Array, - value() { return []; }, - }, - _sameTopic: { - type: Array, - value() { return []; }, - }, - }, + }, + /** @type {?} */ + _relatedResponse: { + type: Object, + value() { return {changes: []}; }, + }, + /** @type {?} */ + _submittedTogether: { + type: Object, + value() { return {changes: []}; }, + }, + _conflicts: { + type: Array, + value() { return []; }, + }, + _cherryPicks: { + type: Array, + value() { return []; }, + }, + _sameTopic: { + type: Array, + value() { return []; }, + }, + }; + } - behaviors: [ - Gerrit.FireBehavior, - Gerrit.PatchSetBehavior, - Gerrit.RESTClientBehavior, - ], - - observers: [ - '_resultsChanged(_relatedResponse, _submittedTogether, ' + + static get observers() { + return [ + '_resultsChanged(_relatedResponse, _submittedTogether, ' + '_conflicts, _cherryPicks, _sameTopic)', - ], + ]; + } clear() { this.loading = true; @@ -96,7 +104,7 @@ this._conflicts = []; this._cherryPicks = []; this._sameTopic = []; - }, + } reload() { if (!this.change || !this.patchNum) { @@ -144,7 +152,7 @@ return Promise.all(promises).then(() => { this.loading = false; }); - }, + } _fireReloadEvent() { // The listener on the change computes height of the related changes @@ -152,12 +160,13 @@ // that requires a flush. Polymer.dom.flush(); this.dispatchEvent(new CustomEvent('new-section-loaded')); - }, + } /** * Determines whether or not the given change has a parent change. If there * is a relation chain, and the change id is not the last item of the * relation chain, there is a parent. + * * @param {number} currentChangeId * @param {!Array} relatedChanges * @return {boolean} @@ -166,34 +175,34 @@ return relatedChanges.length > 0 && relatedChanges[relatedChanges.length - 1].change_id !== currentChangeId; - }, + } _getRelatedChanges() { return this.$.restAPI.getRelatedChanges(this.change._number, this.patchNum); - }, + } _getSubmittedTogether() { return this.$.restAPI.getChangesSubmittedTogether(this.change._number); - }, + } _getServerConfig() { return this.$.restAPI.getConfig(); - }, + } _getConflicts() { return this.$.restAPI.getChangeConflicts(this.change._number); - }, + } _getCherryPicks() { return this.$.restAPI.getChangeCherryPicks(this.change.project, this.change.change_id, this.change._number); - }, + } _getChangesWithSameTopic() { return this.$.restAPI.getChangesWithSameTopic(this.change.topic, this.change._number); - }, + } /** * @param {number} changeNum @@ -203,7 +212,7 @@ */ _computeChangeURL(changeNum, project, opt_patchNum) { return Gerrit.Nav.getUrlForChangeById(changeNum, project, opt_patchNum); - }, + } _computeChangeContainerClass(currentChange, relatedChange) { const classes = ['changeContainer']; @@ -214,11 +223,12 @@ classes.push('thisChange'); } return classes.join(' '); - }, + } /** * Do the given objects describe the same change? Compares the changes by * their numbers. + * * @see /Documentation/rest-api-changes.html#change-info * @see /Documentation/rest-api-changes.html#related-change-and-commit-info * @param {!Object} a Either ChangeInfo or RelatedChangeAndCommitInfo @@ -229,13 +239,14 @@ const aNum = this._getChangeNumber(a); const bNum = this._getChangeNumber(b); return aNum === bNum; - }, + } /** * Get the change number from either a ChangeInfo (such as those included in * SubmittedTogetherInfo responses) or get the change number from a * RelatedChangeAndCommitInfo (such as those included in a * RelatedChangesInfo response). + * * @see /Documentation/rest-api-changes.html#change-info * @see /Documentation/rest-api-changes.html#related-change-and-commit-info * @@ -251,7 +262,7 @@ return change._change_number; } return change._number; - }, + } _computeLinkClass(change) { const statuses = []; @@ -262,7 +273,7 @@ statuses.push('submittable'); } return statuses.join(' '); - }, + } _computeChangeStatusClass(change) { const classes = ['status']; @@ -276,7 +287,7 @@ classes.push('hidden'); } return classes.join(' '); - }, + } _computeChangeStatus(change) { switch (change.status) { @@ -293,7 +304,7 @@ return 'Submittable'; } return ''; - }, + } _resultsChanged(related, submittedTogether, conflicts, cherryPicks, sameTopic) { @@ -323,11 +334,11 @@ } } this.hidden = true; - }, + } _isIndirectAncestor(change) { return !this._connectedRevisions.includes(change.commit.commit); - }, + } _computeConnectedRevisions(change, patchNum, relatedChanges) { // Polymer 2: check for undefined @@ -364,20 +375,22 @@ --pos; } return connected; - }, + } _computeSubmittedTogetherClass(submittedTogether) { if (!submittedTogether || ( - submittedTogether.changes.length === 0 && + submittedTogether.changes.length === 0 && !submittedTogether.non_visible_changes)) { return 'hidden'; } return ''; - }, + } _computeNonVisibleChangesNote(n) { const noun = n === 1 ? 'change' : 'changes'; return `(+ ${n} non-visible ${noun})`; - }, - }); + } + } + + customElements.define(GrRelatedChangesList.is, GrRelatedChangesList); })();
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html index 06b7a5d..38b2fb9 100644 --- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-related-changes-list</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -321,7 +321,7 @@ sandbox.stub(element, '_getCherryPicks') .returns(Promise.resolve()); conflictsStub = sandbox.stub(element, '_getConflicts') - .returns(Promise.resolve()); + .returns(Promise.resolve()); }); test('request conflicts if open and mergeable', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html index 3632348..7d95323 100644 --- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html +++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-reply-dialog</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -156,7 +156,7 @@ const labelScoreRows = Polymer.dom(element.$.labelScores.root) .querySelector('gr-label-score-row[name="Code-Review"]'); const selectedBtn = Polymer.dom(labelScoreRows.root) - .querySelector('gr-button[value="+1"].iron-selected'); + .querySelector('gr-button[data-value="+1"].iron-selected'); assert.isOk(selectedBtn); done(); });
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html index e836ccc..c177283 100644 --- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html +++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -269,16 +269,6 @@ </section> <section class="actions"> <div class="left"> - <template is="dom-if" if="[[canBeStarted]]"> - <gr-button - link - secondary - disabled="[[_isState(knownLatestState, 'not-latest')]]" - class="action save" - has-tooltip - title="[[_saveTooltip]]" - on-click="_saveTapHandler">Save</gr-button> - </template> <span id="checkingStatusLabel" hidden$="[[!_isState(knownLatestState, 'checking')]]"> @@ -297,6 +287,18 @@ id="cancelButton" class="action cancel" on-click="_cancelTapHandler">Cancel</gr-button> + <template is="dom-if" if="[[canBeStarted]]"> + <!-- Use 'Send' here as the change may only about reviewers / ccs + and when this button is visible, the next button will always + be 'Start review' --> + <gr-button + link + disabled="[[_isState(knownLatestState, 'not-latest')]]" + class="action save" + has-tooltip + title="[[_saveTooltip]]" + on-click="_saveClickHandler">Send</gr-button> + </template> <gr-button id="sendButton" link
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js index cc7fbe6..3ce4f1a 100644 --- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -43,7 +43,7 @@ }; const ButtonTooltips = { - SAVE: 'Save reply but do not send notification', + SAVE: 'Send but do not send notification or change review state', START_REVIEW: 'Mark as ready for review and send reply', SEND: 'Send reply', }; @@ -52,9 +52,23 @@ const SEND_REPLY_TIMING_LABEL = 'SendReply'; - Polymer({ - is: 'gr-reply-dialog', - + /** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.RESTClientMixin + */ + class GrReplyDialog extends Polymer.mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + Gerrit.PatchSetBehavior, + Gerrit.RESTClientBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-reply-dialog'; } /** * Fired when a reply is successfully sent. * @@ -87,169 +101,172 @@ * @event comment-refresh */ - /** - * Fires when the state of the send button (enabled/disabled) changes. - * - * @event send-disabled-changed - */ + /** + * Fires when the state of the send button (enabled/disabled) changes. + * + * @event send-disabled-changed + */ - properties: { + constructor() { + super(); + this.FocusTarget = FocusTarget; + } + + static get properties() { + return { /** * @type {{ _number: number, removable_reviewers: Array }} */ - change: Object, - patchNum: String, - canBeStarted: { - type: Boolean, - value: false, - }, - disabled: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - draft: { - type: String, - value: '', - observer: '_draftChanged', - }, - quote: { - type: String, - value: '', - }, - diffDrafts: { - type: Object, - observer: '_handleHeightChanged', - }, - /** @type {!Function} */ - filterReviewerSuggestion: { - type: Function, - value() { - return this._filterReviewerSuggestionGenerator(false); + change: Object, + patchNum: String, + canBeStarted: { + type: Boolean, + value: false, }, - }, - /** @type {!Function} */ - filterCCSuggestion: { - type: Function, - value() { - return this._filterReviewerSuggestionGenerator(true); + disabled: { + type: Boolean, + value: false, + reflectToAttribute: true, }, - }, - permittedLabels: Object, - /** - * @type {{ commentlinks: Array }} - */ - projectConfig: Object, - knownLatestState: String, - underReview: { - type: Boolean, - value: true, - }, - - _account: Object, - _ccs: Array, - /** @type {?Object} */ - _ccPendingConfirmation: { - type: Object, - observer: '_reviewerPendingConfirmationUpdated', - }, - _messagePlaceholder: { - type: String, - computed: '_computeMessagePlaceholder(canBeStarted)', - }, - _owner: Object, - /** @type {?} */ - _pendingConfirmationDetails: Object, - _includeComments: { - type: Boolean, - value: true, - }, - _reviewers: Array, - /** @type {?Object} */ - _reviewerPendingConfirmation: { - type: Object, - observer: '_reviewerPendingConfirmationUpdated', - }, - _previewFormatting: { - type: Boolean, - value: false, - observer: '_handleHeightChanged', - }, - _reviewersPendingRemove: { - type: Object, - value: { - CC: [], - REVIEWER: [], + draft: { + type: String, + value: '', + observer: '_draftChanged', }, - }, - _sendButtonLabel: { - type: String, - computed: '_computeSendButtonLabel(canBeStarted)', - }, - _savingComments: Boolean, - _reviewersMutated: { - type: Boolean, - value: false, - }, - _labelsChanged: { - type: Boolean, - value: false, - }, - _saveTooltip: { - type: String, - value: ButtonTooltips.SAVE, - readOnly: true, - }, - _pluginMessage: { - type: String, - value: '', - }, - _sendDisabled: { - type: Boolean, - computed: '_computeSendButtonDisabled(_sendButtonLabel, diffDrafts, ' + - 'draft, _reviewersMutated, _labelsChanged, _includeComments, ' + - 'disabled)', - observer: '_sendDisabledChanged', - }, - }, + quote: { + type: String, + value: '', + }, + diffDrafts: { + type: Object, + observer: '_handleHeightChanged', + }, + /** @type {!Function} */ + filterReviewerSuggestion: { + type: Function, + value() { + return this._filterReviewerSuggestionGenerator(false); + }, + }, + /** @type {!Function} */ + filterCCSuggestion: { + type: Function, + value() { + return this._filterReviewerSuggestionGenerator(true); + }, + }, + permittedLabels: Object, + /** + * @type {{ commentlinks: Array }} + */ + projectConfig: Object, + knownLatestState: String, + underReview: { + type: Boolean, + value: true, + }, - FocusTarget, + _account: Object, + _ccs: Array, + /** @type {?Object} */ + _ccPendingConfirmation: { + type: Object, + observer: '_reviewerPendingConfirmationUpdated', + }, + _messagePlaceholder: { + type: String, + computed: '_computeMessagePlaceholder(canBeStarted)', + }, + _owner: Object, + /** @type {?} */ + _pendingConfirmationDetails: Object, + _includeComments: { + type: Boolean, + value: true, + }, + _reviewers: Array, + /** @type {?Object} */ + _reviewerPendingConfirmation: { + type: Object, + observer: '_reviewerPendingConfirmationUpdated', + }, + _previewFormatting: { + type: Boolean, + value: false, + observer: '_handleHeightChanged', + }, + _reviewersPendingRemove: { + type: Object, + value: { + CC: [], + REVIEWER: [], + }, + }, + _sendButtonLabel: { + type: String, + computed: '_computeSendButtonLabel(canBeStarted)', + }, + _savingComments: Boolean, + _reviewersMutated: { + type: Boolean, + value: false, + }, + _labelsChanged: { + type: Boolean, + value: false, + }, + _saveTooltip: { + type: String, + value: ButtonTooltips.SAVE, + readOnly: true, + }, + _pluginMessage: { + type: String, + value: '', + }, + _sendDisabled: { + type: Boolean, + computed: '_computeSendButtonDisabled(_sendButtonLabel, ' + + 'diffDrafts, draft, _reviewersMutated, _labelsChanged, ' + + '_includeComments, disabled)', + observer: '_sendDisabledChanged', + }, + }; + } - behaviors: [ - Gerrit.BaseUrlBehavior, - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - Gerrit.PatchSetBehavior, - Gerrit.RESTClientBehavior, - ], + get keyBindings() { + return { + 'esc': '_handleEscKey', + 'ctrl+enter meta+enter': '_handleEnterKey', + }; + } - keyBindings: { - 'esc': '_handleEscKey', - 'ctrl+enter meta+enter': '_handleEnterKey', - }, - - observers: [ - '_changeUpdated(change.reviewers.*, change.owner)', - '_ccsChanged(_ccs.splices)', - '_reviewersChanged(_reviewers.splices)', - ], + static get observers() { + return [ + '_changeUpdated(change.reviewers.*, change.owner)', + '_ccsChanged(_ccs.splices)', + '_reviewersChanged(_reviewers.splices)', + ]; + } attached() { + super.attached(); this._getAccount().then(account => { this._account = account || {}; }); - }, + } ready() { + super.ready(); this.$.jsAPI.addElement(this.$.jsAPI.Element.REPLY_DIALOG, this); - }, + } open(opt_focusTarget) { this.knownLatestState = LatestPatchState.CHECKING; this.fetchChangeUpdates(this.change, this.$.restAPI) .then(result => { this.knownLatestState = result.isLatest ? - LatestPatchState.LATEST : LatestPatchState.NOT_LATEST; + LatestPatchState.LATEST : LatestPatchState.NOT_LATEST; }); this._focusOn(opt_focusTarget); @@ -268,11 +285,11 @@ this._savingComments = false; }); } - }, + } focus() { this._focusOn(FocusTarget.ANY); - }, + } getFocusStops() { const end = this._sendDisabled ? this.$.cancelButton : this.$.sendButton; @@ -280,14 +297,14 @@ start: this.$.reviewers.focusStart, end, }; - }, + } setLabelValue(label, value) { const selectorEl = this.$.labelScores.$$(`gr-label-score-row[name="${label}"]`); if (!selectorEl) { return; } selectorEl.setSelectedValue(value); - }, + } getLabelValue(label) { const selectorEl = @@ -295,23 +312,23 @@ if (!selectorEl) { return null; } return selectorEl.selectedValue; - }, + } _handleEscKey(e) { this.cancel(); - }, + } _handleEnterKey(e) { this._submit(); - }, + } _ccsChanged(splices) { this._reviewerTypeChanged(splices, ReviewerTypes.CC); - }, + } _reviewersChanged(splices) { this._reviewerTypeChanged(splices, ReviewerTypes.REVIEWER); - }, + } _reviewerTypeChanged(splices, reviewerType) { if (splices && splices.indexSplices) { @@ -342,7 +359,7 @@ } } } - }, + } _processReviewerChange(indexSplices, type) { for (const splice of indexSplices) { @@ -354,7 +371,7 @@ this._reviewersPendingRemove[type].push(account); } } - }, + } /** * Resets the state of the _reviewersPendingRemove object, and removes @@ -380,7 +397,7 @@ this._reviewersPendingRemove[type] = []; } } - }, + } /** * Removes an account from the change, both on the backend and the client. @@ -394,17 +411,17 @@ return this.$.restAPI.removeChangeReviewer(this.change._number, account._account_id).then(response => { - if (!response.ok) { return response; } + if (!response.ok) { return response; } - const reviewers = this.change.reviewers[type] || []; - for (let i = 0; i < reviewers.length; i++) { - if (reviewers[i]._account_id == account._account_id) { - this.splice(`change.reviewers.${type}`, i, 1); - break; - } - } - }); - }, + const reviewers = this.change.reviewers[type] || []; + for (let i = 0; i < reviewers.length; i++) { + if (reviewers[i]._account_id == account._account_id) { + this.splice(`change.reviewers.${type}`, i, 1); + break; + } + } + }); + } _mapReviewer(reviewer) { let reviewerId; @@ -416,7 +433,7 @@ confirmed = reviewer.group.confirmed; } return {reviewer: reviewerId, confirmed}; - }, + } send(includeComments, startReview) { this.$.reporting.time(SEND_REPLY_TIMING_LABEL); @@ -479,7 +496,7 @@ this.disabled = false; throw err; }); - }, + } _focusOn(section) { // Safeguard- always want to focus on something. @@ -497,7 +514,7 @@ const ccEntry = this.$.ccs.focusStart; ccEntry.async(ccEntry.focus); } - }, + } _chooseFocusTarget() { // If we are the owner and the reviewers field is empty, focus on that. @@ -509,7 +526,7 @@ // Default to BODY. return FocusTarget.BODY; - }, + } _handle400Error(response) { // A call to _saveReview could fail with a server error if erroneous @@ -551,11 +568,11 @@ this.fire('server-error', {response}); return null; // Means that the error has been handled. }); - }, + } _computeHideDraftList(drafts) { return Object.keys(drafts || {}).length == 0; - }, + } _computeDraftsTitle(drafts) { let total = 0; @@ -567,13 +584,13 @@ if (total == 0) { return ''; } if (total == 1) { return '1 Draft'; } if (total > 1) { return total + ' Drafts'; } - }, + } _computeMessagePlaceholder(canBeStarted) { return canBeStarted ? 'Add a note for your reviewers...' : 'Say something nice...'; - }, + } _changeUpdated(changeRecord, owner) { // Polymer 2: check for undefined @@ -582,7 +599,7 @@ } this._rebuildReviewerArrays(changeRecord.base, owner); - }, + } _rebuildReviewerArrays(change, owner) { this._owner = owner; @@ -614,16 +631,17 @@ this._ccs = ccs; this._reviewers = reviewers; - }, + } _accountOrGroupKey(entry) { return entry.id || entry._account_id; - }, + } /** * Generates a function to filter out reviewer/CC entries. When isCCs is * truthy, the function filters out entries that already exist in this._ccs. * When falsy, the function filters entries that exist in this._reviewers. + * * @param {boolean} isCCs * @return {!Function} */ @@ -650,25 +668,25 @@ } return this._reviewers.find(finder) === undefined; }; - }, + } _getAccount() { return this.$.restAPI.getAccount(); - }, + } _cancelTapHandler(e) { e.preventDefault(); this.cancel(); - }, + } cancel() { this.fire('cancel', null, {bubbles: false}); this.$.textarea.closeDropdown(); this._purgeReviewersPendingRemove(true); this._rebuildReviewerArrays(this.change.reviewers, this._owner); - }, + } - _saveTapHandler(e) { + _saveClickHandler(e) { e.preventDefault(); if (!this.$.ccs.submitEntryText()) { // Do not proceed with the save if there is an invalid email entry in @@ -678,12 +696,12 @@ this.send(this._includeComments, false).then(keepReviewers => { this._purgeReviewersPendingRemove(false, keepReviewers); }); - }, + } _sendTapHandler(e) { e.preventDefault(); this._submit(); - }, + } _submit() { if (!this.$.ccs.submitEntryText()) { @@ -710,12 +728,12 @@ detail: {message: `Error submitting review ${err}`}, })); }); - }, + } _saveReview(review, opt_errFn) { return this.$.restAPI.saveChangeReview(this.change._number, this.patchNum, review, opt_errFn); - }, + } _reviewerPendingConfirmationUpdated(reviewer) { if (reviewer === null) { @@ -725,7 +743,7 @@ this._ccPendingConfirmation || this._reviewerPendingConfirmation; this.$.reviewerConfirmationOverlay.open(); } - }, + } _confirmPendingReviewer() { if (this._ccPendingConfirmation) { @@ -735,7 +753,7 @@ this.$.reviewers.confirmGroup(this._reviewerPendingConfirmation.group); this._focusOn(FocusTarget.REVIEWERS); } - }, + } _cancelPendingReviewer() { this._ccPendingConfirmation = null; @@ -744,7 +762,7 @@ const target = this._ccPendingConfirmation ? FocusTarget.CCS : FocusTarget.REVIEWERS; this._focusOn(target); - }, + } _getStorageLocation() { // Tests trigger this method without setting change. @@ -754,12 +772,12 @@ patchNum: '@change', path: '@change', }; - }, + } _loadStoredDraft() { const draft = this.$.storage.getDraftComment(this._getStorageLocation()); return draft ? draft.message : ''; - }, + } _handleAccountTextEntry() { // When either of the account entries has input added to the autocomplete, @@ -767,7 +785,7 @@ // // Note: if the text is removed, the save button will not get disabled. this._reviewersMutated = true; - }, + } _draftChanged(newDraft, oldDraft) { this.debounce('store', () => { @@ -780,37 +798,37 @@ this.draft); } }, STORAGE_DEBOUNCE_INTERVAL_MS); - }, + } _handleHeightChanged(e) { this.fire('autogrow'); - }, + } _handleLabelsChanged() { this._labelsChanged = Object.keys( this.$.labelScores.getLabelValues()).length !== 0; - }, + } _isState(knownLatestState, value) { return knownLatestState === value; - }, + } _reload() { // Load the current change without any patch range. location.href = this.getBaseUrl() + '/c/' + this.change._number; - }, + } _computeSendButtonLabel(canBeStarted) { return canBeStarted ? ButtonLabels.START_REVIEW : ButtonLabels.SEND; - }, + } _computeSendButtonTooltip(canBeStarted) { return canBeStarted ? ButtonTooltips.START_REVIEW : ButtonTooltips.SEND; - }, + } _computeSavingLabelClass(savingComments) { return savingComments ? 'saving' : ''; - }, + } _computeSendButtonDisabled(buttonLabel, drafts, text, reviewersMutated, labelsChanged, includeComments, disabled) { @@ -831,7 +849,7 @@ if (buttonLabel === ButtonLabels.START_REVIEW) { return false; } const hasDrafts = includeComments && Object.keys(drafts).length; return !hasDrafts && !text.length && !reviewersMutated && !labelsChanged; - }, + } _computePatchSetWarning(patchNum, labelsChanged) { let str = `Patch ${patchNum} is not latest.`; @@ -839,28 +857,30 @@ str += ' Voting on a non-latest patch will have no effect.'; } return str; - }, + } setPluginMessage(message) { this._pluginMessage = message; - }, + } _sendDisabledChanged(sendDisabled) { this.dispatchEvent(new CustomEvent('send-disabled-changed')); - }, + } _getReviewerSuggestionsProvider(change) { const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI, change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER); provider.init(); return provider; - }, + } _getCcSuggestionsProvider(change) { const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI, change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.CC); provider.init(); return provider; - }, - }); + } + } + + customElements.define(GrReplyDialog.is, GrReplyDialog); })();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html index c7a1376..e44f669 100644 --- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-reply-dialog</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -187,7 +187,7 @@ }); test('keep drafts with reply', done => { - MockInteractions.tap(element.$$('#includeComments')); + MockInteractions.tap(element.shadowRoot.querySelector('#includeComments')); assert.equal(element._includeComments, false); // Async tick is needed because iron-selector content is distributed and @@ -414,8 +414,8 @@ }).then(() => { assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay)); const additions = cc ? - element.$.ccs.additions() : - element.$.reviewers.additions(); + element.$.ccs.additions() : + element.$.reviewers.additions(); assert.deepEqual( additions, [ @@ -460,7 +460,8 @@ flushAsynchronousOperations(); assert.isFalse(element._reviewersMutated); assert.isTrue(element.$.ccs.allowAnyInput); - assert.isFalse(element.$$('#reviewers').allowAnyInput); + assert.isFalse(element.shadowRoot + .querySelector('#reviewers').allowAnyInput); element.$.ccs.dispatchEvent(new CustomEvent('account-text-changed', {bubbles: true, composed: true})); assert.isTrue(element._reviewersMutated); @@ -843,7 +844,7 @@ // Send and purge and verify moves, delete cc3. element.send() .then(keepReviewers => - element._purgeReviewersPendingRemove(false, keepReviewers)) + element._purgeReviewersPendingRemove(false, keepReviewers)) .then(() => { assert.deepEqual( mutations, [
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js index d8df9d4..252a490 100644 --- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js +++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -17,70 +17,75 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-reviewer-list', - + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrReviewerList extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-reviewer-list'; } /** * Fired when the "Add reviewer..." button is tapped. * * @event show-reply-dialog */ - properties: { - change: Object, - disabled: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - mutable: { - type: Boolean, - value: false, - }, - reviewersOnly: { - type: Boolean, - value: false, - }, - ccsOnly: { - type: Boolean, - value: false, - }, - maxReviewersDisplayed: Number, + static get properties() { + return { + change: Object, + disabled: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + mutable: { + type: Boolean, + value: false, + }, + reviewersOnly: { + type: Boolean, + value: false, + }, + ccsOnly: { + type: Boolean, + value: false, + }, + maxReviewersDisplayed: Number, - _displayedReviewers: { - type: Array, - value() { return []; }, - }, - _reviewers: { - type: Array, - value() { return []; }, - }, - _showInput: { - type: Boolean, - value: false, - }, - _addLabel: { - type: String, - computed: '_computeAddLabel(ccsOnly)', - }, - _hiddenReviewerCount: { - type: Number, - computed: '_computeHiddenCount(_reviewers, _displayedReviewers)', - }, + _displayedReviewers: { + type: Array, + value() { return []; }, + }, + _reviewers: { + type: Array, + value() { return []; }, + }, + _showInput: { + type: Boolean, + value: false, + }, + _addLabel: { + type: String, + computed: '_computeAddLabel(ccsOnly)', + }, + _hiddenReviewerCount: { + type: Number, + computed: '_computeHiddenCount(_reviewers, _displayedReviewers)', + }, + // Used for testing. + _lastAutocompleteRequest: Object, + _xhrPromise: Object, + }; + } - // Used for testing. - _lastAutocompleteRequest: Object, - _xhrPromise: Object, - }, - - behaviors: [ - Gerrit.FireBehavior, - ], - - observers: [ - '_reviewersChanged(change.reviewers.*, change.owner)', - ], + static get observers() { + return [ + '_reviewersChanged(change.reviewers.*, change.owner)', + ]; + } /** * Converts change.permitted_labels to an array of hashes of label keys to @@ -101,10 +106,11 @@ label, scores: labels[label].map(v => parseInt(v, 10)), })); - }, + } /** * Returns hash of labels to max permitted score. + * * @param {!Object} change * @returns {!Object} labels to max permitted scores hash */ @@ -115,10 +121,11 @@ .map(v => parseInt(v, 10)) .reduce((a, b) => Math.max(a, b))})) .reduce((acc, i) => Object.assign(acc, i), {}); - }, + } /** * Returns max permitted score for reviewer. + * * @param {!Object} reviewer * @param {!Object} change * @param {string} label @@ -140,7 +147,7 @@ return 0; } return NaN; - }, + } _computeReviewerTooltip(reviewer, change) { if (!change || !change.labels) { return ''; } @@ -161,7 +168,7 @@ } else { return ''; } - }, + } _reviewersChanged(changeRecord, owner) { // Polymer 2: check for undefined @@ -195,7 +202,7 @@ } else { this._displayedReviewers = this._reviewers; } - }, + } _computeHiddenCount(reviewers, displayedReviewers) { // Polymer 2: check for undefined @@ -204,7 +211,7 @@ } return reviewers.length - displayedReviewers.length; - }, + } _computeCanRemoveReviewer(reviewer, mutable) { if (!mutable) { return false; } @@ -218,7 +225,7 @@ } } return false; - }, + } _handleRemove(e) { e.preventDefault(); @@ -246,7 +253,7 @@ this.disabled = false; throw err; }); - }, + } _handleAddTap(e) { e.preventDefault(); @@ -258,18 +265,20 @@ value.ccsOnly = true; } this.fire('show-reply-dialog', {value}); - }, + } _handleViewAll(e) { this._displayedReviewers = this._reviewers; - }, + } _removeReviewer(id) { return this.$.restAPI.removeChangeReviewer(this.change._number, id); - }, + } _computeAddLabel(ccsOnly) { return ccsOnly ? 'Add CC' : 'Add reviewer'; - }, - }); + } + } + + customElements.define(GrReviewerList.is, GrReviewerList); })();
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html index ae4934c..18f7bc5 100644 --- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-reviewer-list</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -196,7 +196,7 @@ element.maxReviewersDisplayed = 5; for (let i = 0; i < 6; i++) { reviewers.push( - {email: i+'reviewer@google.com', name: 'reviewer-' + i}); + {email: i+'reviewer@google.com', name: 'reviewer-' + i}); } element.ccsOnly = true; @@ -219,7 +219,7 @@ element.maxReviewersDisplayed = 5; for (let i = 0; i < 8; i++) { reviewers.push( - {email: i+'reviewer@google.com', name: 'reviewer-' + i}); + {email: i+'reviewer@google.com', name: 'reviewer-' + i}); } element.ccsOnly = true; @@ -237,12 +237,11 @@ assert.isFalse(element.$$('.hiddenReviewers').hidden); }); - test('no maxReviewersDisplayed', () => { const reviewers = []; for (let i = 0; i < 7; i++) { reviewers.push( - {email: i+'reviewer@google.com', name: 'reviewer-' + i}); + {email: i+'reviewer@google.com', name: 'reviewer-' + i}); } element.ccsOnly = true; @@ -265,7 +264,7 @@ element.maxReviewersDisplayed = 5; for (let i = 0; i < 100; i++) { reviewers.push( - {email: i+'reviewer@google.com', name: 'reviewer-' + i}); + {email: i+'reviewer@google.com', name: 'reviewer-' + i}); } element.ccsOnly = true; @@ -298,7 +297,7 @@ }, Bar: { all: [{_account_id: 1, permitted_voting_range: {max: 1}}, - {_account_id: 7, permitted_voting_range: {max: 1}}], + {_account_id: 7, permitted_voting_range: {max: 1}}], }, FooBar: { all: [{_account_id: 7, value: 0}],
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js index be2f0ea..de58d72 100644 --- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js +++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
@@ -22,39 +22,42 @@ * * @event thread-list-modified */ + class GrThreadList extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-thread-list'; } - Polymer({ - is: 'gr-thread-list', - - properties: { + static get properties() { + return { /** @type {?} */ - change: Object, - threads: Array, - changeNum: String, - loggedIn: Boolean, - _sortedThreads: { - type: Array, - }, - _filteredThreads: { - type: Array, - computed: '_computeFilteredThreads(_sortedThreads, _unresolvedOnly, ' + - '_draftsOnly)', - }, - _unresolvedOnly: { - type: Boolean, - value: false, - }, - _draftsOnly: { - type: Boolean, - value: false, - }, - }, + change: Object, + threads: Array, + changeNum: String, + loggedIn: Boolean, + _sortedThreads: { + type: Array, + }, + _filteredThreads: { + type: Array, + computed: '_computeFilteredThreads(_sortedThreads, ' + + '_unresolvedOnly, _draftsOnly)', + }, + _unresolvedOnly: { + type: Boolean, + value: false, + }, + _draftsOnly: { + type: Boolean, + value: false, + }, + }; + } - observers: ['_computeSortedThreads(threads.*)'], + static get observers() { return ['_computeSortedThreads(threads.*)']; } _computeShowDraftToggle(loggedIn) { return loggedIn ? 'show' : ''; - }, + } /** * Order as follows: @@ -62,13 +65,14 @@ * - Unresolved threads without drafts (reverse chronological) * - Resolved threads with drafts (reverse chronological) * - Resolved threads without drafts (reverse chronological) + * * @param {!Object} changeRecord */ _computeSortedThreads(changeRecord) { const threads = changeRecord.base; if (!threads) { return []; } this._updateSortedThreads(threads); - }, + } _updateSortedThreads(threads) { this._sortedThreads = @@ -90,7 +94,7 @@ } return dateCompare ? dateCompare : c1.id.localeCompare(c2.id); }); - }, + } _computeFilteredThreads(sortedThreads, unresolvedOnly, draftsOnly) { // Polymer 2: check for undefined @@ -125,15 +129,15 @@ return c; } }).map(threadInfo => threadInfo.thread); - }, + } _getThreadWithSortInfo(thread) { const lastComment = thread.comments[thread.comments.length - 1] || {}; const lastNonDraftComment = (lastComment.__draft && thread.comments.length > 1) ? - thread.comments[thread.comments.length - 2] : - lastComment; + thread.comments[thread.comments.length - 2] : + lastComment; return { thread, @@ -143,7 +147,7 @@ hasDraft: !!lastComment.__draft, updated: lastComment.updated, }; - }, + } removeThread(rootId) { for (let i = 0; i < this.threads.length; i++) { @@ -154,11 +158,11 @@ return; } } - }, + } _handleThreadDiscard(e) { this.removeThread(e.detail.rootId); - }, + } _handleCommentsChanged(e) { // Reset threads so thread computations occur on deep array changes to @@ -167,10 +171,12 @@ this.dispatchEvent(new CustomEvent('thread-list-modified', {detail: {rootId: e.detail.rootId, path: e.detail.path}})); - }, + } _isOnParent(side) { return !!side; - }, - }); + } + } + + customElements.define(GrThreadList.is, GrThreadList); })();
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html index ff65aa8..b29246d 100644 --- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-thread-list</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -290,7 +290,6 @@ assert.equal(element._filteredThreads.includes(thread), true); }); - test('thread removal', () => { threadElements[1].fire('thread-discard', {rootId: 'rc2'}); flushAsynchronousOperations();
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js index 092204a..9a699ea 100644 --- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
@@ -27,41 +27,46 @@ 'pull', ]; - Polymer({ - is: 'gr-upload-help-dialog', - + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrUploadHelpDialog extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-upload-help-dialog'; } /** * Fired when the user presses the close button. * * @event close */ - properties: { - revision: Object, - targetBranch: String, - _commitCommand: { - type: String, - value: COMMIT_COMMAND, - readOnly: true, - }, - _fetchCommand: { - type: String, - computed: '_computeFetchCommand(revision, ' + + static get properties() { + return { + revision: Object, + targetBranch: String, + _commitCommand: { + type: String, + value: COMMIT_COMMAND, + readOnly: true, + }, + _fetchCommand: { + type: String, + computed: '_computeFetchCommand(revision, ' + '_preferredDownloadCommand, _preferredDownloadScheme)', - }, - _preferredDownloadCommand: String, - _preferredDownloadScheme: String, - _pushCommand: { - type: String, - computed: '_computePushCommand(targetBranch)', - }, - }, - - behaviors: [ - Gerrit.FireBehavior, - ], + }, + _preferredDownloadCommand: String, + _preferredDownloadScheme: String, + _pushCommand: { + type: String, + computed: '_computePushCommand(targetBranch)', + }, + }; + } attached() { + super.attached(); this.$.restAPI.getLoggedIn().then(loggedIn => { if (loggedIn) { return this.$.restAPI.getPreferences(); @@ -72,13 +77,13 @@ this._preferredDownloadScheme = prefs.download_scheme; } }); - }, + } _handleCloseTap(e) { e.preventDefault(); e.stopPropagation(); this.fire('close', null, {bubbles: false}); - }, + } _computeFetchCommand(revision, preferredDownloadCommand, preferredDownloadScheme) { @@ -126,10 +131,12 @@ } return undefined; - }, + } _computePushCommand(targetBranch) { return PUSH_COMMAND_PREFIX + targetBranch; - }, - }); + } + } + + customElements.define(GrUploadHelpDialog.is, GrUploadHelpDialog); })();
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html index 577b978..76377fb 100644 --- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-upload-help-dialog</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js index 7cbe988..6aac157 100644 --- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js +++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -19,29 +19,39 @@ const INTERPOLATE_URL_PATTERN = /\$\{([\w]+)\}/g; - Polymer({ - is: 'gr-account-dropdown', + /** + * @appliesMixin Gerrit.DisplayNameMixin + */ + class GrAccountDropdown extends Polymer.mixinBehaviors( [ + Gerrit.DisplayNameBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-account-dropdown'; } - properties: { - account: Object, - config: Object, - links: { - type: Array, - computed: '_getLinks(_switchAccountUrl, _path)', - }, - topContent: { - type: Array, - computed: '_getTopContent(account)', - }, - _path: { - type: String, - value: '/', - }, - _hasAvatars: Boolean, - _switchAccountUrl: String, - }, + static get properties() { + return { + account: Object, + config: Object, + links: { + type: Array, + computed: '_getLinks(_switchAccountUrl, _path)', + }, + topContent: { + type: Array, + computed: '_getTopContent(account)', + }, + _path: { + type: String, + value: '/', + }, + _hasAvatars: Boolean, + _switchAccountUrl: String, + }; + } attached() { + super.attached(); this._handleLocationChange(); this.listen(window, 'location-change', '_handleLocationChange'); this.$.restAPI.getConfig().then(cfg => { @@ -54,15 +64,12 @@ } this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars); }); - }, - - behaviors: [ - Gerrit.DisplayNameBehavior, - ], + } detached() { + super.detached(); this.unlisten(window, 'location-change', '_handleLocationChange'); - }, + } _getLinks(switchAccountUrl, path) { // Polymer 2: check for undefined @@ -78,30 +85,32 @@ } links.push({name: 'Sign out', url: '/logout'}); return links; - }, + } _getTopContent(account) { return [ {text: this._accountName(account), bold: true}, {text: account.email ? account.email : ''}, ]; - }, + } _handleLocationChange() { this._path = window.location.pathname + window.location.search + window.location.hash; - }, + } _interpolateUrl(url, replacements) { return url.replace(INTERPOLATE_URL_PATTERN, (match, p1) => { return replacements[p1] || ''; }); - }, + } _accountName(account) { return this.getUserName(this.config, account, true); - }, - }); + } + } + + customElements.define(GrAccountDropdown.is, GrAccountDropdown); })();
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html index e29faa8..9dafb74 100644 --- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html +++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-account-dropdown</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js index 8d3b58e..2655140 100644 --- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js +++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
@@ -17,21 +17,26 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-error-dialog', - + class GrErrorDialog extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-error-dialog'; } /** * Fired when the dismiss button is pressed. * * @event dismiss */ - properties: { - text: String, - }, + static get properties() { + return { + text: String, + }; + } _handleConfirm() { this.dispatchEvent(new CustomEvent('dismiss')); - }, - }); + } + } + + customElements.define(GrErrorDialog.is, GrErrorDialog); })();
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html index 648f8be..dd4a71a 100644 --- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html +++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-error-dialog</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js index 5865e3c..506cb73 100644 --- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js +++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -25,40 +25,47 @@ const TOO_MANY_FILES = 'too many files to find conflicts'; const AUTHENTICATION_REQUIRED = 'Authentication required\n'; - Polymer({ - is: 'gr-error-manager', + /** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + */ + class GrErrorManager extends Polymer.mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-error-manager'; } - behaviors: [ - Gerrit.BaseUrlBehavior, - Gerrit.FireBehavior, - ], - - properties: { + static get properties() { + return { /** * The ID of the account that was logged in when the app was launched. If * not set, then there was no account at launch. */ - knownAccountId: Number, + knownAccountId: Number, - /** @type {?Object} */ - _alertElement: Object, - /** @type {?number} */ - _hideAlertHandle: Number, - _refreshingCredentials: { - type: Boolean, - value: false, - }, + /** @type {?Object} */ + _alertElement: Object, + /** @type {?number} */ + _hideAlertHandle: Number, + _refreshingCredentials: { + type: Boolean, + value: false, + }, - /** - * The time (in milliseconds) since the most recent credential check. - */ - _lastCredentialCheck: { - type: Number, - value() { return Date.now(); }, - }, - }, + /** + * The time (in milliseconds) since the most recent credential check. + */ + _lastCredentialCheck: { + type: Number, + value() { return Date.now(); }, + }, + }; + } attached() { + super.attached(); this.listen(document, 'server-error', '_handleServerError'); this.listen(document, 'network-error', '_handleNetworkError'); this.listen(document, 'auth-error', '_handleAuthError'); @@ -66,9 +73,10 @@ this.listen(document, 'show-error', '_handleShowErrorDialog'); this.listen(document, 'visibilitychange', '_handleVisibilityChange'); this.listen(document, 'show-auth-required', '_handleAuthRequired'); - }, + } detached() { + super.detached(); this._clearHideAlertHandle(); this.unlisten(document, 'server-error', '_handleServerError'); this.unlisten(document, 'network-error', '_handleNetworkError'); @@ -76,20 +84,20 @@ this.unlisten(document, 'show-auth-required', '_handleAuthRequired'); this.unlisten(document, 'visibilitychange', '_handleVisibilityChange'); this.unlisten(document, 'show-error', '_handleShowErrorDialog'); - }, + } _shouldSuppressError(msg) { return msg.includes(TOO_MANY_FILES); - }, + } _handleAuthRequired() { this._showAuthErrorAlert( 'Log in is required to perform that action.', 'Log in.'); - }, + } _handleAuthError() { this._showAuthErrorAlert('Auth error', 'Refresh credentials.'); - }, + } _handleServerError(e) { const {request, response} = e.detail; @@ -113,7 +121,7 @@ } console.error(errorText); }); - }, + } _constructServerErrorMsg({errorText, status, statusText, url}) { let err = `Error ${status}`; @@ -122,21 +130,21 @@ if (errorText) { err += errorText; } if (url) { err += `\nEndpoint: ${url}`; } return err; - }, + } _handleShowAlert(e) { this._showAlert(e.detail.message, e.detail.action, e.detail.callback, e.detail.dismissOnNavigation); - }, + } _handleNetworkError(e) { this._showAlert('Server unavailable'); console.error(e.detail.error.message); - }, + } _getLoggedIn() { return this.$.restAPI.getLoggedIn(); - }, + } /** * @param {string} text @@ -161,7 +169,7 @@ const el = this._createToastAlert(); el.show(text, opt_actionText, opt_actionCallback); this._alertElement = el; - }, + } _hideAlert() { if (!this._alertElement) { return; } @@ -171,14 +179,14 @@ // Remove listener for page navigation, if it exists. this.unlisten(document, 'location-change', '_hideAlert'); - }, + } _clearHideAlertHandle() { if (this._hideAlertHandle != null) { this.cancelAsync(this._hideAlertHandle); this._hideAlertHandle = null; } - }, + } _showAuthErrorAlert(errorText, actionText) { // TODO(viktard): close alert if it's not for auth error. @@ -193,13 +201,13 @@ if (!document.hidden) { this._handleVisibilityChange(); } - }, + } _createToastAlert() { const el = document.createElement('gr-alert'); el.toast = true; return el; - }, + } _handleVisibilityChange() { // Ignore when the page is transitioning to hidden (or hidden is @@ -216,12 +224,12 @@ this._lastCredentialCheck = Date.now(); this.$.restAPI.checkCredentials(); } - }, + } _requestCheckLoggedIn() { this.debounce( 'checkLoggedIn', this._checkSignedIn, CHECK_SIGN_IN_INTERVAL_MS); - }, + } _checkSignedIn() { this.$.restAPI.checkCredentials().then(account => { @@ -242,11 +250,11 @@ } } }); - }, + } _reloadPage() { window.location.reload(); - }, + } _createLoginPopup() { const left = window.screenLeft + @@ -262,31 +270,33 @@ window.open(this.getBaseUrl() + '/login/%3FcloseAfterLogin', '_blank', options.join(',')); this.listen(window, 'focus', '_handleWindowFocus'); - }, + } _handleCredentialRefreshed() { this.unlisten(window, 'focus', '_handleWindowFocus'); this._refreshingCredentials = false; this._hideAlert(); this._showAlert('Credentials refreshed.'); - }, + } _handleWindowFocus() { this.flushDebouncer('checkLoggedIn'); - }, + } _handleShowErrorDialog(e) { this._showErrorDialog(e.detail.message); - }, + } _handleDismissErrorDialog() { this.$.errorOverlay.close(); - }, + } _showErrorDialog(message) { this.$.reporting.reportErrorDialog(message); this.$.errorDialog.text = message; this.$.errorOverlay.open(); - }, - }); + } + } + + customElements.define(GrErrorManager.is, GrErrorManager); })();
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html index 9140c17..84b6717 100644 --- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html +++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-error-manager</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js index 89d1091..2b86170 100644 --- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js +++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
@@ -17,20 +17,26 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-key-binding-display', + class GrKeyBindingDisplay extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-key-binding-display'; } - properties: { + static get properties() { + return { /** @type {Array<string>} */ - binding: Array, - }, + binding: Array, + }; + } _computeModifiers(binding) { return binding.slice(0, binding.length - 1); - }, + } _computeKey(binding) { return binding[binding.length - 1]; - }, - }); + } + } + + customElements.define(GrKeyBindingDisplay.is, GrKeyBindingDisplay); })();
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html index 39c8af8..e0d2d40 100644 --- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html +++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html
@@ -17,7 +17,7 @@ --> <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-key-binding-display</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js index 4bc6e11..788c9e0 100644 --- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js +++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
@@ -19,60 +19,68 @@ const {ShortcutSection} = window.Gerrit.KeyboardShortcutBinder; - Polymer({ - is: 'gr-keyboard-shortcuts-dialog', - + /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + */ + class GrKeyboardShortcutsDialog extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-keyboard-shortcuts-dialog'; } /** * Fired when the user presses the close button. * * @event close */ - properties: { - _left: Array, - _right: Array, + static get properties() { + return { + _left: Array, + _right: Array, - _propertyBySection: { - type: Object, - value() { - return { - [ShortcutSection.EVERYWHERE]: '_everywhere', - [ShortcutSection.NAVIGATION]: '_navigation', - [ShortcutSection.DASHBOARD]: '_dashboard', - [ShortcutSection.CHANGE_LIST]: '_changeList', - [ShortcutSection.ACTIONS]: '_actions', - [ShortcutSection.REPLY_DIALOG]: '_replyDialog', - [ShortcutSection.FILE_LIST]: '_fileList', - [ShortcutSection.DIFFS]: '_diffs', - }; + _propertyBySection: { + type: Object, + value() { + return { + [ShortcutSection.EVERYWHERE]: '_everywhere', + [ShortcutSection.NAVIGATION]: '_navigation', + [ShortcutSection.DASHBOARD]: '_dashboard', + [ShortcutSection.CHANGE_LIST]: '_changeList', + [ShortcutSection.ACTIONS]: '_actions', + [ShortcutSection.REPLY_DIALOG]: '_replyDialog', + [ShortcutSection.FILE_LIST]: '_fileList', + [ShortcutSection.DIFFS]: '_diffs', + }; + }, }, - }, - }, + }; + } - behaviors: [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - ], - - hostAttributes: { - role: 'dialog', - }, + ready() { + super.ready(); + this._ensureAttribute('role', 'dialog'); + } attached() { + super.attached(); this.addKeyboardShortcutDirectoryListener( this._onDirectoryUpdated.bind(this)); - }, + } detached() { + super.detached(); this.removeKeyboardShortcutDirectoryListener( this._onDirectoryUpdated.bind(this)); - }, + } _handleCloseTap(e) { e.preventDefault(); e.stopPropagation(); this.fire('close', null, {bubbles: false}); - }, + } _onDirectoryUpdated(directory) { const left = []; @@ -122,6 +130,9 @@ this.set('_left', left); this.set('_right', right); - }, - }); + } + } + + customElements.define(GrKeyboardShortcutsDialog.is, + GrKeyboardShortcutsDialog); })();
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html index 1a3d6c7..b713aa1 100644 --- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html +++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html
@@ -17,7 +17,7 @@ --> <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-key-binding-display</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js index 773ad68..d009225 100644 --- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js +++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -69,94 +69,107 @@ 'CUSTOM_EXTENSION', ]); - Polymer({ - is: 'gr-main-header', + /** + * @appliesMixin Gerrit.AdminNavMixin + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.DocsUrlMixin + * @appliesMixin Gerrit.FireMixin + */ + class GrMainHeader extends Polymer.mixinBehaviors( [ + Gerrit.AdminNavBehavior, + Gerrit.BaseUrlBehavior, + Gerrit.DocsUrlBehavior, + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-main-header'; } - hostAttributes: { - role: 'banner', - }, - - properties: { - searchQuery: { - type: String, - notify: true, - }, - loggedIn: { - type: Boolean, - reflectToAttribute: true, - }, - loading: { - type: Boolean, - reflectToAttribute: true, - }, - - /** @type {?Object} */ - _account: Object, - _adminLinks: { - type: Array, - value() { return []; }, - }, - _defaultLinks: { - type: Array, - value() { - return DEFAULT_LINKS; + static get properties() { + return { + searchQuery: { + type: String, + notify: true, }, - }, - _docBaseUrl: { - type: String, - value: null, - }, - _links: { - type: Array, - computed: '_computeLinks(_defaultLinks, _userLinks, _adminLinks, ' + + loggedIn: { + type: Boolean, + reflectToAttribute: true, + }, + loading: { + type: Boolean, + reflectToAttribute: true, + }, + + /** @type {?Object} */ + _account: Object, + _adminLinks: { + type: Array, + value() { return []; }, + }, + _defaultLinks: { + type: Array, + value() { + return DEFAULT_LINKS; + }, + }, + _docBaseUrl: { + type: String, + value: null, + }, + _links: { + type: Array, + computed: '_computeLinks(_defaultLinks, _userLinks, _adminLinks, ' + '_topMenus, _docBaseUrl)', - }, - _loginURL: { - type: String, - value: '/login', - }, - _userLinks: { - type: Array, - value() { return []; }, - }, - _topMenus: { - type: Array, - value() { return []; }, - }, - _registerText: { - type: String, - value: 'Sign up', - }, - _registerURL: { - type: String, - value: null, - }, - }, + }, + _loginURL: { + type: String, + value: '/login', + }, + _userLinks: { + type: Array, + value() { return []; }, + }, + _topMenus: { + type: Array, + value() { return []; }, + }, + _registerText: { + type: String, + value: 'Sign up', + }, + _registerURL: { + type: String, + value: null, + }, + }; + } - behaviors: [ - Gerrit.AdminNavBehavior, - Gerrit.BaseUrlBehavior, - Gerrit.DocsUrlBehavior, - Gerrit.FireBehavior, - ], + static get observers() { + return [ + '_accountLoaded(_account)', + ]; + } - observers: [ - '_accountLoaded(_account)', - ], + ready() { + super.ready(); + this._ensureAttribute('role', 'banner'); + } attached() { + super.attached(); this._loadAccount(); this._loadConfig(); this.listen(window, 'location-change', '_handleLocationChange'); - }, + } detached() { + super.detached(); this.unlisten(window, 'location-change', '_handleLocationChange'); - }, + } reload() { this._loadAccount(); - }, + } _handleLocationChange(e) { const baseUrl = this.getBaseUrl(); @@ -173,11 +186,11 @@ window.location.search + window.location.hash); } - }, + } _computeRelativeURL(path) { return '//' + window.location.host + this.getBaseUrl() + path; - }, + } _computeLinks(defaultLinks, userLinks, adminLinks, topMenus, docBaseUrl) { // Polymer 2: check for undefined @@ -232,7 +245,7 @@ } } return links; - }, + } _getDocLinks(docBaseUrl, docLinks) { if (!docBaseUrl || !docLinks) { @@ -249,7 +262,7 @@ target: '_blank', }; }); - }, + } _loadAccount() { this.loading = true; @@ -273,7 +286,7 @@ this._adminLinks = res.links; }); }); - }, + } _loadConfig() { this.$.restAPI.getConfig() @@ -282,7 +295,7 @@ return this.getDocsBaseUrl(config, this.$.restAPI); }) .then(docBaseUrl => { this._docBaseUrl = docBaseUrl; }); - }, + } _accountLoaded(account) { if (!account) { return; } @@ -290,7 +303,7 @@ this.$.restAPI.getPreferences().then(prefs => { this._userLinks = prefs.my.map(this._fixCustomMenuItem); }); - }, + } _retrieveRegisterURL(config) { if (AUTH_TYPES_WITH_REGISTER_URL.has(config.auth.auth_type)) { @@ -299,11 +312,11 @@ this._registerText = config.auth.register_text; } } - }, + } _computeIsInvisible(registerURL) { return registerURL ? '' : 'invisible'; - }, + } _fixCustomMenuItem(linkObj) { // Normalize all urls to PolyGerrit style. @@ -321,17 +334,17 @@ delete linkObj.target; return linkObj; - }, + } _generateSettingsLink() { return this.getBaseUrl() + '/settings/'; - }, + } _onMobileSearchTap(e) { e.preventDefault(); e.stopPropagation(); this.fire('mobile-search', null, {bubbles: false}); - }, + } _computeLinkGroupClass(linkGroup) { if (linkGroup && linkGroup.class) { @@ -339,6 +352,8 @@ } return ''; - }, - }); + } + } + + customElements.define(GrMainHeader.is, GrMainHeader); })();
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html index 81097f5..403d9d5 100644 --- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html +++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-main-header</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -78,7 +78,7 @@ 'none'); assert.notEqual(getComputedStyle(element.$$('gr-account-dropdown')) .display, - 'none'); + 'none'); assert.notEqual(getComputedStyle(element.$$('.settingsButton')).display, 'none'); }); @@ -118,26 +118,26 @@ /* topMenus= */[], /* docBaseUrl= */ '' ), - defaultLinks.concat({ - title: 'Browse', - links: adminLinks, - })); + defaultLinks.concat({ + title: 'Browse', + links: adminLinks, + })); assert.deepEqual(element._computeLinks( defaultLinks, userLinks, adminLinks, /* topMenus= */[], /* docBaseUrl= */ '' - ), - defaultLinks.concat([ - { - title: 'Your', - links: userLinks, - }, - { - title: 'Browse', - links: adminLinks, - }]) + ), + defaultLinks.concat([ + { + title: 'Your', + links: userLinks, + }, + { + title: 'Browse', + links: adminLinks, + }]) ); });
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html index 9eaf603..e79277a 100644 --- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html +++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -224,6 +224,7 @@ /** * Setup router implementation. + * * @param {function(!string)} navigate the router-abstracted equivalent of * `window.location.href = ...`. Takes a string. * @param {function(!Object): string} generateUrl generates a URL given @@ -260,6 +261,7 @@ /** * Generate a URL for the given route parameters. + * * @param {Object} params * @return {string} */ @@ -336,6 +338,7 @@ /** * Navigate to a search for changes with the given status. + * * @param {string} status */ navigateToStatusSearch(status) { @@ -347,6 +350,7 @@ /** * Navigate to a search query + * * @param {string} query * @param {number=} opt_offset */ @@ -548,6 +552,7 @@ /** * Navigate to an arbitrary relative URL. + * * @param {string} relativeUrl */ navigateToRelativeUrl(relativeUrl) { @@ -570,6 +575,7 @@ /** * Navigate to a repo settings page. + * * @param {string} repoName */ navigateToRepo(repoName) { @@ -732,11 +738,11 @@ getUserDashboard(user = 'self', sections = DEFAULT_SECTIONS, title = '') { sections = sections - .filter(section => (user === 'self' || !section.selfOnly)) - .map(section => Object.assign({}, section, { - name: section.name, - query: section.query.replace(USER_PLACEHOLDER_PATTERN, user), - })); + .filter(section => (user === 'self' || !section.selfOnly)) + .map(section => Object.assign({}, section, { + name: section.name, + query: section.query.replace(USER_PLACEHOLDER_PATTERN, user), + })); return {title, sections}; }, };
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html index 73ce86a..ca44430 100644 --- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html +++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-navigation</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js index 30d322e..5c98281 100644 --- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js +++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -24,7 +24,6 @@ CATEGORY_RPC: 'RPC Timing', // Reported events - alphabetize below. APP_STARTED: 'App Started', - PAGE_LOADED: 'Page Loaded', }; // Plugin-related reporting constants. @@ -41,14 +40,6 @@ DETECTED: 'Extension detected', }; - // Page visibility related constants. - const PAGE_VISIBILITY = { - TYPE: 'lifecycle', - CATEGORY: 'Page Visibility', - // Reported events - alphabetize below. - STARTED_HIDDEN: 'hidden', - }; - // Navigation reporting constants. const NAVIGATION = { TYPE: 'nav-report', @@ -107,8 +98,10 @@ const pending = []; + // Variables that hold context info in global scope const loadedPlugins = []; const detectedExtensions = []; + let reportRepoName = undefined; const onError = function(oldOnError, msg, url, line, column, error) { if (oldOnError) { @@ -148,6 +141,25 @@ }; catchErrors(); + // PerformanceObserver interface is a browser API. + if (window.PerformanceObserver) { + const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || []; + // Safari doesn't support longtask yet + if (supportedEntryTypes.includes('longtask')) { + const catchLongJsTasks = new PerformanceObserver(list => { + for (const task of list.getEntries()) { + // We are interested in longtask longer than 200 ms (default is 50 ms) + if (task.duration > 200) { + GrReporting.prototype.reporter(TIMING.TYPE, + TIMING.CATEGORY_UI_LATENCY, `Task ${task.name}`, + Math.round(task.duration), false); + } + } + }); + catchLongJsTasks.observe({entryTypes: ['longtask']}); + } + } + // The Polymer pass of JSCompiler requires this to be reassignable // eslint-disable-next-line prefer-const let GrReporting = Polymer({ @@ -172,7 +184,7 @@ }, now() { - return window.performance.now(); + return Math.round(window.performance.now()); }, _arePluginsLoaded() { @@ -188,32 +200,48 @@ reporter(...args) { const report = (this._isMetricsPluginLoaded() && !pending.length) ? this.defaultReporter : this.cachingReporter; - args.splice(4, 0, loadedPlugins, detectedExtensions); + const contextInfo = { + loadedPlugins, + detectedExtensions, + repoName: reportRepoName, + isInBackgroundTab: document.visibilityState === 'hidden', + startTimeMs: this.now(), + }; + args.splice(4, 0, contextInfo); report.apply(this, args); }, /** * The default reporter reports events immediately. + * * @param {string} type * @param {string} category * @param {string} eventName * @param {string|number} eventValue - * @param {Array} plugins - * @param {Array} extensions + * @param {Object} contextInfo * @param {boolean|undefined} opt_noLog If true, the event will not be * logged to the JS console. */ - defaultReporter(type, category, eventName, eventValue, - loadedPlugins, detectedExtensions, opt_noLog) { + defaultReporter(type, category, eventName, eventValue, contextInfo, + opt_noLog) { const detail = { type, category, name: eventName, value: eventValue, }; - if (category === TIMING.CATEGORY_UI_LATENCY) { - detail.loadedPlugins = loadedPlugins; - detail.detectedExtensions = detectedExtensions; + if (category === TIMING.CATEGORY_UI_LATENCY && contextInfo) { + detail.loadedPlugins = contextInfo.loadedPlugins; + detail.detectedExtensions = contextInfo.detectedExtensions; + } + if (contextInfo && contextInfo.repoName) { + detail.repoName = contextInfo.repoName; + } + if (contextInfo && contextInfo.isInBackgroundTab !== undefined) { + detail.inBackgroundTab = contextInfo.isInBackgroundTab; + } + if (contextInfo && contextInfo.startTimeMs) { + detail.eventStart = contextInfo.startTimeMs; } document.dispatchEvent(new CustomEvent(type, {detail})); if (opt_noLog) { return; } @@ -231,57 +259,65 @@ /** * The caching reporter will queue reports until plugins have loaded, and * log events immediately if they're reported after plugins have loaded. + * * @param {string} type * @param {string} category * @param {string} eventName * @param {string|number} eventValue - * @param {Array} plugins - * @param {Array} extensions + * @param {Object} contextInfo * @param {boolean|undefined} opt_noLog If true, the event will not be * logged to the JS console. */ - cachingReporter(type, category, eventName, eventValue, - plugins, extensions, opt_noLog) { + cachingReporter(type, category, eventName, eventValue, contextInfo, + opt_noLog) { if (type === ERROR.TYPE && category === ERROR.CATEGORY) { console.error(eventValue && eventValue.error || eventName); } if (this._isMetricsPluginLoaded()) { if (pending.length) { for (const args of pending.splice(0)) { - this.reporter(...args); + this.defaultReporter(...args); } } - this.reporter(type, category, eventName, eventValue, - plugins, extensions, opt_noLog); + this.defaultReporter(type, category, eventName, eventValue, contextInfo, + opt_noLog); } else { - pending.push([type, category, eventName, eventValue, - plugins, extensions, opt_noLog]); + pending.push([type, category, eventName, eventValue, contextInfo, + opt_noLog]); } }, /** * User-perceived app start time, should be reported when the app is ready. */ - appStarted(hidden) { + appStarted() { this.timeEnd(TIMING.APP_STARTED); - if (hidden) { - this.reporter(PAGE_VISIBILITY.TYPE, PAGE_VISIBILITY.CATEGORY, - PAGE_VISIBILITY.STARTED_HIDDEN); - } }, /** - * Page load time, should be reported at any time after navigation. + * Page load time and other metrics, should be reported at any time + * after navigation. */ pageLoaded() { if (this.performanceTiming.loadEventEnd === 0) { console.error('pageLoaded should be called after window.onload'); this.async(this.pageLoaded, 100); } else { - const loadTime = this.performanceTiming.loadEventEnd - + const perfEvents = Object.keys(this.performanceTiming.toJSON()); + perfEvents.forEach( + eventName => this._reportPerformanceTiming(eventName) + ); + } + }, + + _reportPerformanceTiming(eventName) { + const eventTiming = this.performanceTiming[eventName]; + if (eventTiming > 0) { + const elapsedTime = eventTiming - this.performanceTiming.navigationStart; + // NavResTime - Navigation and resource timings. this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY, - TIMING.PAGE_LOADED, loadTime); + `NavResTime - ${eventName}`, elapsedTime, true); } }, @@ -296,6 +332,7 @@ this.time(TIMER.DIFF_VIEW_DISPLAYED); this.time(TIMER.DIFF_VIEW_LOAD_FULL); this.time(TIMER.FILE_LIST_DISPLAYED); + reportRepoName = undefined; }, locationChanged(page) { @@ -406,6 +443,7 @@ /** * Reports just line timeEnd, but additionally reports an average given a * denominator and a separate reporiting name for the average. + * * @param {string} name Timing name. * @param {string} averageName Average timing name. * @param {number} denominator Number by which to divide the total to @@ -418,24 +456,25 @@ // Guard against division by zero. if (!denominator) { return; } - const time = Math.round(this.now() - baseTime); + const time = this.now() - baseTime; this._reportTiming(averageName, time / denominator); }, /** * Send a timing report with an arbitrary time value. + * * @param {string} name Timing name. * @param {number} time The time to report as an integer of milliseconds. */ _reportTiming(name, time) { - this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY, name, - Math.round(time)); + this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY, name, time); }, /** * Get a timer object to for reporing a user timing. The start time will be * the time that the object has been created, and the end time will be the * time that the "end" method is called on the object. + * * @param {string} name Timing name. * @returns {!Object} The timer object. */ @@ -482,6 +521,7 @@ /** * Log timing information for an RPC. + * * @param {string} anonymizedUrl The URL of the RPC with tokens obfuscated. * @param {number} elapsed The time elapsed of the RPC. */ @@ -518,6 +558,10 @@ this.reporter(ERROR_DIALOG.TYPE, ERROR_DIALOG.CATEGORY, 'ErrorDialog: ' + message, {error: new Error(message)}); }, + + setRepoName(repoName) { + reportRepoName = repoName; + }, }); window.GrReporting = GrReporting;
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html index c357979..c7aa896 100644 --- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html +++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-reporting</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -52,6 +52,7 @@ navigationStart: 1, loadEventEnd: 2, }; + fakePerformance.toJSON = () => fakePerformance; sinon.stub(element, 'performanceTiming', {get() { return fakePerformance; }}); sandbox.stub(element, 'reporter'); @@ -64,15 +65,11 @@ test('appStarted', () => { sandbox.stub(element, 'now').returns(42); - element.appStarted(true); + element.appStarted(); assert.isTrue( element.reporter.calledWithExactly( 'timing-report', 'UI Latency', 'App Started', 42 - )); - assert.isTrue( - element.reporter.calledWithExactly( - 'lifecycle', 'Page Visibility', 'hidden' - )); + )); }); test('WebComponentsReady', () => { @@ -87,8 +84,9 @@ element.pageLoaded(); assert.isTrue( element.reporter.calledWithExactly( - 'timing-report', 'UI Latency', 'Page Loaded', - fakePerformance.loadEventEnd - fakePerformance.navigationStart) + 'timing-report', 'UI Latency', 'NavResTime - loadEventEnd', + fakePerformance.loadEventEnd - fakePerformance.navigationStart, + true) ); }); @@ -162,14 +160,14 @@ test('time and timeEnd', () => { const nowStub = sandbox.stub(element, 'now').returns(0); element.time('foo'); - nowStub.returns(1.1); + nowStub.returns(1); element.time('bar'); nowStub.returns(2); element.timeEnd('bar'); - nowStub.returns(3.511); + nowStub.returns(3); element.timeEnd('foo'); assert.isTrue(element.reporter.calledWithExactly( - 'timing-report', 'UI Latency', 'foo', 4 + 'timing-report', 'UI Latency', 'foo', 3 )); assert.isTrue(element.reporter.calledWithExactly( 'timing-report', 'UI Latency', 'bar', 1 @@ -251,6 +249,21 @@ )); }); + test('report start time', () => { + element.reporter.restore(); + sandbox.stub(element, 'now').returns(42); + sandbox.spy(element, 'defaultReporter'); + const dispatchStub = sandbox.spy(document, 'dispatchEvent'); + element.pluginsLoaded(); + element.time('timeAction'); + element.timeEnd('timeAction'); + assert.isTrue(element.defaultReporter.getCall(2).calledWithMatch( + 'timing-report', 'UI Latency', 'timeAction', 0, + {startTimeMs: 42} + )); + assert.equal(dispatchStub.getCall(2).args[0].detail.eventStart, 42); + }); + suite('plugins', () => { setup(() => { element.reporter.restore(); @@ -289,9 +302,9 @@ // element.pluginLoaded('foo'); element.time('timeAction'); element.timeEnd('timeAction'); - assert.isTrue(element.defaultReporter.getCall(1).calledWith( + assert.isTrue(element.defaultReporter.getCall(1).calledWithMatch( 'timing-report', 'UI Latency', 'timeAction', 0, - ['metrics-xyz1'] + {loadedPlugins: ['metrics-xyz1']} )); });
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js index b0fc5b3..e84beb6 100644 --- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js +++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -106,6 +106,7 @@ /** * Support vestigial params from GWT UI. + * * @see Issue 7673. * @type {!RegExp} */ @@ -162,6 +163,7 @@ * the hash of diff URLs. In this format, a number on its own indicates that * line number in the revision of the diff. A number prefixed by either an 'a' * or a 'b' indicates that line number of the base of the diff. + * * @type {RegExp} */ const LINE_ADDRESS_PATTERN = /^([ab]?)(\d+)$/; @@ -207,38 +209,46 @@ }); })(); - Polymer({ - is: 'gr-router', + /** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.URLEncodingMixin + */ + class GrRouter extends Polymer.mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.FireBehavior, + Gerrit.PatchSetBehavior, + Gerrit.URLEncodingBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-router'; } - properties: { - _app: { - type: Object, - value: app, - }, - _isRedirecting: Boolean, - // This variable is to differentiate between internal navigation (false) - // and for first navigation in app after loaded from server (true). - _isInitialLoad: { - type: Boolean, - value: true, - }, - }, - - behaviors: [ - Gerrit.BaseUrlBehavior, - Gerrit.FireBehavior, - Gerrit.PatchSetBehavior, - Gerrit.URLEncodingBehavior, - ], + static get properties() { + return { + _app: { + type: Object, + value: app, + }, + _isRedirecting: Boolean, + // This variable is to differentiate between internal navigation (false) + // and for first navigation in app after loaded from server (true). + _isInitialLoad: { + type: Boolean, + value: true, + }, + }; + } start() { if (!this._app) { return; } this._startRouter(); - }, + } _setParams(params) { this._appElement().params = params; - }, + } _appElement() { // In Polymer2 you have to reach through the shadow root of the app @@ -248,12 +258,12 @@ return document.getElementById('app-element') || document.getElementById('app').shadowRoot.getElementById( 'app-element'); - }, + } _redirect(url) { this._isRedirecting = true; page.redirect(url); - }, + } /** * @param {!Object} params @@ -285,7 +295,7 @@ } return base + url; - }, + } _generateWeblinks(params) { const type = params.type; @@ -299,7 +309,7 @@ default: console.warn(`Unsupported weblink ${type}!`); } - }, + } _getPatchSetWeblink(params) { const {commit, options} = params; @@ -311,7 +321,7 @@ } else { return {name, url: weblink.url}; } - }, + } _firstCodeBrowserWeblink(weblinks) { // This is an ordered whitelist of web link types that provide direct @@ -323,8 +333,7 @@ if (weblink) { return weblink; } } return null; - }, - + } _getBrowseCommitWeblink(weblinks, config) { if (!weblinks) { return null; } @@ -340,7 +349,7 @@ } if (!weblink) { return null; } return weblink; - }, + } _getChangeWeblinks({repo, commit, options: {weblinks, config}}) { if (!weblinks || !weblinks.length) return []; @@ -349,11 +358,11 @@ !commitWeblink || !commitWeblink.name || weblink.name !== commitWeblink.name); - }, + } _getFileWebLinks({repo, commit, file, options: {weblinks}}) { return weblinks; - }, + } /** * @param {!Object} params @@ -400,7 +409,7 @@ } return '/q/' + operators.join('+') + offsetExpr; - }, + } /** * @param {!Object} params @@ -424,7 +433,7 @@ } else { return `/c/${params.changeNum}${suffix}`; } - }, + } /** * @param {!Object} params @@ -449,7 +458,7 @@ // User dashboard. return `/dashboard/${params.user || 'self'}`; } - }, + } /** * @param {!Array<!{name: string, query: string}>} sections @@ -461,12 +470,12 @@ // If there is a repo name provided, make sure to substitute it into the // ${repo} (or legacy ${project}) query tokens. const query = opt_repoName ? - section.query.replace(REPO_TOKEN_PATTERN, opt_repoName) : - section.query; + section.query.replace(REPO_TOKEN_PATTERN, opt_repoName) : + section.query; return encodeURIComponent(section.name) + '=' + encodeURIComponent(query); }); - }, + } /** * @param {!Object} params @@ -492,7 +501,7 @@ } else { return `/c/${params.changeNum}${suffix}`; } - }, + } /** * @param {!Object} params @@ -506,7 +515,7 @@ url += ',audit-log'; } return url; - }, + } /** * @param {!Object} params @@ -526,7 +535,7 @@ url += ',dashboards'; } return url; - }, + } /** * @param {!Object} params @@ -534,12 +543,13 @@ */ _generateSettingsUrl(params) { return '/settings'; - }, + } /** * Given an object of parameters, potentially including a `patchNum` or a * `basePatchNum` or both, return a string representation of that range. If * no range is indicated in the params, the empty string is returned. + * * @param {!Object} params * @return {string} */ @@ -548,7 +558,7 @@ if (params.patchNum) { range = '' + params.patchNum; } if (params.basePatchNum) { range = params.basePatchNum + '..' + range; } return range; - }, + } /** * Given a set of params without a project, gets the project from the rest @@ -572,7 +582,7 @@ this._normalizePatchRangeParams(params); this._redirect(this._generateUrl(params)); }); - }, + } /** * Normalizes the params object, and determines if the URL needs to be @@ -601,29 +611,31 @@ params.basePatchNum = null; } return needsRedirect; - }, + } /** * Redirect the user to login using the given return-URL for redirection * after authentication success. + * * @param {string} returnUrl */ _redirectToLogin(returnUrl) { const basePath = this.getBaseUrl() || ''; page( '/login/' + encodeURIComponent(returnUrl.substring(basePath.length))); - }, + } /** * Hashes parsed by page.js exclude "inner" hashes, so a URL like "/a#b#c" * is parsed to have a hash of "b" rather than "b#c". Instead, this method * parses hashes correctly. Will return an empty string if there is no hash. + * * @param {!string} canonicalPath * @return {!string} Everything after the first '#' ("a#b#c" -> "b#c"). */ _getHashFromCanonicalPath(canonicalPath) { return canonicalPath.split('#').slice(1).join('#'); - }, + } _parseLineAddress(hash) { const match = hash.match(LINE_ADDRESS_PATTERN); @@ -632,12 +644,13 @@ leftSide: !!match[1], lineNum: parseInt(match[2], 10), }; - }, + } /** * Check to see if the user is logged in and return a promise that only * resolves if the user is logged in. If the user us not logged in, the * promise is rejected and the page is redirected to the login flow. + * * @param {!Object} data The parsed route data. * @return {!Promise<!Object>} A promise yielding the original route data * (if it resolves). @@ -651,12 +664,12 @@ return Promise.reject(new Error()); } }); - }, + } /** Page.js middleware that warms the REST API's logged-in cache line. */ _loadUserMiddleware(ctx, next) { this.$.restAPI.getLoggedIn().then(() => { next(); }); - }, + } /** * Map a route to a method on the router. @@ -683,7 +696,7 @@ this._redirectIfNotLoggedIn(data) : Promise.resolve(); promise.then(() => { this[handlerName](data); }); }); - }, + } _startRouter() { const base = this.getBaseUrl(); @@ -879,7 +892,7 @@ this._mapRoute(RoutePattern.DEFAULT, '_handleDefaultRoute'); page.start(); - }, + } /** * @param {!Object} data @@ -920,7 +933,7 @@ this._redirect('/q/status:open'); } }); - }, + } /** * Decode an application/x-www-form-urlencoded string. @@ -930,7 +943,7 @@ */ _decodeQueryString(qs) { return decodeURIComponent(qs.replace(PLUS_PATTERN, ' ')); - }, + } /** * Parse a query string (e.g. window.location.search) into an array of @@ -962,7 +975,7 @@ } }); return params; - }, + } /** * Handle dashboard routes. These may be user, or project dashboards. @@ -987,7 +1000,7 @@ }); } }); - }, + } /** * Handle custom dashboard routes. @@ -1015,8 +1028,8 @@ forEachQuery = forEachParam[1]; } const sectionParams = queryParams.filter( - elem => elem[0] && elem[1] && elem[0].toLowerCase() !== 'title' - && elem[0].toLowerCase() !== 'foreach'); + elem => elem[0] && elem[1] && elem[0].toLowerCase() !== 'title' && + elem[0].toLowerCase() !== 'foreach'); const sections = sectionParams.map(elem => { const query = forEachQuery ? `${forEachQuery} ${elem[1]}` : elem[1]; return { @@ -1039,30 +1052,32 @@ // Redirect /dashboard/ -> /dashboard/self. this._redirect('/dashboard/self'); return Promise.resolve(); - }, + } _handleProjectDashboardRoute(data) { + const project = data.params[0]; this._setParams({ view: Gerrit.Nav.View.DASHBOARD, - project: data.params[0], + project, dashboard: decodeURIComponent(data.params[1]), }); - }, + this.$.reporting.setRepoName(project); + } _handleGroupInfoRoute(data) { this._redirect('/admin/groups/' + encodeURIComponent(data.params[0])); - }, + } _handleGroupSelfRedirectRoute(data) { this._redirect('/settings/#Groups'); - }, + } _handleGroupRoute(data) { this._setParams({ view: Gerrit.Nav.View.GROUP, groupId: data.params[0], }); - }, + } _handleGroupAuditLogRoute(data) { this._setParams({ @@ -1070,7 +1085,7 @@ detail: Gerrit.Nav.GroupDetailView.LOG, groupId: data.params[0], }); - }, + } _handleGroupMembersRoute(data) { this._setParams({ @@ -1078,7 +1093,7 @@ detail: Gerrit.Nav.GroupDetailView.MEMBERS, groupId: data.params[0], }); - }, + } _handleGroupListOffsetRoute(data) { this._setParams({ @@ -1088,7 +1103,7 @@ filter: null, openCreateModal: data.hash === 'create', }); - }, + } _handleGroupListFilterOffsetRoute(data) { this._setParams({ @@ -1097,7 +1112,7 @@ offset: data.params.offset, filter: data.params.filter, }); - }, + } _handleGroupListFilterRoute(data) { this._setParams({ @@ -1105,7 +1120,7 @@ adminView: 'gr-admin-group-list', filter: data.params.filter || null, }); - }, + } _handleProjectsOldRoute(data) { let params = ''; @@ -1118,31 +1133,37 @@ } this._redirect(`/admin/repos/${params}`); - }, + } _handleRepoCommandsRoute(data) { + const repo = data.params[0]; this._setParams({ view: Gerrit.Nav.View.REPO, detail: Gerrit.Nav.RepoDetailView.COMMANDS, - repo: data.params[0], + repo, }); - }, + this.$.reporting.setRepoName(repo); + } _handleRepoAccessRoute(data) { + const repo = data.params[0]; this._setParams({ view: Gerrit.Nav.View.REPO, detail: Gerrit.Nav.RepoDetailView.ACCESS, - repo: data.params[0], + repo, }); - }, + this.$.reporting.setRepoName(repo); + } _handleRepoDashboardsRoute(data) { + const repo = data.params[0]; this._setParams({ view: Gerrit.Nav.View.REPO, detail: Gerrit.Nav.RepoDetailView.DASHBOARDS, - repo: data.params[0], + repo, }); - }, + this.$.reporting.setRepoName(repo); + } _handleBranchListOffsetRoute(data) { this._setParams({ @@ -1152,7 +1173,7 @@ offset: data.params[2] || 0, filter: null, }); - }, + } _handleBranchListFilterOffsetRoute(data) { this._setParams({ @@ -1162,7 +1183,7 @@ offset: data.params.offset, filter: data.params.filter, }); - }, + } _handleBranchListFilterRoute(data) { this._setParams({ @@ -1171,7 +1192,7 @@ repo: data.params.repo, filter: data.params.filter || null, }); - }, + } _handleTagListOffsetRoute(data) { this._setParams({ @@ -1181,7 +1202,7 @@ offset: data.params[2] || 0, filter: null, }); - }, + } _handleTagListFilterOffsetRoute(data) { this._setParams({ @@ -1191,7 +1212,7 @@ offset: data.params.offset, filter: data.params.filter, }); - }, + } _handleTagListFilterRoute(data) { this._setParams({ @@ -1200,7 +1221,7 @@ repo: data.params.repo, filter: data.params.filter || null, }); - }, + } _handleRepoListOffsetRoute(data) { this._setParams({ @@ -1210,7 +1231,7 @@ filter: null, openCreateModal: data.hash === 'create', }); - }, + } _handleRepoListFilterOffsetRoute(data) { this._setParams({ @@ -1219,7 +1240,7 @@ offset: data.params.offset, filter: data.params.filter, }); - }, + } _handleRepoListFilterRoute(data) { this._setParams({ @@ -1227,26 +1248,28 @@ adminView: 'gr-repo-list', filter: data.params.filter || null, }); - }, + } _handleCreateProjectRoute(data) { // Redirects the legacy route to the new route, which displays the project // list with a hash 'create'. this._redirect('/admin/repos#create'); - }, + } _handleCreateGroupRoute(data) { // Redirects the legacy route to the new route, which displays the group // list with a hash 'create'. this._redirect('/admin/groups#create'); - }, + } _handleRepoRoute(data) { + const repo = data.params[0]; this._setParams({ view: Gerrit.Nav.View.REPO, - repo: data.params[0], + repo, }); - }, + this.$.reporting.setRepoName(repo); + } _handlePluginListOffsetRoute(data) { this._setParams({ @@ -1255,7 +1278,7 @@ offset: data.params[1] || 0, filter: null, }); - }, + } _handlePluginListFilterOffsetRoute(data) { this._setParams({ @@ -1264,7 +1287,7 @@ offset: data.params.offset, filter: data.params.filter, }); - }, + } _handlePluginListFilterRoute(data) { this._setParams({ @@ -1272,14 +1295,14 @@ adminView: 'gr-plugin-list', filter: data.params.filter || null, }); - }, + } _handlePluginListRoute(data) { this._setParams({ view: Gerrit.Nav.View.ADMIN, adminView: 'gr-plugin-list', }); - }, + } _handleQueryRoute(data) { this._setParams({ @@ -1287,15 +1310,15 @@ query: data.params[0], offset: data.params[2], }); - }, + } _handleQueryLegacySuffixRoute(ctx) { this._redirect(ctx.path.replace(LEGACY_QUERY_SUFFIX_PATTERN, '')); - }, + } _handleChangeNumberLegacyRoute(ctx) { this._redirect('/c/' + encodeURIComponent(ctx.params[0])); - }, + } _handleChangeRoute(ctx) { // Parameter order is based on the regex group number matched. @@ -1307,8 +1330,9 @@ view: Gerrit.Nav.View.CHANGE, }; + this.$.reporting.setRepoName(params.project); this._redirectOrNavigate(params); - }, + } _handleDiffRoute(ctx) { // Parameter order is based on the regex group number matched. @@ -1326,9 +1350,9 @@ params.leftSide = address.leftSide; params.lineNum = address.lineNum; } - + this.$.reporting.setRepoName(params.project); this._redirectOrNavigate(params); - }, + } _handleChangeLegacyRoute(ctx) { // Parameter order is based on the regex group number matched. @@ -1341,11 +1365,11 @@ }; this._normalizeLegacyRouteParams(params); - }, + } _handleLegacyLinenum(ctx) { this._redirect(ctx.path.replace(LEGACY_LINENUM_PATTERN, '#$1')); - }, + } _handleDiffLegacyRoute(ctx) { // Parameter order is based on the regex group number matched. @@ -1364,29 +1388,33 @@ } this._normalizeLegacyRouteParams(params); - }, + } _handleDiffEditRoute(ctx) { // Parameter order is based on the regex group number matched. + const project = ctx.params[0]; this._redirectOrNavigate({ - project: ctx.params[0], + project, changeNum: ctx.params[1], patchNum: ctx.params[2], path: ctx.params[3], view: Gerrit.Nav.View.EDIT, }); - }, + this.$.reporting.setRepoName(project); + } _handleChangeEditRoute(ctx) { // Parameter order is based on the regex group number matched. + const project = ctx.params[0]; this._redirectOrNavigate({ - project: ctx.params[0], + project, changeNum: ctx.params[1], patchNum: ctx.params[3], view: Gerrit.Nav.View.CHANGE, edit: true, }); - }, + this.$.reporting.setRepoName(project); + } /** * Normalize the patch range params for a the change or diff view and @@ -1399,16 +1427,16 @@ } else { this._setParams(params); } - }, + } _handleAgreementsRoute() { this._redirect('/settings/#Agreements'); - }, + } _handleNewAgreementsRoute(data) { data.params.view = Gerrit.Nav.View.AGREEMENTS; this._setParams(data.params); - }, + } _handleSettingsLegacyRoute(data) { // email tokens may contain '+' but no space. @@ -1419,11 +1447,11 @@ view: Gerrit.Nav.View.SETTINGS, emailToken: token, }); - }, + } _handleSettingsRoute(data) { this._setParams({view: Gerrit.Nav.View.SETTINGS}); - }, + } _handleRegisterRoute(ctx) { this._setParams({justRegistered: true}); @@ -1434,7 +1462,7 @@ if (path[0] !== '/') { return; } this._redirect(this.getBaseUrl() + path); - }, + } /** * Handler for routes that should pass through the router and not be caught @@ -1442,8 +1470,7 @@ */ _handlePassThroughRoute() { location.reload(); - }, - + } /** * URL may sometimes have /+/ encoded to / /. @@ -1453,26 +1480,26 @@ let hash = this._getHashFromCanonicalPath(ctx.canonicalPath); if (hash.length) { hash = '#' + hash; } this._redirect(`/c/${ctx.params[0]}/+/${ctx.params[1]}${hash}`); - }, + } _handlePluginScreen(ctx) { const view = Gerrit.Nav.View.PLUGIN_SCREEN; const plugin = ctx.params[0]; const screen = ctx.params[1]; this._setParams({view, plugin, screen}); - }, + } _handleDocumentationSearchRoute(data) { this._setParams({ view: Gerrit.Nav.View.DOCUMENTATION_SEARCH, filter: data.params.filter || null, }); - }, + } _handleDocumentationSearchRedirectRoute(data) { this._redirect('/Documentation/q/filter:' + encodeURIComponent(data.params[0])); - }, + } _handleDocumentationRedirectRoute(data) { if (data.params[1]) { @@ -1481,7 +1508,7 @@ // Redirect /Documentation to /Documentation/index.html this._redirect('/Documentation/index.html'); } - }, + } /** * Catchall route for when no other route is matched. @@ -1494,7 +1521,7 @@ // Route can be recognized by server, so we pass it to server. this._handlePassThroughRoute(); } - }, + } _show404() { // Note: the app's 404 display is tightly-coupled with catching 404 @@ -1502,6 +1529,8 @@ // TODO: Decouple the gr-app error view from network responses. this._appElement().dispatchEvent(new CustomEvent('page-error', {detail: {response: {status: 404}}})); - }, - }); + } + } + + customElements.define(GrRouter.is, GrRouter); })();
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html index 4bfc35b..fb7f0d6 100644 --- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html +++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-router</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html index 0cdef8c..1b57ddb 100644 --- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html +++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
@@ -29,11 +29,9 @@ } gr-autocomplete { background-color: var(--view-background-color); - border: 1px solid var(--border-color); border-radius: var(--border-radius); flex: 1; outline: none; - padding: var(--spacing-xs); } </style> <form> @@ -45,7 +43,6 @@ on-commit="_handleInputCommit" allow-non-suggested-values multi - borderless threshold="[[_threshold]]" tab-complete vertical-offset="30"></gr-autocomplete>
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js index 6c7b0fd..48e8662 100644 --- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js +++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -90,6 +90,7 @@ 'status:merged', 'status:open', 'status:reviewed', + 'submissionid:', 'topic:', 'tr:', ]; @@ -102,75 +103,80 @@ const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g; - Polymer({ - is: 'gr-search-bar', - + /** + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @appliesMixin Gerrit.URLEncodingMixin + */ + class GrSearchBar extends Polymer.mixinBehaviors( [ + Gerrit.KeyboardShortcutBehavior, + Gerrit.URLEncodingBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-search-bar'; } /** * Fired when a search is committed * * @event handle-search */ - behaviors: [ - Gerrit.KeyboardShortcutBehavior, - Gerrit.URLEncodingBehavior, - ], - - properties: { - value: { - type: String, - value: '', - notify: true, - observer: '_valueChanged', - }, - keyEventTarget: { - type: Object, - value() { return document.body; }, - }, - query: { - type: Function, - value() { - return this._getSearchSuggestions.bind(this); + static get properties() { + return { + value: { + type: String, + value: '', + notify: true, + observer: '_valueChanged', }, - }, - projectSuggestions: { - type: Function, - value() { - return () => Promise.resolve([]); + keyEventTarget: { + type: Object, + value() { return document.body; }, }, - }, - groupSuggestions: { - type: Function, - value() { - return () => Promise.resolve([]); + query: { + type: Function, + value() { + return this._getSearchSuggestions.bind(this); + }, }, - }, - accountSuggestions: { - type: Function, - value() { - return () => Promise.resolve([]); + projectSuggestions: { + type: Function, + value() { + return () => Promise.resolve([]); + }, }, - }, - _inputVal: String, - _threshold: { - type: Number, - value: 1, - }, - }, + groupSuggestions: { + type: Function, + value() { + return () => Promise.resolve([]); + }, + }, + accountSuggestions: { + type: Function, + value() { + return () => Promise.resolve([]); + }, + }, + _inputVal: String, + _threshold: { + type: Number, + value: 1, + }, + }; + } keyboardShortcuts() { return { [this.Shortcut.SEARCH]: '_handleSearch', }; - }, + } _valueChanged(value) { this._inputVal = value; - }, + } _handleInputCommit(e) { this._preventDefaultAndNavigateToInputVal(e); - }, + } /** * This function is called in a few different cases: @@ -204,11 +210,12 @@ detail: {inputVal: this._inputVal}, })); } - }, + } /** * Determine what array of possible suggestions should be provided * to _getSearchSuggestions. + * * @param {string} input - The full search term, in lowercase. * @return {!Promise} This returns a promise that resolves to an array of * suggestion objects. @@ -246,10 +253,11 @@ .filter(operator => operator.includes(input)) .map(operator => ({text: operator}))); } - }, + } /** * Get the sorted, pruned list of suggestions for the current search query. + * * @param {string} input - The complete search query. * @return {!Promise} This returns a promise that resolves to an array of * suggestions. @@ -289,7 +297,7 @@ }; }); }); - }, + } _handleSearch(e) { const keyboardEvent = this.getKeyboardEvent(e); @@ -299,6 +307,8 @@ e.preventDefault(); this.$.searchInput.focus(); this.$.searchInput.selectAll(); - }, - }); + } + } + + customElements.define(GrSearchBar.is, GrSearchBar); })();
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html index 2fe940c..c822b73 100644 --- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html +++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-search-bar</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -61,8 +61,8 @@ getActiveElement = () => { return document.activeElement.shadowRoot ? - document.activeElement.shadowRoot.activeElement : - document.activeElement; + document.activeElement.shadowRoot.activeElement : + document.activeElement; }; test('enter in search input fires event', done => {
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js index 94de00c..935352f 100644 --- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js +++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
@@ -21,55 +21,62 @@ const SELF_EXPRESSION = 'self'; const ME_EXPRESSION = 'me'; - Polymer({ - is: 'gr-smart-search', + /** + * @appliesMixin Gerrit.DisplayNameMixin + */ + class GrSmartSearch extends Polymer.mixinBehaviors( [ + Gerrit.DisplayNameBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-smart-search'; } - properties: { - searchQuery: String, - _config: Object, - _projectSuggestions: { - type: Function, - value() { - return this._fetchProjects.bind(this); + static get properties() { + return { + searchQuery: String, + _config: Object, + _projectSuggestions: { + type: Function, + value() { + return this._fetchProjects.bind(this); + }, }, - }, - _groupSuggestions: { - type: Function, - value() { - return this._fetchGroups.bind(this); + _groupSuggestions: { + type: Function, + value() { + return this._fetchGroups.bind(this); + }, }, - }, - _accountSuggestions: { - type: Function, - value() { - return this._fetchAccounts.bind(this); + _accountSuggestions: { + type: Function, + value() { + return this._fetchAccounts.bind(this); + }, }, - }, - }, - - behaviors: [ - Gerrit.DisplayNameBehavior, - ], + }; + } attached() { + super.attached(); this.$.restAPI.getConfig().then(cfg => { this._config = cfg; }); - }, + } _handleSearch(e) { const input = e.detail.inputVal; if (input) { Gerrit.Nav.navigateToSearchQuery(input); } - }, + } _accountOrAnon(name) { return this.getUserName(this._serverConfig, name, false); - }, + } /** * Fetch from the API the predicted projects. + * * @param {string} predicate - The first part of the search term, e.g. * 'project' * @param {string} expression - The second part of the search term, e.g. @@ -86,10 +93,11 @@ const keys = Object.keys(projects); return keys.map(key => ({text: predicate + ':' + key})); }); - }, + } /** * Fetch from the API the predicted groups. + * * @param {string} predicate - The first part of the search term, e.g. * 'ownerin' * @param {string} expression - The second part of the search term, e.g. @@ -107,10 +115,11 @@ const keys = Object.keys(groups); return keys.map(key => ({text: predicate + ':' + key})); }); - }, + } /** * Fetch from the API the predicted accounts. + * * @param {string} predicate - The first part of the search term, e.g. * 'owner' * @param {string} expression - The second part of the search term, e.g. @@ -138,15 +147,17 @@ return accounts; } }); - }, + } _mapAccountsHelper(accounts, predicate) { return accounts.map(account => ({ label: account.name || '', text: account.email ? - `${predicate}:${account.email}` : - `${predicate}:"${this._accountOrAnon(account)}"`, + `${predicate}:${account.email}` : + `${predicate}:"${this._accountOrAnon(account)}"`, })); - }, - }); + } + } + + customElements.define(GrSmartSearch.is, GrSmartSearch); })();
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html index a70eb7c..5a4e341 100644 --- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html +++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-smart-search</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -48,7 +48,6 @@ sandbox.restore(); }); - test('Autocompletes accounts', () => { sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () => Promise.resolve([
diff --git a/polygerrit-ui/app/elements/custom-dark-theme_test.html b/polygerrit-ui/app/elements/custom-dark-theme_test.html index 4cf35f1..1cd042a 100644 --- a/polygerrit-ui/app/elements/custom-dark-theme_test.html +++ b/polygerrit-ui/app/elements/custom-dark-theme_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-app-it_test</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/custom-light-theme_test.html b/polygerrit-ui/app/elements/custom-light-theme_test.html index e346af5..0cabd09 100644 --- a/polygerrit-ui/app/elements/custom-light-theme_test.html +++ b/polygerrit-ui/app/elements/custom-light-theme_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-app-it_test</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.html b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.html new file mode 100644 index 0000000..1569094 --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.html
@@ -0,0 +1,91 @@ +<!-- +@license +Copyright (C) 2019 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> +<link rel="import" href="/bower_components/polymer/polymer.html"> +<link rel="import" href="/bower_components/iron-icon/iron-icon.html"> +<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> +<link rel="import" href="../../../styles/shared-styles.html"> +<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> +<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> +<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../gr-diff/gr-diff.html"> + +<dom-module id="gr-apply-fix-dialog"> + <template> + <style include="shared-styles"> + gr-diff { + --content-width: 90vw; + } + .diffContainer { + padding: var(--spacing-l) 0; + border-bottom: 1px solid var(--border-color); + } + .file-name { + display: block; + padding: var(--spacing-s) var(--spacing-l); + background-color: var(--background-color-secondary); + border-bottom: 1px solid var(--border-color); + } + .fixActions { + display: flex; + justify-content: flex-end; + } + gr-button { + margin-left: var(--spacing-m); + } + .fix-picker { + display: flex; + align-items: center; + margin-right: var(--spacing-l); + } + </style> + <gr-overlay id="applyFixOverlay" with-backdrop> + <gr-dialog + id="applyFixDialog" + on-confirm="_handleApplyFix" + confirm-label="[[_getApplyFixButtonLabel(_isApplyFixLoading)]]" + disabled="[[_isApplyFixLoading]]" + on-cancel="onCancel"> + <div slot="header">[[_robotId]] - [[getFixDescription(_currentFix)]]</div> + <div slot="main"> + <template is="dom-repeat" items="[[_currentPreviews]]"> + <div class="file-name"> + <span>[[item.filepath]]</span> + </div> + <div class="diffContainer"> + <gr-diff + prefs="[[overridePartialPrefs(prefs)]]" + change-num="[[changeNum]]" + path="[[item.filepath]]" + diff="[[item.preview]]"></gr-diff> + </div> + </template> + </div> + <div slot="footer" class="fix-picker" hidden$="[[hasSingleFix(_fixSuggestions)]]"> + <span>Suggested fix [[addOneTo(_selectedFixIdx)]] of [[_fixSuggestions.length]]</span> + <gr-button on-click="_onPrevFixClick" disabled$="[[_noPrevFix(_selectedFixIdx)]]"> + <iron-icon icon="gr-icons:chevron-left"></iron-icon> + </gr-button> + <gr-button on-click="_onNextFixClick" disabled$="[[_noNextFix(_selectedFixIdx)]]"> + <iron-icon icon="gr-icons:chevron-right"></iron-icon> + </gr-button> + </div> + </gr-dialog> + </gr-overlay> + <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> + </template> + <script src="gr-apply-fix-dialog.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js new file mode 100644 index 0000000..5130620 --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js
@@ -0,0 +1,192 @@ +/** + * @license + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +(function() { + 'use strict'; + Polymer({ + is: 'gr-apply-fix-dialog', + + properties: { + // Diff rendering preference API response. + prefs: Array, + // ChangeInfo API response object. + change: Object, + changeNum: String, + _patchNum: Number, + // robot ID associated with a robot comment. + _robotId: String, + // Selected FixSuggestionInfo entity from robot comment API response. + _currentFix: Object, + // Flattened /preview API response DiffInfo map object. + _currentPreviews: {type: Array, value: () => []}, + // FixSuggestionInfo entities from robot comment API response. + _fixSuggestions: Array, + _isApplyFixLoading: { + type: Boolean, + value: false, + }, + // Index of currently showing suggested fix. + _selectedFixIdx: Number, + }, + + behaviors: [ + Gerrit.FireBehavior, + ], + + /** + * Given robot comment CustomEvent objevt, fetch diffs associated + * with first robot comment suggested fix and open dialog. + * + * @param {*} e CustomEvent to be passed from gr-comment with + * robot comment detail. + * @return {Promise<undefined>} Promise that resolves either when all + * preview diffs are fetched or no fix suggestions in custom event detail. + */ + open(e) { + this._patchNum = e.detail.patchNum; + this._fixSuggestions = e.detail.comment.fix_suggestions; + this._robotId = e.detail.comment.robot_id; + if (this._fixSuggestions == null || this._fixSuggestions.length == 0) { + return Promise.resolve(); + } + this._selectedFixIdx = 0; + const promises = []; + promises.push( + this._showSelectedFixSuggestion(this._fixSuggestions[0]), + this.$.applyFixOverlay.open() + ); + return Promise.all(promises) + .then(() => { + // ensures gr-overlay repositions overlay in center + this.$.applyFixOverlay.fire('iron-resize'); + }); + }, + + _showSelectedFixSuggestion(fixSuggestion) { + this._currentFix = fixSuggestion; + return this._fetchFixPreview(fixSuggestion.fix_id); + }, + + _fetchFixPreview(fixId) { + return this.$.restAPI + .getRobotCommentFixPreview(this.changeNum, this._patchNum, fixId) + .then(res => { + if (res != null) { + const previews = Object.keys(res).map(key => + ({filepath: key, preview: res[key]})); + this._currentPreviews = previews; + } + }).catch(err => { + this._close(); + this.dispatchEvent(new CustomEvent('show-error', { + bubbles: true, + composed: true, + detail: {message: `Error generating fix preview: ${err}`}, + })); + }); + }, + + hasSingleFix(_fixSuggestions) { + return (_fixSuggestions || {}).length === 1; + }, + + overridePartialPrefs(prefs) { + // generate a smaller gr-diff than fullscreen for dialog + return Object.assign({}, prefs, {line_length: 50}); + }, + + onCancel(e) { + if (e) { + e.stopPropagation(); + } + this._close(); + }, + + addOneTo(_selectedFixIdx) { + return _selectedFixIdx + 1; + }, + + _onPrevFixClick(e) { + if (e) e.stopPropagation(); + if (this._selectedFixIdx >= 1 && this._fixSuggestions != null) { + this._selectedFixIdx -= 1; + return this._showSelectedFixSuggestion( + this._fixSuggestions[this._selectedFixIdx]); + } + }, + + _onNextFixClick(e) { + if (e) e.stopPropagation(); + if (this._selectedFixIdx < this._fixSuggestions.length && + this._fixSuggestions != null) { + this._selectedFixIdx += 1; + return this._showSelectedFixSuggestion( + this._fixSuggestions[this._selectedFixIdx]); + } + }, + + _noPrevFix(_selectedFixIdx) { + return _selectedFixIdx === 0; + }, + + _noNextFix(_selectedFixIdx) { + if (this._fixSuggestions == null) return true; + return _selectedFixIdx === this._fixSuggestions.length - 1; + }, + + _close() { + this._currentFix = {}; + this._currentPreviews = []; + this._isApplyFixLoading = false; + + this.dispatchEvent(new CustomEvent('close-fix-preview', { + bubbles: true, + composed: true, + })); + this.$.applyFixOverlay.close(); + }, + + _getApplyFixButtonLabel(isLoading) { + return isLoading ? 'Saving...' : 'Apply Fix'; + }, + + _handleApplyFix(e) { + if (e) { + e.stopPropagation(); + } + if (this._currentFix == null || this._currentFix.fix_id == null) { + return; + } + this._isApplyFixLoading = true; + return this.$.restAPI.applyFixSuggestion(this.changeNum, this._patchNum, + this._currentFix.fix_id).then(res => { + Gerrit.Nav.navigateToChange(this.change, 'edit', this._patchNum); + this._close(); + }).catch(err => { + this.dispatchEvent(new CustomEvent('show-error', { + bubbles: true, + composed: true, + detail: {message: `Error applying fix suggestion: ${err}`}, + })); + }); + }, + + getFixDescription(currentFix) { + return currentFix != null && currentFix.description ? + currentFix.description : ''; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html new file mode 100644 index 0000000..5a2193d --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html
@@ -0,0 +1,191 @@ +<!DOCTYPE html> +<!-- +@license +Copyright (C) 2019 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the 'License'); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an 'AS IS' BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> +<meta name='viewport' content='width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes'> +<title>gr-apply-fix-dialog</title> +<script src='/test/common-test-setup.js'></script> +<script src='/bower_components/webcomponentsjs/custom-elements-es5-adapter.js'></script> + +<script src='/bower_components/webcomponentsjs/webcomponents-lite.js'></script> +<script src='/bower_components/web-component-tester/browser.js'></script> +<link rel='import' href='../../../test/common-test-setup.html' /> + +<link rel='import' href='./gr-apply-fix-dialog.html'> + +<script>void (0);</script> + +<test-fixture id='basic'> + <template> + <gr-apply-fix-dialog></gr-apply-fix-dialog> + </template> +</test-fixture> + +<script> + suite('gr-apply-fix-dialog tests', () => { + let element; + let sandbox; + const ROBOT_COMMENT = { + robot_id: 'robot_1', + fix_suggestions: [{fix_id: 'fix_1'}, {fix_id: 'fix_2'}], + }; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + element.changeNum = '1'; + element._patchNum = 2; + element.change = { + _number: '1', + project: 'project', + }; + element.prefs = { + font_size: 12, + line_length: 100, + tab_size: 4, + }; + }); + + teardown(() => { + sandbox.restore(); + }); + + test('dialog opens fetch and sets previews', done => { + sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview') + .returns(Promise.resolve({ + f1: { + meta_a: {}, + meta_b: {}, + content: [ + { + ab: ['loqlwkqll'], + }, + { + b: ['qwqqsqw'], + }, + { + ab: ['qwqqsqw', 'qweqeqweqeq', 'qweqweq'], + }, + ], + }, + f2: { + meta_a: {}, + meta_b: {}, + content: [ + { + ab: ['eqweqweqwex'], + }, + { + b: ['zassdasd'], + }, + { + ab: ['zassdasd', 'dasdasda', 'asdasdad'], + }, + ], + }, + })); + sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve()); + + element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT}}) + .then(() => { + assert.equal(element._currentFix.fix_id, 'fix_1'); + assert.equal(element._currentPreviews.length, 2); + assert.equal(element._robotId, 'robot_1'); + done(); + }); + }); + + test('preview endpoint throws error should reset dialog', done => { + element.addEventListener('show-error', () => { + assert.deepEqual(element._currentFix, {}); + assert.equal(element._currentPreviews.length, 0); + done(); + }); + sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview', + () => Promise.reject(new Error('backend error'))); + + element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT}}); + }); + + test('apply fix button should call apply' + + 'and navigate to change view', done => { + sandbox.stub(element.$.restAPI, 'applyFixSuggestion') + .returns(Promise.resolve()); + sandbox.stub(Gerrit.Nav, 'navigateToChange'); + element._currentFix = {fix_id: '123'}; + + element._handleApplyFix().then(() => { + assert.isTrue(element.$.restAPI.applyFixSuggestion + .calledWithExactly('1', 2, '123')); + assert.isTrue(Gerrit.Nav.navigateToChange.calledWithExactly({ + _number: '1', + project: 'project', + }, 'edit', 2)); + + // reset gr-apply-fix-dialog and close + assert.deepEqual(element._currentFix, {}); + assert.equal(element._currentPreviews.length, 0); + done(); + }); + }); + + test('select fix forward and back of multiple suggested fixes', done => { + sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview') + .returns(Promise.resolve({ + f1: { + meta_a: {}, + meta_b: {}, + content: [ + { + ab: ['loqlwkqll'], + }, + { + b: ['qwqqsqw'], + }, + { + ab: ['qwqqsqw', 'qweqeqweqeq', 'qweqweq'], + }, + ], + }, + f2: { + meta_a: {}, + meta_b: {}, + content: [ + { + ab: ['eqweqweqwex'], + }, + { + b: ['zassdasd'], + }, + { + ab: ['zassdasd', 'dasdasda', 'asdasdad'], + }, + ], + }, + })); + sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve()); + + element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT}}) + .then(() => { + element._onNextFixClick(); + assert.equal(element._currentFix.fix_id, 'fix_2'); + element._onPrevFixClick(); + assert.equal(element._currentFix.fix_id, 'fix_1'); + done(); + }); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock.js index b7994e6..4009420 100644 --- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock.js +++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock.js
@@ -17,16 +17,20 @@ (function() { 'use strict'; - Polymer({ - is: 'comment-api-mock', + class CommentApiMock extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'comment-api-mock'; } - properties: { - _changeComments: Object, - }, + static get properties() { + return { + _changeComments: Object, + }; + } loadComments() { return this._reloadComments(); - }, + } /** * For the purposes of the mock, _reloadDrafts is not included because its @@ -38,13 +42,15 @@ return this._reloadComments().then(() => { return e.detail.resolve(); }); - }, + } _reloadComments() { return this.$.commentAPI.loadAll(this._changeNum) .then(comments => { this._changeComments = this.$.commentAPI._changeComments; }); - }, - }); + } + } + + customElements.define(CommentApiMock.is, CommentApiMock); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js index efed78d..8edc546 100644 --- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js +++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
@@ -248,9 +248,9 @@ const all = comments.concat(drafts).concat(robotComments); const baseComments = all.filter(c => - this._isInBaseOfPatchRange(c, patchRange)); + this._isInBaseOfPatchRange(c, patchRange)); const revisionComments = all.filter(c => - this._isInRevisionOfPatchRange(c, patchRange)); + this._isInRevisionOfPatchRange(c, patchRange)); return { meta: { @@ -348,7 +348,7 @@ const threads = this.getCommentThreads(this._sortComments(comments)); const unresolvedThreads = threads - .filter(thread => + .filter(thread => thread.comments.length && thread.comments[thread.comments.length - 1].unresolved); @@ -406,12 +406,13 @@ }; /** - * Whether the given comment should be included in the base side of the - * given patch range. - * @param {!Object} comment - * @param {!Gerrit.PatchRange} range - * @return {boolean} - */ + * Whether the given comment should be included in the base side of the + * given patch range. + * + * @param {!Object} comment + * @param {!Gerrit.PatchRange} range + * @return {boolean} + */ ChangeComments.prototype._isInBaseOfPatchRange = function(comment, range) { // If the base of the patch range is a parent of a merge, and the comment // appears on a specific parent then only show the comment if the parent @@ -439,6 +440,7 @@ /** * Whether the given comment should be included in the revision side of the * given patch range. + * * @param {!Object} comment * @param {!Gerrit.PatchRange} range * @return {boolean} @@ -451,6 +453,7 @@ /** * Whether the given comment should be included in the given patch range. + * * @param {!Object} comment * @param {!Gerrit.PatchRange} range * @return {boolean|undefined} @@ -460,20 +463,27 @@ this._isInRevisionOfPatchRange(comment, range); }; - Polymer({ - is: 'gr-comment-api', + /** + * @appliesMixin Gerrit.PatchSetMixin + */ + class GrCommentApi extends Polymer.mixinBehaviors( [ + Gerrit.PatchSetBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-comment-api'; } - properties: { - _changeComments: Object, - }, + static get properties() { + return { + _changeComments: Object, + }; + } - listeners: { - 'reload-drafts': 'reloadDrafts', - }, - - behaviors: [ - Gerrit.PatchSetBehavior, - ], + created() { + super.created(); + this.addEventListener('reload-drafts', + changeNum => this.reloadDrafts(changeNum)); + } /** * Load all comments (with drafts and robot comments) for the given change @@ -491,10 +501,10 @@ return Promise.all(promises).then(([comments, robotComments, drafts]) => { this._changeComments = new ChangeComments(comments, - robotComments, drafts, changeNum); + robotComments, drafts, changeNum); return this._changeComments; }); - }, + } /** * Re-initialize _changeComments with a new ChangeComments object, that @@ -513,6 +523,8 @@ this._changeComments.robotComments, drafts, changeNum); return this._changeComments; }); - }, - }); + } + } + + customElements.define(GrCommentApi.is, GrCommentApi); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html index c44e8c4..4261c2d 100644 --- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html +++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-comment-api</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -109,7 +109,7 @@ let draftStub; setup(() => { commentStub = sandbox.stub(element.$.restAPI, 'getDiffComments') - .returns(Promise.resolve({})); + .returns(Promise.resolve({})); robotCommentStub = sandbox.stub(element.$.restAPI, 'getDiffRobotComments').returns(Promise.resolve({})); draftStub = sandbox.stub(element.$.restAPI, 'getDiffDrafts')
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js index 3d9c172..201da05 100644 --- a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js +++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
@@ -24,36 +24,40 @@ [Gerrit.CoverageType.NOT_INSTRUMENTED, 'Not instrumented by any tests.'], ]); - Polymer({ - is: 'gr-coverage-layer', + class GrCoverageLayer extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-coverage-layer'; } - properties: { + static get properties() { + return { /** * Must be sorted by code_range.start_line. * Must only contain ranges that match the side. * * @type {!Array<!Gerrit.CoverageRange>} */ - coverageRanges: Array, - side: String, + coverageRanges: Array, + side: String, - /** - * We keep track of the line number from the previous annotate() call, - * and also of the index of the coverage range that had matched. - * annotate() calls are coming in with increasing line numbers and - * coverage ranges are sorted by line number. So this is a very simple - * and efficient way for finding the coverage range that matches a given - * line number. - */ - _lineNumber: { - type: Number, - value: 0, - }, - _index: { - type: Number, - value: 0, - }, - }, + /** + * We keep track of the line number from the previous annotate() call, + * and also of the index of the coverage range that had matched. + * annotate() calls are coming in with increasing line numbers and + * coverage ranges are sorted by line number. So this is a very simple + * and efficient way for finding the coverage range that matches a given + * line number. + */ + _lineNumber: { + type: Number, + value: 0, + }, + _index: { + type: Number, + value: 0, + }, + }; + } /** * Layer method to add annotations to a line. @@ -102,6 +106,8 @@ lineNumberEl.title = TOOLTIP_MAP.get(coverageRange.type); return; } - }, - }); + } + } + + customElements.define(GrCoverageLayer.is, GrCoverageLayer); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html index 45a67e1..99c583d 100644 --- a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html +++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-coverage-layer</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js index 0a51d92..144cc56 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
@@ -105,7 +105,7 @@ let tr = content.parentElement.parentElement; while (tr = tr.nextSibling) { if (tr.classList.contains('both') || ( - (side === 'left' && tr.classList.contains('remove')) || + (side === 'left' && tr.classList.contains('remove')) || (side === 'right' && tr.classList.contains('add')))) { return tr.querySelector('.contentText'); }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html index 19e017d..4c44414 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>GrDiffBuilderUnified</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html index 69c2419..40fbe3c 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -77,7 +77,6 @@ properties: { diff: Object, - diffPath: String, changeNum: String, patchNum: String, viewMode: String, @@ -114,6 +113,7 @@ * The promise last returned from `render()` while the asynchronous * rendering is running - `null` otherwise. Provides a `cancel()` * method that rejects it with `{isCancelled: true}`. + * * @type {?Object} */ _cancelableRenderPromise: Object, @@ -219,8 +219,8 @@ getLineNumberByChild(node) { const lineEl = this.getLineElByChild(node); return lineEl ? - parseInt(lineEl.getAttribute('data-value'), 10) : - null; + parseInt(lineEl.getAttribute('data-value'), 10) : + null; }, getContentByLine(lineNumber, opt_side, opt_root) { @@ -249,7 +249,7 @@ getSideByLineEl(lineEl) { return lineEl.classList.contains(GrDiffBuilder.Side.RIGHT) ? - GrDiffBuilder.Side.RIGHT : GrDiffBuilder.Side.LEFT; + GrDiffBuilder.Side.RIGHT : GrDiffBuilder.Side.LEFT; }, emitGroup(group, sectionEl) { @@ -304,7 +304,7 @@ let builder = null; if (this.isImageDiff) { builder = new GrDiffBuilderImage(diff, prefs, this.diffElement, - this.baseImage, this.revisionImage); + this.baseImage, this.revisionImage); } else if (diff.binary) { // If the diff is binary, but not an image. return new GrDiffBuilderBinary(diff, prefs, this.diffElement); @@ -352,8 +352,8 @@ // If endIndex isn't present, continue to the end of the line. const endIndex = highlight.endIndex === undefined ? - line.text.length : - highlight.endIndex; + line.text.length : + highlight.endIndex; GrAnnotation.annotateElement( contentEl,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js index c25e90c..62b35e4 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -92,6 +92,7 @@ /** * Abstract method + * * @param {string} outputEl * @param {number} fontSize */ @@ -101,6 +102,7 @@ /** * Abstract method + * * @param {Object} group */ GrDiffBuilder.prototype.buildSectionElement = function() { @@ -183,7 +185,7 @@ continue; } const lineNumber = opt_side === 'left' ? - line.beforeNumber : line.afterNumber; + line.beforeNumber : line.afterNumber; if (lineNumber < start || lineNumber > end) { continue; } if (out_lines) { out_lines.push(line); } @@ -292,6 +294,7 @@ e.detail = { groups, section, + numLines, }; // Let it bubble up the DOM tree. }); @@ -306,10 +309,16 @@ td.classList.add(opt_class); } - if (line.type === GrDiffLine.Type.REMOVE) { - td.setAttribute('aria-label', `${number} removed`); - } else if (line.type === GrDiffLine.Type.ADD) { - td.setAttribute('aria-label', `${number} added`); + // Add aria-labels for valid line numbers. + // For unified diff, this method will be called with number set to 0 for + // the empty line number column for added/removed lines. This should not + // be announced to the screenreader. + if (number > 0) { + if (line.type === GrDiffLine.Type.REMOVE) { + td.setAttribute('aria-label', `${number} removed`); + } else if (line.type === GrDiffLine.Type.ADD) { + td.setAttribute('aria-label', `${number} added`); + } } if (line.type === GrDiffLine.Type.BLANK) { @@ -348,7 +357,9 @@ } for (const layer of this.layers) { - layer.annotate(contentText, lineNumberEl, line); + if (typeof layer.annotate == 'function') { + layer.annotate(contentText, lineNumberEl, line); + } } td.appendChild(contentText); @@ -462,6 +473,7 @@ /** * Finds the next DIV.contentText element following the given element, and on * the same side. Will only search within a group. + * * @param {HTMLElement} content * @param {string} side Either 'left' or 'right' * @return {HTMLElement} @@ -473,6 +485,7 @@ /** * Determines whether the given group is either totally an addition or totally * a removal. + * * @param {!Object} group (GrDiffGroup) * @return {boolean} */ @@ -485,6 +498,7 @@ /** * Set the blame information for the diff. For any already-rendered line, * re-render its blame cell content. + * * @param {Object} blame */ GrDiffBuilder.prototype.setBlame = function(blame) { @@ -512,6 +526,7 @@ /** * Find the blame cell for a given line number. + * * @param {number} lineNum * @return {HTMLTableDataCellElement} */ @@ -524,6 +539,7 @@ * Given a base line number, return the commit containing that line in the * current set of blame information. If no blame information has been * provided, null is returned. + * * @param {number} lineNum * @return {Object} The commit information. */ @@ -543,6 +559,7 @@ /** * Given the number of a base line, get the content for the blame cell of that * line. If there is no blame information for that line, returns null. + * * @param {number} lineNum * @param {Object=} opt_commit Optionally provide the commit object, so that * it does not need to be searched. @@ -559,6 +576,10 @@ isStartOfRange ? 'startOfRange' : ''); const shaNode = this._createElement('span', 'sha'); shaNode.innerText = commit.id.substr(0, 7); + shaNode.onclick = function() { + location.href = '/q/' + shaNode.innerText; + }; + blameNode.appendChild(shaNode); blameNode.append(` on ${date} by ${commit.author}`); return blameNode; @@ -567,6 +588,7 @@ /** * Create a blame cell for the given base line. Blame information will be * included in the cell if available. + * * @param {GrDiffLine} line * @return {HTMLTableDataCellElement} */
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html index 3fdf242..a67835d 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff-builder</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -171,7 +171,6 @@ '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍'); }); - test('line_length ignored if line_wrapping is true', () => { builder._prefs = {line_wrapping: true, tab_size: 4, line_length: 50}; const text = 'a'.repeat(51); @@ -257,8 +256,8 @@ }); test('tab wrapper style', () => { - const pattern = new RegExp('^<span class="style-scope gr-diff tab" ' - + 'style="(?:-moz-)?tab-size: (\\d+);">\\t<\\/span>$'); + const pattern = new RegExp('^<span class="style-scope gr-diff tab" ' + + 'style="(?:-moz-)?tab-size: (\\d+);">\\t<\\/span>$'); for (const size of [1, 3, 8, 55]) { const html = builder._getTabWrapper(size).outerHTML;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js index a53b86e..25f8770 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
@@ -35,92 +35,117 @@ const LEFT_SIDE_CLASS = 'target-side-left'; const RIGHT_SIDE_CLASS = 'target-side-right'; - Polymer({ - is: 'gr-diff-cursor', + class GrDiffCursor extends Polymer.mixinBehaviors([Gerrit.FireBehavior], + Polymer.GestureEventListeners( + Polymer.LegacyElementMixin(Polymer.Element))) { + static get is() { return 'gr-diff-cursor'; } - properties: { + static get properties() { + return { /** * Either DiffSides.LEFT or DiffSides.RIGHT. */ - side: { - type: String, - value: DiffSides.RIGHT, - }, - /** @type {!HTMLElement|undefined} */ - diffRow: { - type: Object, - notify: true, - observer: '_rowChanged', - }, + side: { + type: String, + value: DiffSides.RIGHT, + }, + /** @type {!HTMLElement|undefined} */ + diffRow: { + type: Object, + notify: true, + observer: '_rowChanged', + }, - /** - * The diff views to cursor through and listen to. - */ - diffs: { - type: Array, - value() { return []; }, - }, + /** + * The diff views to cursor through and listen to. + */ + diffs: { + type: Array, + value() { return []; }, + }, - /** - * If set, the cursor will attempt to move to the line number (instead of - * the first chunk) the next time the diff renders. It is set back to null - * when used. It should be only used if you want the line to be focused - * after initialization of the component and page should scroll - * to that position. This parameter should be set at most for one gr-diff - * element in the page. - * - * @type (?number) - */ - initialLineNumber: { - type: Number, - value: null, - }, + /** + * If set, the cursor will attempt to move to the line number (instead of + * the first chunk) the next time the diff renders. It is set back to null + * when used. It should be only used if you want the line to be focused + * after initialization of the component and page should scroll + * to that position. This parameter should be set at most for one gr-diff + * element in the page. + * + * @type {?number} + */ + initialLineNumber: { + type: Number, + value: null, + }, - /** - * The scroll behavior for the cursor. Values are 'never' and - * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond - * the viewport. - */ - _scrollBehavior: { - type: String, - value: ScrollBehavior.KEEP_VISIBLE, - }, + /** + * The scroll behavior for the cursor. Values are 'never' and + * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond + * the viewport. + */ + _scrollBehavior: { + type: String, + value: ScrollBehavior.KEEP_VISIBLE, + }, - _focusOnMove: { - type: Boolean, - value: true, - }, + _focusOnMove: { + type: Boolean, + value: true, + }, - _listeningForScroll: Boolean, - }, + _listeningForScroll: Boolean, + }; + } - observers: [ - '_updateSideClass(side)', - '_diffsChanged(diffs.splices)', - ], + static get observers() { + return [ + '_updateSideClass(side)', + '_diffsChanged(diffs.splices)', + ]; + } + + ready() { + super.ready(); + Polymer.RenderStatus.afterNextRender(this, () => { + /* + This represents the diff cursor is ready for interaction coming from + client components. It is more then Polymer "ready" lifecycle, as no + "ready" events are automatically fired by Polymer, it means + the cursor is completely interactable - in this case attached and + painted on the page. We name it "ready" instead of "rendered" as the + long-term goal is to make gr-diff-cursor a javascript class - not a DOM + element with an actual lifecycle. This will be triggered only once + per element. + */ + this.fire('ready', null, {bubbles: false}); + }); + } attached() { + super.attached(); // Catch when users are scrolling as the view loads. this.listen(window, 'scroll', '_handleWindowScroll'); - }, + } detached() { + super.detached(); this.unlisten(window, 'scroll', '_handleWindowScroll'); - }, + } moveLeft() { this.side = DiffSides.LEFT; if (this._isTargetBlank()) { this.moveUp(); } - }, + } moveRight() { this.side = DiffSides.RIGHT; if (this._isTargetBlank()) { this.moveUp(); } - }, + } moveDown() { if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) { @@ -128,7 +153,7 @@ } else { this.$.cursorManager.next(); } - }, + } moveUp() { if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) { @@ -136,7 +161,7 @@ } else { this.$.cursorManager.previous(); } - }, + } moveToNextChunk(opt_clipToTop) { this.$.cursorManager.next(this._isFirstRowOfChunk.bind(this), @@ -144,22 +169,22 @@ return target.parentNode.scrollHeight; }, opt_clipToTop); this._fixSide(); - }, + } moveToPreviousChunk() { this.$.cursorManager.previous(this._isFirstRowOfChunk.bind(this)); this._fixSide(); - }, + } moveToNextCommentThread() { this.$.cursorManager.next(this._rowHasThread.bind(this)); this._fixSide(); - }, + } moveToPreviousCommentThread() { this.$.cursorManager.previous(this._rowHasThread.bind(this)); this._fixSide(); - }, + } /** * @param {number} number @@ -172,10 +197,11 @@ this.side = side; this.$.cursorManager.setCursor(row); } - }, + } /** * Get the line number element targeted by the cursor row and side. + * * @return {?Element|undefined} */ getTargetLineElement() { @@ -190,24 +216,24 @@ } return this.diffRow.querySelector(lineElSelector); - }, + } getTargetDiffElement() { if (!this.diffRow) return null; const hostOwner = Polymer.dom(/** @type {Node} */ (this.diffRow)) - .getOwnerRoot(); + .getOwnerRoot(); if (hostOwner && hostOwner.host && hostOwner.host.tagName === 'GR-DIFF') { return hostOwner.host; } return null; - }, + } moveToFirstChunk() { this.$.cursorManager.moveToStart(); this.moveToNextChunk(true); - }, + } reInitCursor() { this._updateStops(); @@ -217,7 +243,7 @@ } else { this.moveToFirstChunk(); } - }, + } _handleWindowScroll() { if (this._listeningForScroll) { @@ -225,32 +251,47 @@ this._focusOnMove = false; this._listeningForScroll = false; } - }, + } handleDiffUpdate() { this._updateStops(); if (!this.diffRow) { // does not scroll during init unless requested const scrollingBehaviorForInit = this.initialLineNumber ? - ScrollBehavior.KEEP_VISIBLE : - ScrollBehavior.NEVER; + ScrollBehavior.KEEP_VISIBLE : + ScrollBehavior.NEVER; this._scrollBehavior = scrollingBehaviorForInit; this.reInitCursor(); } this._scrollBehavior = ScrollBehavior.KEEP_VISIBLE; this._focusOnMove = true; this._listeningForScroll = false; - }, + } _handleDiffRenderStart() { this._listeningForScroll = true; - }, + } + + createCommentInPlace() { + const diffWithRangeSelected = this.diffs.find(diff => { + return diff.isRangeSelected(); + }); + if (diffWithRangeSelected) { + diffWithRangeSelected.createRangeComment(); + } else { + const line = this.getTargetLineElement(); + if (line) { + this.getTargetDiffElement().addDraftAtLine(line); + } + } + } /** * Get an object describing the location of the cursor. Such as * {leftSide: false, number: 123} for line 123 of the revision, or * {leftSide: true, number: 321} for line 321 of the base patch. * Returns null if an address is not available. + * * @return {?Object} */ getAddress() { @@ -276,7 +317,7 @@ leftSide: cell.matches('.left'), number: parseInt(number, 10), }; - }, + } _getViewMode() { if (!this.diffRow) { @@ -288,24 +329,24 @@ } else { return DiffViewMode.UNIFIED; } - }, + } _rowHasSide(row) { const selector = (this.side === DiffSides.LEFT ? '.left' : '.right') + ' + .content'; return !!row.querySelector(selector); - }, + } _isFirstRowOfChunk(row) { const parentClassList = row.parentNode.classList; return parentClassList.contains('section') && parentClassList.contains('delta') && !row.previousSibling; - }, + } _rowHasThread(row) { return row.querySelector('.thread-group'); - }, + } /** * If we jumped to a row where there is no content on the current side then @@ -315,9 +356,9 @@ if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE && this._isTargetBlank()) { this.side = this.side === DiffSides.LEFT ? - DiffSides.RIGHT : DiffSides.LEFT; + DiffSides.RIGHT : DiffSides.LEFT; } - }, + } _isTargetBlank() { if (!this.diffRow) { @@ -327,14 +368,14 @@ const actions = this._getActionsForRow(); return (this.side === DiffSides.LEFT && !actions.left) || (this.side === DiffSides.RIGHT && !actions.right); - }, + } _rowChanged(newRow, oldRow) { if (oldRow) { oldRow.classList.remove(LEFT_SIDE_CLASS, RIGHT_SIDE_CLASS); } this._updateSideClass(); - }, + } _updateSideClass() { if (!this.diffRow) { @@ -344,11 +385,11 @@ this.diffRow); this.toggleClass(RIGHT_SIDE_CLASS, this.side === DiffSides.RIGHT, this.diffRow); - }, + } _isActionType(type) { return type !== 'blank' && type !== 'contextControl'; - }, + } _getActionsForRow() { const actions = {left: false, right: false}; @@ -359,22 +400,23 @@ this.diffRow.getAttribute('right-type')); } return actions; - }, + } _getStops() { return this.diffs.reduce( (stops, diff) => { return stops.concat(diff.getCursorStops()); }, []); - }, + } _updateStops() { this.$.cursorManager.stops = this._getStops(); - }, + } /** * Setup and tear down on-render listeners for any diffs that are added or * removed from the cursor. + * * @private */ _diffsChanged(changeRecord) { @@ -391,22 +433,22 @@ splice = changeRecord.indexSplices[spliceIdx]; for (i = splice.index; - i < splice.index + splice.addedCount; - i++) { + i < splice.index + splice.addedCount; + i++) { this.listen(this.diffs[i], 'render-start', '_handleDiffRenderStart'); this.listen(this.diffs[i], 'render-content', 'handleDiffUpdate'); } for (i = 0; - i < splice.removed && splice.removed.length; - i++) { + i < splice.removed && splice.removed.length; + i++) { this.unlisten(splice.removed[i], 'render-start', '_handleDiffRenderStart'); this.unlisten(splice.removed[i], 'render-content', 'handleDiffUpdate'); } } - }, + } _findRowByNumberAndFile(targetNumber, side, opt_path) { let stops; @@ -423,6 +465,8 @@ return stops[i]; } } - }, - }); + } + } + + customElements.define(GrDiffCursor.is, GrDiffCursor); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html index 1c1100d..dff9e79 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff-cursor</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -42,6 +42,12 @@ </template> </test-fixture> +<test-fixture id="empty"> + <template> + <div></div> + </template> +</test-fixture> + <script> suite('gr-diff-cursor tests', () => { let sandbox; @@ -259,6 +265,70 @@ ); }); + suite('createCommentInPlace', () => { + setup(() => { + diffElement.loggedIn = true; + }); + + test('adds new draft for selected line on the left', done => { + cursorElement.moveToLineNumber(2, 'left'); + diffElement.addEventListener('create-comment', e => { + const {lineNum, range, side, patchNum} = e.detail; + assert.equal(lineNum, 2); + assert.equal(range, undefined); + assert.equal(patchNum, 1); + assert.equal(side, 'left'); + done(); + }); + cursorElement.createCommentInPlace(); + }); + + test('adds draft for selected line on the right', done => { + cursorElement.moveToLineNumber(4, 'right'); + diffElement.addEventListener('create-comment', e => { + const {lineNum, range, side, patchNum} = e.detail; + assert.equal(lineNum, 4); + assert.equal(range, undefined); + assert.equal(patchNum, 2); + assert.equal(side, 'right'); + done(); + }); + cursorElement.createCommentInPlace(); + }); + + test('createCommentInPlace creates comment for range if selected', done => { + const someRange = { + start_line: 2, + start_character: 3, + end_line: 6, + end_character: 1, + }; + diffElement.$.highlights.selectedRange = { + side: 'right', + range: someRange, + }; + diffElement.addEventListener('create-comment', e => { + const {lineNum, range, side, patchNum} = e.detail; + assert.equal(lineNum, 6); + assert.equal(range, someRange); + assert.equal(patchNum, 2); + assert.equal(side, 'right'); + done(); + }); + cursorElement.createCommentInPlace(); + }); + + test('createCommentInPlace ignores call if nothing is selected', () => { + const createRangeCommentStub = sandbox.stub(diffElement, + 'createRangeComment'); + const addDraftAtLineStub = sandbox.stub(diffElement, 'addDraftAtLine'); + cursorElement.diffRow = undefined; + cursorElement.createCommentInPlace(); + assert.isFalse(createRangeCommentStub.called); + assert.isFalse(addDraftAtLineStub.called); + }); + }); + test('getAddress', () => { // It should initialize to the first chunk: line 5 of the revision. assert.deepEqual(cursorElement.getAddress(), @@ -306,4 +376,24 @@ }); }); }); + + suite('gr-diff-cursor event tests', () => { + let sandbox; + let someEmptyDiv; + + setup(() => { + sandbox = sinon.sandbox.create(); + someEmptyDiv = fixture('empty'); + }); + + teardown(() => sandbox.restore()); + + test('ready is fired after component is rendered', done => { + const cursorElement = document.createElement('gr-diff-cursor'); + cursorElement.addEventListener('ready', () => { + done(); + }); + someEmptyDiv.appendChild(cursorElement); + }); + }); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js index c1d53aa..14c8800 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js
@@ -31,6 +31,7 @@ /** * The DOM API textContent.length calculation is broken when the text * contains Unicode. See https://mathiasbynens.be/notes/javascript-unicode . + * * @param {!Text} node text node. * @return {number} The length of the text. */ @@ -43,6 +44,65 @@ }, /** + * Annotates the [offset, offset+length) text segment in the parent with the + * element definition provided as arguments. + * + * @param {!Element} parent the node whose contents will be annotated. + * @param {number} offset the 0-based offset from which the annotation will + * start. + * @param {number} length of the annotated text. + * @param {GrAnnotation.ElementSpec} elementSpec the spec to create the + * annotating element. + */ + annotateWithElement(parent, offset, length, {tagName, attributes = {}}) { + let childNodes; + + if (parent instanceof Element) { + childNodes = Array.from(parent.childNodes); + } else if (parent instanceof Text) { + childNodes = [parent]; + parent = parent.parentNode; + } else { + return; + } + + const nestedNodes = []; + for (let node of childNodes) { + const initialNodeLength = this.getLength(node); + // If the current node is completely before the offset. + if (offset > 0 && initialNodeLength <= offset) { + offset -= initialNodeLength; + continue; + } + + if (offset > 0) { + node = this.splitNode(node, offset); + offset = 0; + } + if (this.getLength(node) > length) { + this.splitNode(node, length); + } + nestedNodes.push(node); + + length -= this.getLength(node); + if (!length) break; + } + + const wrapper = document.createElement(tagName); + const sanitizer = window.Polymer.sanitizeDOMValue; + for (const [name, value] of Object.entries(attributes)) { + wrapper.setAttribute( + name, sanitizer ? + sanitizer(value, name, 'attribute', wrapper) : + value); + } + for (const inner of nestedNodes) { + parent.replaceChild(wrapper, inner); + wrapper.appendChild(inner); + } + }, + + /** * Surrounds the element's text at specified range in an ANNOTATION_TAG * element. If the element has child elements, the range is split and * applied as deeply as possible. @@ -210,5 +270,15 @@ }, }; + /** + * Data used to construct an element. + * + * @typedef {{ + * tagName: string, + * attributes: (!Object<string, *>|undefined) + * }} + */ + GrAnnotation.ElementSpec; + window.GrAnnotation = GrAnnotation; })(window);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html index 86d5e45..c7fe997 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-annotation</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -40,13 +40,19 @@ let str; let parent; let textNode; + let sandbox; setup(() => { + sandbox = sinon.sandbox.create(); parent = fixture('basic'); textNode = parent.childNodes[0]; str = textNode.textContent; }); + teardown(() => { + sandbox.restore(); + }); + test('_annotateText Case 1', () => { GrAnnotation._annotateText(textNode, 0, str.length, 'foobar'); @@ -168,7 +174,7 @@ assert.equal(layer4[0].textContent + layer4[1].textContent + layer4[2].textContent, - layers[3]); + layers[3]); }); test('splitTextNode', () => { @@ -191,5 +197,95 @@ assert(node.textContent, helloString); assert(tail.textContent, unicodeString); }); + + suite('annotateWithElement', () => { + const fullText = '01234567890123456789'; + let mockSanitize; + + setup(() => { + mockSanitize = sandbox.spy(window.Polymer, 'sanitizeDOMValue'); + }); + + test('annotates when fully contained', () => { + const length = 10; + const container = document.createElement('div'); + container.textContent = fullText; + GrAnnotation.annotateWithElement( + container, 1, length, {tagName: 'test-wrapper'}); + + assert.equal( + container.innerHTML, + '0<test-wrapper>1234567890</test-wrapper>123456789'); + }); + + test('annotates when spanning multiple nodes', () => { + const length = 10; + const container = document.createElement('div'); + container.textContent = fullText; + GrAnnotation.annotateElement(container, 5, length, 'testclass'); + GrAnnotation.annotateWithElement( + container, 1, length, {tagName: 'test-wrapper'}); + + assert.equal( + container.innerHTML, + '0' + + '<test-wrapper>' + + '1234' + + '<hl class="testclass">567890</hl>' + + '</test-wrapper>' + + '<hl class="testclass">1234</hl>' + + '56789'); + }); + + test('annotates text node', () => { + const length = 10; + const container = document.createElement('div'); + container.textContent = fullText; + GrAnnotation.annotateWithElement( + container.childNodes[0], 1, length, {tagName: 'test-wrapper'}); + + assert.equal( + container.innerHTML, + '0<test-wrapper>1234567890</test-wrapper>123456789'); + }); + + test('handles zero-length nodes', () => { + const container = document.createElement('div'); + container.appendChild(document.createTextNode('0123456789')); + container.appendChild(document.createElement('span')); + container.appendChild(document.createTextNode('0123456789')); + GrAnnotation.annotateWithElement( + container, 1, 10, {tagName: 'test-wrapper'}); + + assert.equal( + container.innerHTML, + '0<test-wrapper>123456789<span></span>0</test-wrapper>123456789'); + }); + + test('sets sanitized attributes', () => { + const container = document.createElement('div'); + container.textContent = fullText; + const attributes = { + 'href': 'foo', + 'data-foo': 'bar', + 'class': 'hello world', + }; + GrAnnotation.annotateWithElement( + container, 1, length, {tagName: 'test-wrapper', attributes}); + assert(mockSanitize.calledWith( + 'foo', 'href', 'attribute', sinon.match.instanceOf(Element))); + assert(mockSanitize.calledWith( + 'bar', 'data-foo', 'attribute', sinon.match.instanceOf(Element))); + assert(mockSanitize.calledWith( + 'hello world', + 'class', + 'attribute', + sinon.match.instanceOf(Element))); + const el = container.querySelector('test-wrapper'); + assert.equal(el.getAttribute('href'), 'foo'); + assert.equal(el.getAttribute('data-foo'), 'bar'); + assert.equal(el.getAttribute('class'), 'hello world'); + }); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js index 5472489..adb1be8 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -17,33 +17,55 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-diff-highlight', + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrDiffHighlight extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-diff-highlight'; } - properties: { + static get properties() { + return { /** @type {!Array<!Gerrit.HoveredRange>} */ - commentRanges: { - type: Array, - notify: true, - }, - loggedIn: Boolean, - /** - * querySelector can return null, so needs to be nullable. - * - * @type {?HTMLElement} - * */ - _cachedDiffBuilder: Object, - }, + commentRanges: { + type: Array, + notify: true, + }, + loggedIn: Boolean, + /** + * querySelector can return null, so needs to be nullable. + * + * @type {?HTMLElement} + * */ + _cachedDiffBuilder: Object, - behaviors: [ - Gerrit.FireBehavior, - ], + /** + * Which range is currently selected by the user. + * Stored in order to add a range-based comment + * later. + * undefined if no range is selected. + * + * @type {{side: string, range: Gerrit.Range}|undefined} + */ + selectedRange: { + type: Object, + notify: true, + }, + }; + } - listeners: { - 'comment-thread-mouseleave': '_handleCommentThreadMouseleave', - 'comment-thread-mouseenter': '_handleCommentThreadMouseenter', - 'create-range-comment': '_createRangeComment', - }, + created() { + super.created(); + this.addEventListener('comment-thread-mouseleave', + e => this._handleCommentThreadMouseleave(e)); + this.addEventListener('comment-thread-mouseenter', + e => this._handleCommentThreadMouseenter(e)); + this.addEventListener('create-comment-requested', + e => this._handleRangeCommentRequest(e)); + } get diffBuilder() { if (!this._cachedDiffBuilder) { @@ -51,12 +73,7 @@ Polymer.dom(this).querySelector('gr-diff-builder'); } return this._cachedDiffBuilder; - }, - - - isRangeSelected() { - return !!this.$$('gr-selection-action-box'); - }, + } /** * Determines side/line/range for a DOM selection and shows a tooltip. @@ -85,7 +102,7 @@ this.debounce( 'selectionChange', () => this._handleSelection(selection, isMouseUp), 10); - }, + } _getThreadEl(e) { const path = Polymer.dom(e).path || []; @@ -93,7 +110,7 @@ if (pathEl.classList.contains('comment-thread')) return pathEl; } return null; - }, + } _handleCommentThreadMouseenter(e) { const threadEl = this._getThreadEl(e); @@ -102,7 +119,7 @@ if (index !== undefined) { this.set(['commentRanges', index, 'hovering'], true); } - }, + } _handleCommentThreadMouseleave(e) { const threadEl = this._getThreadEl(e); @@ -111,7 +128,7 @@ if (index !== undefined) { this.set(['commentRanges', index, 'hovering'], false); } - }, + } _indexForThreadEl(threadEl) { const side = threadEl.getAttribute('comment-side'); @@ -120,7 +137,7 @@ if (!range) return undefined; return this._indexOfCommentRange(side, range); - }, + } _indexOfCommentRange(side, range) { function rangesEqual(a, b) { @@ -137,14 +154,15 @@ } return this.commentRanges.findIndex(commentRange => - commentRange.side === side && rangesEqual(commentRange.range, range)); - }, + commentRange.side === side && rangesEqual(commentRange.range, range)); + } /** * Get current normalized selection. * Merges multiple ranges, accounts for triple click, accounts for * syntax highligh, convert native DOM Range objects to Gerrit concepts * (line, side, etc). + * * @param {Selection} selection * @return {({ * start: { @@ -176,10 +194,11 @@ end: endRange.end, }; } - }, + } /** * Normalize a specific DOM Range. + * * @return {!Object} fixed normalized range */ _normalizeRange(domRange) { @@ -190,7 +209,7 @@ end: this._normalizeSelectionSide( range.endContainer, range.endOffset), }, domRange); - }, + } /** * Adjust triple click selection for the whole line. @@ -231,7 +250,7 @@ }; } return range; - }, + } /** * Convert DOM Range selection to concrete numbers (line, column, side). @@ -288,7 +307,7 @@ line, column, }; - }, + } /** * The only line in which add a comment tooltip is cut off is the first @@ -304,7 +323,7 @@ } actionBox.positionBelow = true; actionBox.placeBelow(range); - }, + } _isRangeValid(range) { if (!range || !range.start || !range.end) { @@ -318,7 +337,7 @@ return false; } return true; - }, + } _handleSelection(selection, isMouseUp) { const normalizedRange = this._getNormalizedRange(selection); @@ -351,12 +370,12 @@ // is empty to see that it's at the end of a line. const content = domRange.cloneContents().querySelector('.contentText'); if (isMouseUp && this._getLength(content) === 0) { - this.fire('create-range-comment', {side: start.side, range: { + this._fireCreateRangeComment(start.side, { start_line: start.line, start_character: 0, end_line: start.line, end_character: start.column, - }}); + }); } return; } @@ -367,13 +386,15 @@ const root = Polymer.dom(this.root); root.insertBefore(actionBox, root.firstElementChild); } - actionBox.range = { - start_line: start.line, - start_character: start.column, - end_line: end.line, - end_character: end.column, + this.selectedRange = { + range: { + start_line: start.line, + start_character: start.column, + end_line: end.line, + end_character: end.column, + }, + side: start.side, }; - actionBox.side = start.side; if (start.line === end.line) { this._positionActionBox(actionBox, start.line, domRange); } else if (start.node instanceof Text) { @@ -388,18 +409,29 @@ } else { this._positionActionBox(actionBox, start.line, start.node); } - }, + } - _createRangeComment(e) { + _fireCreateRangeComment(side, range) { + this.fire('create-range-comment', {side, range}); this._removeActionBox(); - }, + } + + _handleRangeCommentRequest(e) { + e.stopPropagation(); + if (!this.selectedRange) { + throw Error('Selected Range is needed for new range comment!'); + } + const {side, range} = this.selectedRange; + this._fireCreateRangeComment(side, range); + } _removeActionBox() { + this.selectedRange = undefined; const actionBox = this.$$('gr-selection-action-box'); if (actionBox) { Polymer.dom(this.root).removeChild(actionBox); } - }, + } _convertOffsetToColumn(el, offset) { if (el instanceof Element && el.classList.contains('content')) { @@ -415,7 +447,7 @@ } } return offset; - }, + } /** * Traverse Element from right to left, call callback for each node. @@ -440,7 +472,7 @@ } node = nextNode; } - }, + } /** * Get length of a node. If the node is a content node, then only give the @@ -455,6 +487,8 @@ } else { return GrAnnotation.getLength(node); } - }, - }); + } + } + + customElements.define(GrDiffHighlight.is, GrDiffHighlight); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html index 7ca5f5e..3de521d 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff-highlight</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -228,13 +228,25 @@ assert.isFalse(element.set.called); }); - test('on create-range-comment action box is removed', () => { + test(`create-range-comment for range when create-comment-requested + is fired`, () => { sandbox.stub(element, '_removeActionBox'); - element.fire('create-range-comment', { - comment: { - range: {}, + element.selectedRange = { + side: 'left', + range: { + start_line: 7, + start_character: 11, + end_line: 24, + end_character: 42, }, + }; + const requestEvent = new CustomEvent('create-comment-requested'); + let createRangeEvent; + element.addEventListener('create-range-comment', e => { + createRangeEvent = e; }); + element.dispatchEvent(requestEvent); + assert.deepEqual(element.selectedRange, createRangeEvent.detail); assert.isTrue(element._removeActionBox.called); }); }); @@ -271,14 +283,6 @@ element._handleSelection(selection); }; - const getActionRange = () => - Polymer.dom(element.root).querySelector( - 'gr-selection-action-box').range; - - const getActionSide = () => - Polymer.dom(element.root).querySelector( - 'gr-selection-action-box').side; - const getLineElByChild = node => { const stubs = contentStubs.find(stub => stub.contentTd.contains(node)); return stubs && stubs.lineEl; @@ -329,14 +333,14 @@ sandbox.spy(element, '_positionActionBox'); emulateSelection(content.firstChild, 5, content.firstChild, 12); const actionBox = element.$$('gr-selection-action-box'); - assert.isTrue(element.isRangeSelected()); - assert.deepEqual(getActionRange(), { + const {range, side} = element.selectedRange; + assert.deepEqual(range, { start_line: 138, start_character: 5, end_line: 138, end_character: 12, }); - assert.equal(getActionSide(), 'left'); + assert.equal(side, 'left'); assert.notOk(actionBox.positionBelow); }); @@ -346,16 +350,15 @@ sandbox.spy(element, '_positionActionBox'); emulateSelection( startContent.firstChild, 10, endContent.lastChild, 7); - assert.isTrue(element.isRangeSelected()); const actionBox = element.$$('gr-selection-action-box'); - - assert.deepEqual(getActionRange(), { + const {range, side} = element.selectedRange; + assert.deepEqual(range, { start_line: 119, start_character: 10, end_line: 120, end_character: 36, }); - assert.equal(getActionSide(), 'right'); + assert.equal(side, 'right'); assert.notOk(actionBox.positionBelow); }); @@ -381,8 +384,8 @@ removeAllRanges: sandbox.stub(), }; element._handleSelection(selection); - assert.isTrue(element.isRangeSelected()); - assert.deepEqual(getActionRange(), { + const {range} = element.selectedRange; + assert.deepEqual(range, { start_line: 119, start_character: 10, end_line: 120, @@ -394,43 +397,43 @@ const startContent = stubContent(119, 'right'); const endContent = stubContent(120, 'right'); emulateSelection(startContent.firstChild, 10, endContent.firstChild, 2); - assert.isTrue(element.isRangeSelected()); - assert.deepEqual(getActionRange(), { + const {range, side} = element.selectedRange; + assert.deepEqual(range, { start_line: 119, start_character: 10, end_line: 120, end_character: 2, }); - assert.equal(getActionSide(), 'right'); + assert.equal(side, 'right'); }); test('collapsed', () => { const content = stubContent(138, 'left'); emulateSelection(content.firstChild, 5, content.firstChild, 5); assert.isOk(window.getSelection().getRangeAt(0).startContainer); - assert.isFalse(element.isRangeSelected()); + assert.isFalse(!!element.selectedRange); }); test('starts inside hl', () => { const content = stubContent(140, 'left'); const hl = content.querySelector('.foo'); emulateSelection(hl.firstChild, 2, hl.nextSibling, 7); - assert.isTrue(element.isRangeSelected()); - assert.deepEqual(getActionRange(), { + const {range, side} = element.selectedRange; + assert.deepEqual(range, { start_line: 140, start_character: 8, end_line: 140, end_character: 23, }); - assert.equal(getActionSide(), 'left'); + assert.equal(side, 'left'); }); test('ends inside hl', () => { const content = stubContent(140, 'left'); const hl = content.querySelector('.bar'); emulateSelection(hl.previousSibling, 2, hl.firstChild, 3); - assert.isTrue(element.isRangeSelected()); - assert.deepEqual(getActionRange(), { + const {range} = element.selectedRange; + assert.deepEqual(range, { start_line: 140, start_character: 18, end_line: 140, @@ -442,14 +445,14 @@ const content = stubContent(140, 'left'); const hl = content.querySelectorAll('hl')[4]; emulateSelection(content.firstChild, 2, hl.firstChild, 2); - assert.isTrue(element.isRangeSelected()); - assert.deepEqual(getActionRange(), { + const {range, side} = element.selectedRange; + assert.deepEqual(range, { start_line: 140, start_character: 2, end_line: 140, end_character: 61, }); - assert.equal(getActionSide(), 'left'); + assert.equal(side, 'left'); }); test('starts outside of diff', () => { @@ -458,21 +461,21 @@ emulateSelection(contentTd.previousElementSibling, 0, contentText.firstChild, 2); - assert.isFalse(element.isRangeSelected()); + assert.isFalse(!!element.selectedRange); }); test('ends outside of diff', () => { const content = stubContent(140, 'left'); emulateSelection(content.nextElementSibling.firstChild, 2, content.firstChild, 2); - assert.isFalse(element.isRangeSelected()); + assert.isFalse(!!element.selectedRange); }); test('starts and ends on different sides', () => { const startContent = stubContent(140, 'left'); const endContent = stubContent(130, 'right'); emulateSelection(startContent.firstChild, 2, endContent.firstChild, 2); - assert.isFalse(element.isRangeSelected()); + assert.isFalse(!!element.selectedRange); }); test('starts in comment thread element', () => { @@ -481,14 +484,14 @@ '.comment-thread'); const endContent = stubContent(141, 'left'); emulateSelection(comment.firstChild, 2, endContent.firstChild, 4); - assert.isTrue(element.isRangeSelected()); - assert.deepEqual(getActionRange(), { + const {range, side} = element.selectedRange; + assert.deepEqual(range, { start_line: 140, start_character: 83, end_line: 141, end_character: 4, }); - assert.equal(getActionSide(), 'left'); + assert.equal(side, 'left'); }); test('ends in comment thread element', () => { @@ -496,14 +499,14 @@ const comment = content.parentElement.querySelector( '.comment-thread'); emulateSelection(content.firstChild, 4, comment.firstChild, 1); - assert.isTrue(element.isRangeSelected()); - assert.deepEqual(getActionRange(), { + const {range, side} = element.selectedRange; + assert.deepEqual(range, { start_line: 140, start_character: 4, end_line: 140, end_character: 83, }); - assert.equal(getActionSide(), 'left'); + assert.equal(side, 'left'); }); test('starts in context element', () => { @@ -512,7 +515,7 @@ const content = stubContent(146, 'right'); emulateSelection(contextControl, 0, content.firstChild, 7); // TODO (viktard): Select nearest line. - assert.isFalse(element.isRangeSelected()); + assert.isFalse(!!element.selectedRange); }); test('ends in context element', () => { @@ -521,35 +524,35 @@ const content = stubContent(141, 'left'); emulateSelection(content.firstChild, 2, contextControl, 1); // TODO (viktard): Select nearest line. - assert.isFalse(element.isRangeSelected()); + assert.isFalse(!!element.selectedRange); }); test('selection containing context element', () => { const startContent = stubContent(130, 'right'); const endContent = stubContent(146, 'right'); emulateSelection(startContent.firstChild, 3, endContent.firstChild, 14); - assert.isTrue(element.isRangeSelected()); - assert.deepEqual(getActionRange(), { + const {range, side} = element.selectedRange; + assert.deepEqual(range, { start_line: 130, start_character: 3, end_line: 146, end_character: 14, }); - assert.equal(getActionSide(), 'right'); + assert.equal(side, 'right'); }); test('ends at a tab', () => { const content = stubContent(140, 'left'); emulateSelection( content.firstChild, 1, content.querySelector('span'), 0); - assert.isTrue(element.isRangeSelected()); - assert.deepEqual(getActionRange(), { + const {range, side} = element.selectedRange; + assert.deepEqual(range, { start_line: 140, start_character: 1, end_line: 140, end_character: 51, }); - assert.equal(getActionSide(), 'left'); + assert.equal(side, 'left'); }); test('starts at a tab', () => { @@ -557,14 +560,14 @@ emulateSelection( content.querySelectorAll('hl')[3], 0, content.querySelectorAll('span')[1].nextSibling, 1); - assert.isTrue(element.isRangeSelected()); - assert.deepEqual(getActionRange(), { + const {range, side} = element.selectedRange; + assert.deepEqual(range, { start_line: 140, start_character: 51, end_line: 140, end_character: 71, }); - assert.equal(getActionSide(), 'left'); + assert.equal(side, 'left'); }); test('properly accounts for syntax highlighting', () => { @@ -593,14 +596,14 @@ const startContent = stubContent(119, 'right'); const endContent = stubContent(120, 'right'); emulateSelection(startContent.firstChild, 0, endContent.firstChild, 0); - assert.isTrue(element.isRangeSelected()); - assert.deepEqual(getActionRange(), { + const {range, side} = element.selectedRange; + assert.deepEqual(range, { start_line: 119, start_character: 0, end_line: 119, end_character: element._getLength(startContent), }); - assert.equal(getActionSide(), 'right'); + assert.equal(side, 'right'); }); test('_fixTripleClickSelection empty line', () => { @@ -608,14 +611,14 @@ const endContent = stubContent(165, 'left'); emulateSelection(startContent.firstChild, 0, endContent.parentElement.previousElementSibling, 0); - assert.isTrue(element.isRangeSelected()); - assert.deepEqual(getActionRange(), { + const {range, side} = element.selectedRange; + assert.deepEqual(range, { start_line: 146, start_character: 0, end_line: 146, end_character: 84, }); - assert.equal(getActionSide(), 'right'); + assert.equal(side, 'right'); }); }); });
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js index 556395c..3b6c41c 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js
@@ -97,13 +97,14 @@ /** * The DOM API textContent.length calculation is broken when the text * contains Unicode. See https://mathiasbynens.be/notes/javascript-unicode . + * * @param {text} node A text node. * @return {number} The length of the text. */ _getLength(node) { - return node - ? node.textContent.replace(REGEX_ASTRAL_SYMBOL, '_').length - : 0; + return node ? + node.textContent.replace(REGEX_ASTRAL_SYMBOL, '_').length : + 0; }, };
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js index 54398b8..7d78212 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
@@ -69,16 +69,25 @@ }; /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.PatchSetMixin + */ + /** * Wrapper around gr-diff. * * Webcomponent fetching diffs and related data from restAPI and passing them * to the presentational gr-diff for rendering. */ - Polymer({ - is: 'gr-diff-host', - + class GrDiffHost extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.PatchSetBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-diff-host'; } /** * Fired when the user selects a line. + * * @event line-selected */ @@ -94,188 +103,199 @@ * @event diff-comments-modified */ - properties: { - changeNum: String, - noAutoRender: { - type: Boolean, - value: false, - }, - /** @type {?} */ - patchRange: Object, - path: String, - prefs: { - type: Object, - }, - projectName: String, - displayLine: { - type: Boolean, - value: false, - }, - isImageDiff: { - type: Boolean, - computed: '_computeIsImageDiff(diff)', - notify: true, - }, - commitRange: Object, - filesWeblinks: { - type: Object, - value() { - return {}; + static get properties() { + return { + changeNum: String, + noAutoRender: { + type: Boolean, + value: false, }, - notify: true, - }, - hidden: { - type: Boolean, - reflectToAttribute: true, - }, - noRenderOnPrefsChange: { - type: Boolean, - value: false, - }, - comments: { - type: Object, - observer: '_commentsChanged', - }, - lineWrapping: { - type: Boolean, - value: false, - }, - viewMode: { - type: String, - value: DiffViewMode.SIDE_BY_SIDE, - }, + /** @type {?} */ + patchRange: Object, + path: String, + prefs: { + type: Object, + }, + projectName: String, + displayLine: { + type: Boolean, + value: false, + }, + isImageDiff: { + type: Boolean, + computed: '_computeIsImageDiff(diff)', + notify: true, + }, + commitRange: Object, + filesWeblinks: { + type: Object, + value() { + return {}; + }, + notify: true, + }, + hidden: { + type: Boolean, + reflectToAttribute: true, + }, + noRenderOnPrefsChange: { + type: Boolean, + value: false, + }, + comments: { + type: Object, + observer: '_commentsChanged', + }, + lineWrapping: { + type: Boolean, + value: false, + }, + viewMode: { + type: String, + value: DiffViewMode.SIDE_BY_SIDE, + }, - /** - * Special line number which should not be collapsed into a shared region. - * @type {{ - * number: number, - * leftSide: {boolean} - * }|null} - */ - lineOfInterest: Object, + /** + * Special line number which should not be collapsed into a shared region. + * + * @type {{ + * number: number, + * leftSide: {boolean} + * }|null} + */ + lineOfInterest: Object, - /** - * If the diff fails to load, show the failure message in the diff rather - * than bubbling the error up to the whole page. This is useful for when - * loading inline diffs because one diff failing need not mark the whole - * page with a failure. - */ - showLoadFailure: Boolean, + /** + * If the diff fails to load, show the failure message in the diff rather + * than bubbling the error up to the whole page. This is useful for when + * loading inline diffs because one diff failing need not mark the whole + * page with a failure. + */ + showLoadFailure: Boolean, - isBlameLoaded: { - type: Boolean, - notify: true, - computed: '_computeIsBlameLoaded(_blame)', - }, + isBlameLoaded: { + type: Boolean, + notify: true, + computed: '_computeIsBlameLoaded(_blame)', + }, - _loggedIn: { - type: Boolean, - value: false, - }, + _loggedIn: { + type: Boolean, + value: false, + }, - _loading: { - type: Boolean, - value: false, - }, + _loading: { + type: Boolean, + value: false, + }, - /** @type {?string} */ - _errorMessage: { - type: String, - value: null, - }, + /** @type {?string} */ + _errorMessage: { + type: String, + value: null, + }, - /** @type {?Object} */ - _baseImage: Object, - /** @type {?Object} */ - _revisionImage: Object, - /** - * This is a DiffInfo object. - */ - diff: { - type: Object, - notify: true, - }, + /** @type {?Object} */ + _baseImage: Object, + /** @type {?Object} */ + _revisionImage: Object, + /** + * This is a DiffInfo object. + */ + diff: { + type: Object, + notify: true, + }, - /** @type {?Object} */ - _blame: { - type: Object, - value: null, - }, + /** @type {?Object} */ + _blame: { + type: Object, + value: null, + }, - /** - * @type {!Array<!Gerrit.CoverageRange>} - */ - _coverageRanges: { - type: Array, - value: () => [], - }, + /** + * @type {!Array<!Gerrit.CoverageRange>} + */ + _coverageRanges: { + type: Array, + value: () => [], + }, - _loadedWhitespaceLevel: String, + _loadedWhitespaceLevel: String, - _parentIndex: { - type: Number, - computed: '_computeParentIndex(patchRange.*)', - }, + _parentIndex: { + type: Number, + computed: '_computeParentIndex(patchRange.*)', + }, - _syntaxHighlightingEnabled: { - type: Boolean, - computed: + _syntaxHighlightingEnabled: { + type: Boolean, + computed: '_isSyntaxHighlightingEnabled(prefs.*, diff)', - }, + }, - _layers: { - type: Array, - value: [], - }, - }, + _layers: { + type: Array, + value: [], + }, + }; + } - behaviors: [ - Gerrit.FireBehavior, - Gerrit.PatchSetBehavior, - ], - - listeners: { - // These are named inconsistently for a reason: - // The create-comment event is fired to indicate that we should - // create a comment. - // The comment-* events are just notifying that the comments did already - // change in some way, and that we should update any models we may want - // to keep in sync. - 'create-comment': '_handleCreateComment', - 'comment-discard': '_handleCommentDiscard', - 'comment-update': '_handleCommentUpdate', - 'comment-save': '_handleCommentSave', - - 'render-start': '_handleRenderStart', - 'render-content': '_handleRenderContent', - - 'normalize-range': '_handleNormalizeRange', - }, - - observers: [ - '_whitespaceChanged(prefs.ignore_whitespace, _loadedWhitespaceLevel,' + + static get observers() { + return [ + '_whitespaceChanged(prefs.ignore_whitespace, _loadedWhitespaceLevel,' + ' noRenderOnPrefsChange)', - '_syntaxHighlightingChanged(noRenderOnPrefsChange, prefs.*)', - ], + '_syntaxHighlightingChanged(noRenderOnPrefsChange, prefs.*)', + ]; + } + + created() { + super.created(); + this.addEventListener( + // These are named inconsistently for a reason: + // The create-comment event is fired to indicate that we should + // create a comment. + // The comment-* events are just notifying that the comments did already + // change in some way, and that we should update any models we may want + // to keep in sync. + 'create-comment', + e => this._handleCreateComment(e)); + this.addEventListener('comment-discard', + e => this._handleCommentDiscard(e)); + this.addEventListener('comment-update', + e => this._handleCommentUpdate(e)); + this.addEventListener('comment-save', + e => this._handleCommentSave(e)); + this.addEventListener('render-start', + () => this._handleRenderStart()); + this.addEventListener('render-content', + () => this._handleRenderContent()); + this.addEventListener('normalize-range', + event => this._handleNormalizeRange(event)); + this.addEventListener('diff-context-expanded', + event => this._handleDiffContextExpanded(event)); + } ready() { + super.ready(); if (this._canReload()) { this.reload(); } - }, + } attached() { + super.attached(); this._getLoggedIn().then(loggedIn => { this._loggedIn = loggedIn; }); - }, + } /** - * @param {boolean=} haveParamsChanged ends reporting events that started - * on location change. + * @param {boolean=} shouldReportMetric indicate a new Diff Page. This is a + * signal to report metrics event that started on location change. * @return {!Promise} **/ - reload(haveParamsChanged) { + reload(shouldReportMetric) { this._loading = true; this._errorMessage = null; const whitespaceLevel = this._getIgnoreWhitespace(); @@ -283,31 +303,18 @@ const layers = [this.$.syntaxLayer]; // Get layers from plugins (if any). for (const pluginLayer of this.$.jsAPI.getDiffLayers( - this.diffPath, this.changeNum, this.patchNum)) { + this.path, this.changeNum, this.patchNum)) { layers.push(pluginLayer); } this._layers = layers; - if (haveParamsChanged) { + if (shouldReportMetric) { // We listen on render viewport only on DiffPage (on paramsChanged) this._listenToViewportRender(); } this._coverageRanges = []; - const {changeNum, path, patchRange: {basePatchNum, patchNum}} = this; - this.$.jsAPI.getCoverageRanges(changeNum, path, basePatchNum, patchNum). - then(coverageRanges => { - if (changeNum !== this.changeNum || - path !== this.path || - basePatchNum !== this.patchRange.basePatchNum || - patchNum !== this.patchRange.patchNum) { - return; - } - this._coverageRanges = coverageRanges; - }).catch(err => { - console.warn('Loading coverage ranges failed: ', err); - }); - + this._getCoverageData(); const diffRequest = this._getDiff() .then(diff => { this._loadedWhitespaceLevel = whitespaceLevel; @@ -326,6 +333,8 @@ return this._loadDiffAssets(diff); }); + // Not waiting for coverage ranges intentionally as + // plugin loading should not block the content rendering return Promise.all([diffRequest, assetRequest]) .then(results => { const diff = results[0]; @@ -335,8 +344,8 @@ this.filesWeblinks = this._getFilesWeblinks(diff); return new Promise(resolve => { const callback = event => { - const needsSyntaxHighlighting = event.detail - && event.detail.contentRendered; + const needsSyntaxHighlighting = event.detail && + event.detail.contentRendered; if (needsSyntaxHighlighting) { this.$.reporting.time(TimingLabel.SYNTAX); this.$.syntaxLayer.process().then(() => { @@ -349,6 +358,11 @@ resolve(); } this.removeEventListener('render', callback); + if (shouldReportMetric) { + // We report diffViewContentDisplayed only on reload caused + // by params changed - expected only on Diff Page. + this.$.reporting.diffViewContentDisplayed(); + } }; this.addEventListener('render', callback); this.diff = diff; @@ -358,7 +372,50 @@ console.warn('Error encountered loading diff:', err); }) .then(() => { this._loading = false; }); - }, + } + + _getCoverageData() { + const {changeNum, path, patchRange: {basePatchNum, patchNum}} = this; + this.$.jsAPI.getCoverageAnnotationApi(). + then(coverageAnnotationApi => { + if (!coverageAnnotationApi) return; + const provider = coverageAnnotationApi.getCoverageProvider(); + return provider(changeNum, path, basePatchNum, patchNum) + .then(coverageRanges => { + if (!coverageRanges || + changeNum !== this.changeNum || + path !== this.path || + basePatchNum !== this.patchRange.basePatchNum || + patchNum !== this.patchRange.patchNum) { + return; + } + + const existingCoverageRanges = this._coverageRanges; + this._coverageRanges = coverageRanges; + + // Notify with existing coverage ranges + // in case there is some existing coverage data that needs to be removed + existingCoverageRanges.forEach(range => { + coverageAnnotationApi.notify( + path, + range.code_range.start_line, + range.code_range.end_line, + range.side); + }); + + // Notify with new coverage data + coverageRanges.forEach(range => { + coverageAnnotationApi.notify( + path, + range.code_range.start_line, + range.code_range.end_line, + range.side); + }); + }); + }).catch(err => { + console.warn('Loading coverage ranges failed: ', err); + }); + } _getFilesWeblinks(diff) { if (!this.commitRange) { @@ -372,29 +429,34 @@ this.projectName, this.commitRange.commit, this.path, {weblinks: diff && diff.meta_b && diff.meta_b.web_links}), }; - }, + } /** Cancel any remaining diff builder rendering work. */ cancel() { this.$.diff.cancel(); - }, + } /** @return {!Array<!HTMLElement>} */ getCursorStops() { return this.$.diff.getCursorStops(); - }, + } /** @return {boolean} */ isRangeSelected() { return this.$.diff.isRangeSelected(); - }, + } + + createRangeComment() { + return this.$.diff.createRangeComment(); + } toggleLeftDiff() { this.$.diff.toggleLeftDiff(); - }, + } /** * Load and display blame information for the base of the diff. + * * @return {Promise} A promise that resolves when blame finishes rendering. */ loadBlame() { @@ -408,45 +470,46 @@ this._blame = blame; }); - }, + } /** Unload blame information for the diff. */ clearBlame() { this._blame = null; - }, + } /** * The thread elements in this diff, in no particular order. + * * @return {!Array<!HTMLElement>} */ getThreadEls() { return Array.from( Polymer.dom(this.$.diff).querySelectorAll('.comment-thread')); - }, + } /** @param {HTMLElement} el */ addDraftAtLine(el) { this.$.diff.addDraftAtLine(el); - }, + } clearDiffContent() { this.$.diff.clearDiffContent(); - }, + } expandAllContext() { this.$.diff.expandAllContext(); - }, + } /** @return {!Promise} */ _getLoggedIn() { return this.$.restAPI.getLoggedIn(); - }, + } /** @return {boolean}} */ _canReload() { return !!this.changeNum && !!this.patchRange && !!this.path && !this.noAutoRender; - }, + } /** @return {!Promise<!Object>} */ _getDiff() { @@ -462,7 +525,7 @@ reject) .then(resolve); }); - }, + } _handleGetDiffError(response) { // Loading the diff may respond with 409 if the file is too large. In this @@ -482,7 +545,7 @@ } this.fire('page-error', {response}); - }, + } /** * Report info about the diff response. @@ -511,7 +574,7 @@ // digits. Diffs with no delta are considered 0%. const totalDelta = rebaseDelta + nonRebaseDelta; const percentRebaseDelta = !totalDelta ? 0 : - Math.round(100 * rebaseDelta / totalDelta); + Math.round(100 * rebaseDelta / totalDelta); // Report the due_to_rebase percentage in the "diff" category when // applicable. @@ -523,7 +586,7 @@ this.$.reporting.reportInteraction(EVENT_NONZERO_REBASE, percentRebaseDelta); } - }, + } /** * @param {Object} diff @@ -540,7 +603,7 @@ this._revisionImage = null; return Promise.resolve(); } - }, + } /** * @param {Object} diff @@ -548,7 +611,7 @@ */ _computeIsImageDiff(diff) { return isImageDiff(diff); - }, + } _commentsChanged(newComments) { const allComments = []; @@ -569,7 +632,7 @@ const threadEl = this._createThreadElement(thread); this._attachThreadElement(threadEl); } - }, + } /** * @param {!Array<!Object>} comments @@ -588,7 +651,7 @@ // thread and append to it. if (comment.in_reply_to) { const thread = threads.find(thread => - thread.comments.some(c => c.id === comment.in_reply_to)); + thread.comments.some(c => c.id === comment.in_reply_to)); if (thread) { thread.comments.push(comment); continue; @@ -611,7 +674,7 @@ threads.push(newThread); } return threads; - }, + } /** * @param {Object} blame @@ -619,7 +682,7 @@ */ _computeIsBlameLoaded(blame) { return !!blame; - }, + } /** * @param {Object} diff @@ -628,7 +691,7 @@ _getImages(diff) { return this.$.restAPI.getImagesForDiff(this.changeNum, diff, this.patchRange); - }, + } /** @param {CustomEvent} e */ _handleCreateComment(e) { @@ -638,7 +701,7 @@ threadEl.addOrEditDraft(lineNum, range); this.$.reporting.recordDraftInteraction(); - }, + } /** * Gets or creates a comment thread at a given location. @@ -665,18 +728,18 @@ this._attachThreadElement(threadEl); } return threadEl; - }, + } _attachThreadElement(threadEl) { Polymer.dom(this.$.diff).appendChild(threadEl); - }, + } _clearThreads() { for (const threadEl of this.getThreadEls()) { const parent = Polymer.dom(threadEl).parentNode; Polymer.dom(parent).removeChild(threadEl); } - }, + } _createThreadElement(thread) { const threadEl = document.createElement('gr-comment-thread'); @@ -707,7 +770,7 @@ }; threadEl.addEventListener('thread-discard', threadDiscardListener); return threadEl; - }, + } /** * Gets a comment thread element at a given location. @@ -729,14 +792,14 @@ } function matchesRange(threadEl) { const threadRange = /** @type {!Gerrit.Range} */( - JSON.parse(threadEl.getAttribute('range'))); + JSON.parse(threadEl.getAttribute('range'))); return Gerrit.rangesEqual(threadRange, range); } const filteredThreadEls = this._filterThreadElsForLocation( this.getThreadEls(), line, commentSide).filter(matchesRange); return filteredThreadEls.length ? filteredThreadEls[0] : null; - }, + } /** * @param {!Array<!HTMLElement>} threadEls @@ -780,15 +843,15 @@ matchers.push(matchesFileComment); } return threadEls.filter(threadEl => - matchers.some(matcher => matcher(threadEl))); - }, + matchers.some(matcher => matcher(threadEl))); + } _getIgnoreWhitespace() { if (!this.prefs || !this.prefs.ignore_whitespace) { return WHITESPACE_IGNORE_NONE; } return this.prefs.ignore_whitespace; - }, + } _whitespaceChanged( preferredWhitespaceLevel, loadedWhitespaceLevel, @@ -806,7 +869,7 @@ !noRenderOnPrefsChange) { this.reload(); } - }, + } _syntaxHighlightingChanged(noRenderOnPrefsChange, prefsChangeRecord) { // Polymer 2: check for undefined @@ -824,7 +887,7 @@ if (!noRenderOnPrefsChange) { this.reload(); } - }, + } /** * @param {Object} patchRangeRecord @@ -832,8 +895,8 @@ */ _computeParentIndex(patchRangeRecord) { return this.isMergeParent(patchRangeRecord.base.basePatchNum) ? - this.getParentIndex(patchRangeRecord.base.basePatchNum) : null; - }, + this.getParentIndex(patchRangeRecord.base.basePatchNum) : null; + } _handleCommentSave(e) { const comment = e.detail.comment; @@ -841,13 +904,13 @@ const idx = this._findDraftIndex(comment, side); this.set(['comments', side, idx], comment); this._handleCommentSaveOrDiscard(); - }, + } _handleCommentDiscard(e) { const comment = e.detail.comment; this._removeComment(comment); this._handleCommentSaveOrDiscard(); - }, + } /** * Closure annotation for Polymer.prototype.push is off. Submitted PR: @@ -868,17 +931,17 @@ } else { // Create new draft. this.push(['comments', side], comment); } - }, + } _handleCommentSaveOrDiscard() { this.dispatchEvent(new CustomEvent( 'diff-comments-modified', {bubbles: true, composed: true})); - }, + } _removeComment(comment) { const side = comment.__commentSide; this._removeCommentFromSide(comment, side); - }, + } _removeCommentFromSide(comment, side) { let idx = this._findCommentIndex(comment, side); @@ -888,7 +951,7 @@ if (idx !== -1) { this.splice('comments.' + side, idx, 1); } - }, + } /** @return {number} */ _findCommentIndex(comment, side) { @@ -896,7 +959,7 @@ return -1; } return this.comments[side].findIndex(item => item.id === comment.id); - }, + } /** @return {number} */ _findDraftIndex(comment, side) { @@ -905,7 +968,7 @@ } return this.comments[side].findIndex( item => item.__draftID === comment.__draftID); - }, + } _isSyntaxHighlightingEnabled(preferenceChangeRecord, diff) { if (!preferenceChangeRecord || @@ -916,7 +979,7 @@ } return !this._anyLineTooLong(diff) && this.$.diff.getDiffLength(diff) <= SYNTAX_MAX_DIFF_LENGTH; - }, + } /** * @return {boolean} whether any of the lines in diff are longer @@ -926,11 +989,11 @@ if (!diff) return false; return diff.content.some(section => { const lines = section.ab ? - section.ab : - (section.a || []).concat(section.b || []); + section.ab : + (section.a || []).concat(section.b || []); return lines.some(line => line.length >= SYNTAX_MAX_LINE_LENGTH); }); - }, + } _listenToViewportRender() { const renderUpdateListener = start => { @@ -941,22 +1004,28 @@ }; this.$.syntaxLayer.addListener(renderUpdateListener); - }, + } _handleRenderStart() { this.$.reporting.time(TimingLabel.TOTAL); this.$.reporting.time(TimingLabel.CONTENT); - }, + } _handleRenderContent() { this.$.reporting.timeEnd(TimingLabel.CONTENT); - this.$.reporting.diffViewContentDisplayed(); - }, + } _handleNormalizeRange(event) { this.$.reporting.reportInteraction('normalize-range', `Modified invalid comment range on l. ${event.detail.lineNum}` + ` of the ${event.detail.side} side`); - }, - }); + } + + _handleDiffContextExpanded(event) { + this.$.reporting.reportInteraction( + 'diff-context-expanded', event.detail.numLines); + } + } + + customElements.define(GrDiffHost.is, GrDiffHost); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html index 16b7728..1c70747 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -58,7 +58,6 @@ sandbox.restore(); }); - suite('plugin layers', () => { const pluginLayers = [{annotate: () => {}}, {annotate: () => {}}]; setup(() => { @@ -320,7 +319,7 @@ element.patchRange = {}; element.$.restAPI.getDiffPreferences().then(prefs => { element.prefs = prefs; - return element.reload(); + return element.reload(true); }); // Multiple cascading microtasks are scheduled. setTimeout(() => { @@ -509,7 +508,7 @@ 'getB64FileContents', (changeId, patchNum, path, opt_parentIndex) => { return Promise.resolve(opt_parentIndex === 1 ? mockFile1 : - mockFile2); + mockFile2); }); element.patchRange = {basePatchNum: 'PARENT', patchNum: 1}; @@ -1261,7 +1260,6 @@ assert.equal(threads[0].isOnParent, false); assert.equal(threads[0].patchNum, 2); - // Try to fetch a thread with a different range. range = { start_line: 1, @@ -1431,5 +1429,69 @@ assert.isFalse(element.$.syntaxLayer.enabled); }); }); + + suite('coverage layer', () => { + let notifyStub; + setup(() => { + notifyStub = sinon.stub(); + stub('gr-js-api-interface', { + getCoverageAnnotationApi() { + return Promise.resolve({ + notify: notifyStub, + getCoverageProvider() { + return () => Promise.resolve([ + { + type: 'COVERED', + side: 'right', + code_range: { + start_line: 1, + end_line: 2, + }, + }, + { + type: 'NOT_COVERED', + side: 'right', + code_range: { + start_line: 3, + end_line: 4, + }, + }, + ]); + }, + }); + }, + }); + element = fixture('basic'); + const prefs = { + line_length: 10, + show_tabs: true, + tab_size: 4, + context: -1, + }; + element.diff = { + content: [{ + a: ['foo'], + }], + }; + element.patchRange = {}; + element.prefs = prefs; + }); + + test('getCoverageAnnotationApi should be called', done => { + element.reload(); + flush(() => { + assert.isTrue(element.$.jsAPI.getCoverageAnnotationApi.calledOnce); + done(); + }); + }); + + test('coverageRangeChanged should be called', done => { + element.reload(); + flush(() => { + assert.equal(notifyStub.callCount, 2); + done(); + }); + }); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js index 88dd91a..ae7ef6d 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
@@ -17,34 +17,38 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-diff-mode-selector', + class GrDiffModeSelector extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-diff-mode-selector'; } - properties: { - mode: { - type: String, - notify: true, - }, - - /** - * If set to true, the user's preference will be updated every time a - * button is tapped. Don't set to true if there is no user. - */ - saveOnChange: { - type: Boolean, - value: false, - }, - - /** @type {?} */ - _VIEW_MODES: { - type: Object, - readOnly: true, - value: { - SIDE_BY_SIDE: 'SIDE_BY_SIDE', - UNIFIED: 'UNIFIED_DIFF', + static get properties() { + return { + mode: { + type: String, + notify: true, }, - }, - }, + + /** + * If set to true, the user's preference will be updated every time a + * button is tapped. Don't set to true if there is no user. + */ + saveOnChange: { + type: Boolean, + value: false, + }, + + /** @type {?} */ + _VIEW_MODES: { + type: Object, + readOnly: true, + value: { + SIDE_BY_SIDE: 'SIDE_BY_SIDE', + UNIFIED: 'UNIFIED_DIFF', + }, + }, + }; + } /** * Set the mode. If save on change is enabled also update the preference. @@ -54,18 +58,20 @@ this.$.restAPI.savePreferences({diff_view: newMode}); } this.mode = newMode; - }, + } _computeSelectedClass(diffViewMode, buttonViewMode) { return buttonViewMode === diffViewMode ? 'selected' : ''; - }, + } _handleSideBySideTap() { this.setMode(this._VIEW_MODES.SIDE_BY_SIDE); - }, + } _handleUnifiedTap() { this.setMode(this._VIEW_MODES.UNIFIED); - }, - }); + } + } + + customElements.define(GrDiffModeSelector.is, GrDiffModeSelector); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html index adeaa15..333a3d6 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff-mode-selector</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js index 6ecd4d6..7ed27dd 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
@@ -17,39 +17,44 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-diff-preferences-dialog', + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrDiffPreferencesDialog extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-diff-preferences-dialog'; } - properties: { + static get properties() { + return { /** @type {?} */ - diffPrefs: Object, + diffPrefs: Object, - _diffPrefsChanged: Boolean, - }, - - behaviors: [ - Gerrit.FireBehavior, - ], + _diffPrefsChanged: Boolean, + }; + } getFocusStops() { return { start: this.$.diffPreferences.$.contextSelect, end: this.$.saveButton, }; - }, + } resetFocus() { this.$.diffPreferences.$.contextSelect.focus(); - }, + } _computeHeaderClass(changed) { return changed ? 'edited' : ''; - }, + } _handleCancelDiff(e) { e.stopPropagation(); this.$.diffPrefsOverlay.close(); - }, + } open() { this.$.diffPrefsOverlay.open().then(() => { @@ -57,7 +62,7 @@ this.$.diffPrefsOverlay.setFocusStops(focusStops); this.resetFocus(); }); - }, + } _handleSaveDiffPreferences() { this.$.diffPreferences.save().then(() => { @@ -65,6 +70,8 @@ this.$.diffPrefsOverlay.close(); }); - }, - }); + } + } + + customElements.define(GrDiffPreferencesDialog.is, GrDiffPreferencesDialog); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js index 4716544..0b46d75 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
@@ -64,75 +64,83 @@ * that the part that is within the context or has comments is shown, while * the rest is not. */ - Polymer({ - is: 'gr-diff-processor', + class GrDiffProcessor extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-diff-processor'; } - properties: { + static get properties() { + return { - /** - * The amount of context around collapsed groups. - */ - context: Number, + /** + * The amount of context around collapsed groups. + */ + context: Number, - /** - * The array of groups output by the processor. - */ - groups: { - type: Array, - notify: true, - }, + /** + * The array of groups output by the processor. + */ + groups: { + type: Array, + notify: true, + }, - /** - * Locations that should not be collapsed, including the locations of - * comments. - */ - keyLocations: { - type: Object, - value() { return {left: {}, right: {}}; }, - }, + /** + * Locations that should not be collapsed, including the locations of + * comments. + */ + keyLocations: { + type: Object, + value() { return {left: {}, right: {}}; }, + }, - /** - * The maximum number of lines to process synchronously. - */ - _asyncThreshold: { - type: Number, - value: 64, - }, + /** + * The maximum number of lines to process synchronously. + */ + _asyncThreshold: { + type: Number, + value: 64, + }, - /** @type {?number} */ - _nextStepHandle: Number, - /** - * The promise last returned from `process()` while the asynchronous - * processing is running - `null` otherwise. Provides a `cancel()` - * method that rejects it with `{isCancelled: true}`. - * @type {?Object} - */ - _processPromise: { - type: Object, - value: null, - }, - _isScrolling: Boolean, - }, + /** @type {?number} */ + _nextStepHandle: Number, + /** + * The promise last returned from `process()` while the asynchronous + * processing is running - `null` otherwise. Provides a `cancel()` + * method that rejects it with `{isCancelled: true}`. + * + * @type {?Object} + */ + _processPromise: { + type: Object, + value: null, + }, + _isScrolling: Boolean, + }; + } attached() { + super.attached(); this.listen(window, 'scroll', '_handleWindowScroll'); - }, + } detached() { + super.detached(); this.cancel(); this.unlisten(window, 'scroll', '_handleWindowScroll'); - }, + } _handleWindowScroll() { this._isScrolling = true; this.debounce('resetIsScrolling', () => { this._isScrolling = false; }, 50); - }, + } /** * Asynchronously process the diff chunks into groups. As it processes, it * will splice groups into the `groups` property of the component. + * * @param {!Array<!Gerrit.DiffChunk>} chunks * @param {boolean} isBinary * @return {!Promise<!Array<!Object>>} A promise that resolves with an @@ -150,7 +158,6 @@ // so finish processing. if (isBinary) { return Promise.resolve(); } - this._processPromise = util.makeCancelable( new Promise(resolve => { const state = { @@ -197,7 +204,7 @@ })); return this._processPromise .finally(() => { this._processPromise = null; }); - }, + } /** * Cancel any jobs that are running. @@ -210,7 +217,7 @@ if (this._processPromise) { this._processPromise.cancel(); } - }, + } /** * Process the next uncollapsible chunk, or the next collapsible chunks. @@ -237,15 +244,15 @@ return this._processCollapsibleChunks( state, chunks, firstUncollapsibleChunkIndex); - }, + } _linesLeft(chunk) { return chunk.ab || chunk.a || []; - }, + } _linesRight(chunk) { return chunk.ab || chunk.b || []; - }, + } _firstUncollapsibleChunkIndex(chunks, offset) { let chunkIndex = offset; @@ -254,11 +261,11 @@ chunkIndex++; } return chunkIndex; - }, + } _isCollapsibleChunk(chunk) { return (chunk.ab || chunk.common) && !chunk.keyLocation; - }, + } /** * Process a stretch of collapsible chunks. @@ -290,7 +297,7 @@ if (this.context !== WHOLE_FILE) { const hiddenStart = state.chunkIndex === 0 ? 0 : this.context; const hiddenEnd = lineCount - ( - firstUncollapsibleChunkIndex === chunks.length ? + firstUncollapsibleChunkIndex === chunks.length ? 0 : this.context); groups = GrDiffGroup.hideInContextControl( groups, hiddenStart, hiddenEnd); @@ -304,7 +311,7 @@ groups, newChunkIndex: firstUncollapsibleChunkIndex, }; - }, + } _commonChunkLength(chunk) { console.assert(chunk.ab || chunk.common); @@ -312,7 +319,7 @@ !chunk.a || (chunk.b && chunk.a.length === chunk.b.length), `common chunk needs same number of a and b lines: `, chunk); return this._linesLeft(chunk).length; - }, + } /** * @param {!Array<!Object>} chunks @@ -328,7 +335,7 @@ offsetRight += chunkLength; return group; }); - }, + } /** * @param {!Object} chunk @@ -344,7 +351,7 @@ group.dueToRebase = chunk.due_to_rebase; group.ignoredWhitespaceOnly = chunk.common; return group; - }, + } _linesFromChunk(chunk, offsetLeft, offsetRight) { if (chunk.ab) { @@ -367,7 +374,7 @@ chunk[DiffHighlights.ADDED])); } return lines; - }, + } /** * @param {string} lineType (GrDiffLine.Type) @@ -381,7 +388,7 @@ this._convertIntralineInfos(rows, opt_intralineInfos) : undefined; return rows.map((row, i) => this._lineFromRow( lineType, offset, offset, row, i, grDiffHighlights)); - }, + } /** * @param {string} type (GrDiffLine.Type) @@ -404,15 +411,14 @@ line.hasIntralineInfo = false; } return line; - }, + } _makeFileComments() { const line = new GrDiffLine(GrDiffLine.Type.BOTH); line.beforeNumber = GrDiffLine.FILE; line.afterNumber = GrDiffLine.FILE; return new GrDiffGroup(GrDiffGroup.Type.BOTH, [line]); - }, - + } /** * Split chunks into smaller chunks of the same kind. @@ -454,12 +460,13 @@ } } return newChunks; - }, + } /** * In order to show key locations, such as comments, out of the bounds of * the selected context, treat them as separate chunks within the model so * that the content (and context surrounding it) renders correctly. + * * @param {!Array<!Object>} chunks DiffContents as returned from server. * @return {!Array<!Object>} Finer grained DiffContents. */ @@ -483,7 +490,7 @@ if (chunk.common && chunk.a.length != chunk.b.length) { throw new Error( - 'DiffContent with common=true must always have equal length'); + 'DiffContent with common=true must always have equal length'); } const numLines = this._commonChunkLength(chunk); const chunkEnds = this._findChunkEndsAtKeyLocations( @@ -494,7 +501,7 @@ if (chunk.ab) { result.push(...this._splitAtChunkEnds(chunk.ab, chunkEnds) .map(({lines, keyLocation}) => - Object.assign({}, chunk, {ab: lines, keyLocation}))); + Object.assign({}, chunk, {ab: lines, keyLocation}))); } else if (chunk.common) { const aChunks = this._splitAtChunkEnds(chunk.a, chunkEnds); const bChunks = this._splitAtChunkEnds(chunk.b, chunkEnds); @@ -505,7 +512,7 @@ } return result; - }, + } /** * @return {!Array<{offset: number, keyLocation: boolean}>} Offsets of the @@ -536,7 +543,7 @@ } return result; - }, + } _splitAtChunkEnds(lines, chunkEnds) { const result = []; @@ -547,7 +554,7 @@ lastChunkEndOffset = offset; } return result; - }, + } /** * Converts `IntralineInfo`s return by the API to `GrLineHighlights` used @@ -597,12 +604,13 @@ normalized.push(lineHighlight); } return normalized; - }, + } /** * If a group is an addition or a removal, break it down into smaller groups * of that type using the MAX_GROUP_SIZE. If the group is a shared chunk * or a delta it is returned as the single element of the result array. + * * @param {!Gerrit.DiffChunk} chunk A raw chunk from a diff response. * @return {!Array<!Array<!Object>>} */ @@ -627,11 +635,12 @@ } return subChunk; }); - }, + } /** * Given an array and a size, return an array of arrays where no inner array * is larger than that size, preserving the original order. + * * @param {!Array<T>} array * @param {number} size * @return {!Array<!Array<T>>} @@ -645,6 +654,8 @@ const tail = array.slice(array.length - size); return this._breakdown(head, size).concat([tail]); - }, - }); + } + } + + customElements.define(GrDiffProcessor.is, GrDiffProcessor); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html index c04b066..308428f 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff-processor test</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js index 055b200..37d54c9 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -30,35 +30,46 @@ const getNewCache = () => { return {left: null, right: null}; }; - Polymer({ - is: 'gr-diff-selection', + /** + * @appliesMixin Gerrit.DomUtilMixin + */ + class GrDiffSelection extends Polymer.mixinBehaviors( [ + Gerrit.DomUtilBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-diff-selection'; } - properties: { - diff: Object, - /** @type {?Object} */ - _cachedDiffBuilder: Object, - _linesCache: { - type: Object, - value: getNewCache(), - }, - }, + static get properties() { + return { + diff: Object, + /** @type {?Object} */ + _cachedDiffBuilder: Object, + _linesCache: { + type: Object, + value: getNewCache(), + }, + }; + } - observers: [ - '_diffChanged(diff)', - ], + static get observers() { + return [ + '_diffChanged(diff)', + ]; + } - listeners: { - copy: '_handleCopy', - down: '_handleDown', - }, - - behaviors: [ - Gerrit.DomUtilBehavior, - ], + created() { + super.created(); + this.addEventListener('copy', + e => this._handleCopy(e)); + Polymer.Gestures.addListener(this, 'down', + e => this._handleDown(e)); + } attached() { + super.attached(); this.classList.add(SelectionClass.RIGHT); - }, + } get diffBuilder() { if (!this._cachedDiffBuilder) { @@ -66,11 +77,11 @@ Polymer.dom(this).querySelector('gr-diff-builder'); } return this._cachedDiffBuilder; - }, + } _diffChanged() { this._linesCache = getNewCache(); - }, + } _handleDownOnRangeComment(node) { if (node && @@ -79,13 +90,13 @@ this._setClasses([ SelectionClass.COMMENT, node.commentSide === 'left' ? - SelectionClass.LEFT : - SelectionClass.RIGHT, + SelectionClass.LEFT : + SelectionClass.RIGHT, ]); return true; } return false; - }, + } _handleDown(e) { // Handle the down event on comment thread in Polymer 2 @@ -106,8 +117,8 @@ const side = this.diffBuilder.getSideByLineEl(lineEl); targetClasses.push(side === 'left' ? - SelectionClass.LEFT : - SelectionClass.RIGHT); + SelectionClass.LEFT : + SelectionClass.RIGHT); if (commentSelected) { targetClasses.push(SelectionClass.COMMENT); @@ -115,11 +126,12 @@ } this._setClasses(targetClasses); - }, + } /** * Set the provided list of classes on the element, to the exclusion of all * other SelectionClass values. + * * @param {!Array<!string>} targetClasses */ _setClasses(targetClasses) { @@ -138,11 +150,11 @@ this.classList.add(_class); } } - }, + } _getCopyEventTarget(e) { return Polymer.dom(e).rootTarget; - }, + } /** * Utility function to determine whether an element is a descendant of @@ -155,7 +167,7 @@ _elementDescendedFromClass(element, className) { return this.descendedFromClass(element, className, this.diffBuilder.diffElement); - }, + } _handleCopy(e) { let commentSelected = false; @@ -175,7 +187,7 @@ e.clipboardData.setData('Text', text); e.preventDefault(); } - }, + } /** * For Polymer 2, use shadowRoot.getSelection instead. @@ -186,7 +198,7 @@ diffHost.shadowRoot && diffHost.shadowRoot.getSelection(); return selection ? selection: window.getSelection(); - }, + } /** * Get the text of the current selection. If commentSelected is @@ -225,7 +237,7 @@ return this._getRangeFromDiff(startLineNum, range.startOffset, endLineNum, range.endOffset, side); - }, + } /** * Query the diff object for the selected lines. @@ -247,7 +259,7 @@ lines[0] = lines[0].substring(startOffset); } return lines.join('\n'); - }, + } /** * Query the diff object for the lines from a particular side. @@ -270,7 +282,7 @@ } this._linesCache[side] = lines; return lines; - }, + } /** * Query the diffElement for comments and check whether they lie inside the @@ -308,7 +320,7 @@ } return content.join('\n'); - }, + } /** * Given a DOM node, a selection, and a selection range, recursively get all @@ -338,6 +350,8 @@ } } return text; - }, - }); + } + } + + customElements.define(GrDiffSelection.is, GrDiffSelection); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html index 0f5c6dd..6fe319d 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff-selection</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -375,7 +375,6 @@ 'his is a differ'); }); - test('multi level element as startContainer of range', () => { range.setStart(nodes[2].childNodes[1], 0); range.setEnd(nodes[2].childNodes[2], 7);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html index 40a02a3..7275ae5 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -37,6 +37,7 @@ <link rel="import" href="../../shared/revision-info/revision-info.html"> <link rel="import" href="../gr-comment-api/gr-comment-api.html"> <link rel="import" href="../gr-diff-cursor/gr-diff-cursor.html"> +<link rel="import" href="../gr-apply-fix-dialog/gr-apply-fix-dialog.html"> <link rel="import" href="../gr-diff-host/gr-diff-host.html"> <link rel="import" href="../gr-diff-mode-selector/gr-diff-mode-selector.html"> <link rel="import" href="../gr-diff-preferences-dialog/gr-diff-preferences-dialog.html"> @@ -277,7 +278,7 @@ </span> </div> <div class="rightControls"> - <span class$="blameLoader [[_computeBlameLoaderClass(_isImageDiff)]]"> + <span class$="blameLoader [[_computeBlameLoaderClass(_isImageDiff, _path)]]"> <gr-button link disabled="[[_isBlameLoading]]" @@ -343,6 +344,12 @@ on-comment-anchor-tap="_onLineSelected" on-line-selected="_onLineSelected"> </gr-diff-host> + <gr-apply-fix-dialog + id="applyFixDialog" + prefs="[[_prefs]]" + change="[[_change]]" + change-num="[[_changeNum]]"> + </gr-apply-fix-dialog> <gr-diff-preferences-dialog id="diffPreferencesDialog" diff-prefs="{{_prefs}}"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js index 8158c96..23c6741 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -33,9 +33,23 @@ UNIFIED: 'UNIFIED_DIFF', }; - Polymer({ - is: 'gr-diff-view', - + /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.PathListMixin + * @appliesMixin Gerrit.RESTClientMixin + */ + class GrDiffView extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + Gerrit.PatchSetBehavior, + Gerrit.PathListBehavior, + Gerrit.RESTClientBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-diff-view'; } /** * Fired when the title of the page should change. * @@ -48,154 +62,152 @@ * @event show-alert */ - properties: { + static get properties() { + return { /** * URL params passed from the router. */ - params: { - type: Object, - observer: '_paramsChanged', - }, - keyEventTarget: { - type: Object, - value() { return document.body; }, - }, - /** - * @type {{ diffMode: (string|undefined) }} - */ - changeViewState: { - type: Object, - notify: true, - value() { return {}; }, - observer: '_changeViewStateChanged', - }, - disableDiffPrefs: { - type: Boolean, - value: false, - }, - _diffPrefsDisabled: { - type: Boolean, - computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)', - }, - /** @type {?} */ - _patchRange: Object, - /** @type {?} */ - _commitRange: Object, - /** - * @type {{ - * subject: string, - * project: string, - * revisions: string, - * }} - */ - _change: Object, - /** @type {?} */ - _changeComments: Object, - _changeNum: String, - /** - * This is a DiffInfo object. - * This is retrieved and owned by a child component. - */ - _diff: Object, - // An array specifically formatted to be used in a gr-dropdown-list - // element for selected a file to view. - _formattedFiles: { - type: Array, - computed: '_formatFilesForDropdown(_fileList, _patchRange.patchNum, ' + - '_changeComments)', - }, - // An sorted array of files, as returned by the rest API. - _fileList: { - type: Array, - value() { return []; }, - }, - _path: { - type: String, - observer: '_pathChanged', - }, - _fileNum: { - type: Number, - computed: '_computeFileNum(_path, _formattedFiles)', - }, - _loggedIn: { - type: Boolean, - value: false, - }, - _loading: { - type: Boolean, - value: true, - }, - _prefs: Object, - _localPrefs: Object, - _projectConfig: Object, - _userPrefs: Object, - _diffMode: { - type: String, - computed: '_getDiffViewMode(changeViewState.diffMode, _userPrefs)', - }, - _isImageDiff: Boolean, - _filesWeblinks: Object, + params: { + type: Object, + observer: '_paramsChanged', + }, + keyEventTarget: { + type: Object, + value() { return document.body; }, + }, + /** + * @type {{ diffMode: (string|undefined) }} + */ + changeViewState: { + type: Object, + notify: true, + value() { return {}; }, + observer: '_changeViewStateChanged', + }, + disableDiffPrefs: { + type: Boolean, + value: false, + }, + _diffPrefsDisabled: { + type: Boolean, + computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)', + }, + /** @type {?} */ + _patchRange: Object, + /** @type {?} */ + _commitRange: Object, + /** + * @type {{ + * subject: string, + * project: string, + * revisions: string, + * }} + */ + _change: Object, + /** @type {?} */ + _changeComments: Object, + _changeNum: String, + /** + * This is a DiffInfo object. + * This is retrieved and owned by a child component. + */ + _diff: Object, + // An array specifically formatted to be used in a gr-dropdown-list + // element for selected a file to view. + _formattedFiles: { + type: Array, + computed: '_formatFilesForDropdown(_fileList, ' + + '_patchRange.patchNum, _changeComments)', + }, + // An sorted array of files, as returned by the rest API. + _fileList: { + type: Array, + value() { return []; }, + }, + _path: { + type: String, + observer: '_pathChanged', + }, + _fileNum: { + type: Number, + computed: '_computeFileNum(_path, _formattedFiles)', + }, + _loggedIn: { + type: Boolean, + value: false, + }, + _loading: { + type: Boolean, + value: true, + }, + _prefs: Object, + _localPrefs: Object, + _projectConfig: Object, + _userPrefs: Object, + _diffMode: { + type: String, + computed: '_getDiffViewMode(changeViewState.diffMode, _userPrefs)', + }, + _isImageDiff: Boolean, + _filesWeblinks: Object, - /** - * Map of paths in the current change and patch range that have comments - * or drafts or robot comments. - */ - _commentMap: Object, + /** + * Map of paths in the current change and patch range that have comments + * or drafts or robot comments. + */ + _commentMap: Object, - _commentsForDiff: Object, + _commentsForDiff: Object, - /** - * Object to contain the path of the next and previous file in the current - * change and patch range that has comments. - */ - _commentSkips: { - type: Object, - computed: '_computeCommentSkips(_commentMap, _fileList, _path)', - }, - _panelFloatingDisabled: { - type: Boolean, - value: () => { return window.PANEL_FLOATING_DISABLED; }, - }, - _editMode: { - type: Boolean, - computed: '_computeEditMode(_patchRange.*)', - }, - _isBlameLoaded: Boolean, - _isBlameLoading: { - type: Boolean, - value: false, - }, - _allPatchSets: { - type: Array, - computed: 'computeAllPatchSets(_change, _change.revisions.*)', - }, - _revisionInfo: { - type: Object, - computed: '_getRevisionInfo(_change)', - }, - _reviewedFiles: { - type: Object, - value: () => new Set(), - }, - }, + /** + * Object to contain the path of the next and previous file in the current + * change and patch range that has comments. + */ + _commentSkips: { + type: Object, + computed: '_computeCommentSkips(_commentMap, _fileList, _path)', + }, + _panelFloatingDisabled: { + type: Boolean, + value: () => { return window.PANEL_FLOATING_DISABLED; }, + }, + _editMode: { + type: Boolean, + computed: '_computeEditMode(_patchRange.*)', + }, + _isBlameLoaded: Boolean, + _isBlameLoading: { + type: Boolean, + value: false, + }, + _allPatchSets: { + type: Array, + computed: 'computeAllPatchSets(_change, _change.revisions.*)', + }, + _revisionInfo: { + type: Object, + computed: '_getRevisionInfo(_change)', + }, + _reviewedFiles: { + type: Object, + value: () => new Set(), + }, + }; + } - behaviors: [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - Gerrit.PatchSetBehavior, - Gerrit.PathListBehavior, - Gerrit.RESTClientBehavior, - ], + static get observers() { + return [ + '_getProjectConfig(_change.project)', + '_getFiles(_changeNum, _patchRange.*)', + '_setReviewedObserver(_loggedIn, params.*, _prefs)', + ]; + } - observers: [ - '_getProjectConfig(_change.project)', - '_getFiles(_changeNum, _patchRange.*)', - '_setReviewedObserver(_loggedIn, params.*, _prefs)', - ], - - keyBindings: { - esc: '_handleEscKey', - }, + get keyBindings() { + return { + esc: '_handleEscKey', + }; + } keyboardShortcuts() { return { @@ -230,37 +242,40 @@ [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null, [this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null, }; - }, + } attached() { + super.attached(); this._getLoggedIn().then(loggedIn => { this._loggedIn = loggedIn; }); + this.addEventListener('open-fix-preview', + this._onOpenFixPreview.bind(this)); this.$.cursor.push('diffs', this.$.diffHost); - }, + } _getLoggedIn() { return this.$.restAPI.getLoggedIn(); - }, + } _getProjectConfig(project) { return this.$.restAPI.getProjectConfig(project).then( config => { this._projectConfig = config; }); - }, + } _getChangeDetail(changeNum) { return this.$.restAPI.getDiffChangeDetail(changeNum).then(change => { this._change = change; return change; }); - }, + } _getChangeEdit(changeNum) { return this.$.restAPI.getChangeEdit(this._changeNum); - }, + } _getFiles(changeNum, patchRangeRecord) { // Polymer 2: check for undefined @@ -272,27 +287,27 @@ const patchRange = patchRangeRecord.base; return this.$.restAPI.getChangeFilePathsAsSpeciallySortedArray( changeNum, patchRange).then(files => { - this._fileList = files; - }); - }, + this._fileList = files; + }); + } _getDiffPreferences() { return this.$.restAPI.getDiffPreferences().then(prefs => { this._prefs = prefs; }); - }, + } _getPreferences() { return this.$.restAPI.getPreferences(); - }, + } _getWindowWidth() { return window.innerWidth; - }, + } _handleReviewedChange(e) { this._setReviewed(Polymer.dom(e).rootTarget.checked); - }, + } _setReviewed(reviewed) { if (this._editMode) { return; } @@ -301,12 +316,12 @@ this.fire('show-alert', {message: ERR_REVIEW_STATUS}); throw err; }); - }, + } _saveReviewedState(reviewed) { return this.$.restAPI.saveFileReviewed(this._changeNum, this._patchRange.patchNum, this._path, reviewed); - }, + } _handleToggleFileReviewed(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -314,7 +329,7 @@ e.preventDefault(); this._setReviewed(!this.$.reviewed.checked); - }, + } _handleEscKey(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -322,21 +337,21 @@ e.preventDefault(); this.$.diffHost.displayLine = false; - }, + } _handleLeftPane(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } e.preventDefault(); this.$.cursor.moveLeft(); - }, + } _handleRightPane(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } e.preventDefault(); this.$.cursor.moveRight(); - }, + } _handlePrevLineOrFileWithComments(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } @@ -350,7 +365,11 @@ e.preventDefault(); this.$.diffHost.displayLine = true; this.$.cursor.moveUp(); - }, + } + + _onOpenFixPreview(e) { + this.$.applyFixDialog.open(e); + } _handleNextLineOrFileWithComments(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } @@ -364,7 +383,7 @@ e.preventDefault(); this.$.diffHost.displayLine = true; this.$.cursor.moveDown(); - }, + } _moveToPreviousFileWithComment() { if (!this._commentSkips) { return; } @@ -378,7 +397,7 @@ Gerrit.Nav.navigateToDiff(this._change, this._commentSkips.previous, this._patchRange.patchNum, this._patchRange.basePatchNum); - }, + } _moveToNextFileWithComment() { if (!this._commentSkips) { return; } @@ -391,19 +410,14 @@ Gerrit.Nav.navigateToDiff(this._change, this._commentSkips.next, this._patchRange.patchNum, this._patchRange.basePatchNum); - }, + } _handleNewComment(e) { - if (this.shouldSuppressKeyboardShortcut(e)) { return; } - if (this.$.diffHost.isRangeSelected()) { return; } - if (this.modifierPressed(e)) { return; } - + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } e.preventDefault(); - const line = this.$.cursor.getTargetLineElement(); - if (line) { - this.$.diffHost.addDraftAtLine(line); - } - }, + this.$.cursor.createCommentInPlace(); + } _handlePrevFile(e) { // Check for meta key to avoid overriding native chrome shortcut. @@ -412,7 +426,7 @@ e.preventDefault(); this._navToFile(this._path, this._fileList, -1); - }, + } _handleNextFile(e) { // Check for meta key to avoid overriding native chrome shortcut. @@ -421,7 +435,7 @@ e.preventDefault(); this._navToFile(this._path, this._fileList, 1); - }, + } _handleNextChunkOrCommentThread(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } @@ -433,7 +447,7 @@ if (this.modifierPressed(e)) { return; } this.$.cursor.moveToNextChunk(); } - }, + } _handlePrevChunkOrCommentThread(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } @@ -445,7 +459,7 @@ if (this.modifierPressed(e)) { return; } this.$.cursor.moveToPreviousChunk(); } - }, + } _handleOpenReplyDialogOrToggleLeftPane(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } @@ -463,7 +477,7 @@ this.set('changeViewState.showReplyDialog', true); e.preventDefault(); this._navToChangeView(); - }, + } _handleUpToChange(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -471,7 +485,7 @@ e.preventDefault(); this._navToChangeView(); - }, + } _handleCommaKey(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -480,7 +494,7 @@ e.preventDefault(); this.$.diffPreferencesDialog.open(); - }, + } _handleToggleDiffMode(e) { if (this.shouldSuppressKeyboardShortcut(e) || @@ -492,7 +506,7 @@ } else { this.$.modeSelect.setMode(DiffViewMode.SIDE_BY_SIDE); } - }, + } _navToChangeView() { if (!this._changeNum || !this._patchRange.patchNum) { return; } @@ -500,7 +514,7 @@ this._change, this._patchRange, this._change && this._change.revisions); - }, + } _navToFile(path, fileList, direction) { const newPath = this._getNavLinkPath(path, fileList, direction); @@ -516,7 +530,7 @@ Gerrit.Nav.navigateToDiff(this._change, newPath.path, this._patchRange.patchNum, this._patchRange.basePatchNum); - }, + } /** * @param {?string} path The path of the current file being shown. @@ -540,7 +554,7 @@ this._change && this._change.revisions); } return this._getDiffUrl(this._change, this._patchRange, newPath.path); - }, + } /** * Gives an object representing the target of navigating either left or @@ -566,8 +580,8 @@ let idx = fileList.indexOf(path); if (idx === -1) { const file = direction > 0 ? - fileList[0] : - fileList[fileList.length - 1]; + fileList[0] : + fileList[fileList.length - 1]; return {path: file}; } @@ -580,7 +594,7 @@ } return {path: fileList[idx]}; - }, + } _getReviewedFiles(changeNum, patchNum) { return this.$.restAPI.getReviewedFiles(changeNum, patchNum) @@ -588,13 +602,13 @@ this._reviewedFiles = new Set(files); return this._reviewedFiles; }); - }, + } _getReviewedStatus(editMode, changeNum, patchNum, path) { if (editMode) { return Promise.resolve(false); } return this._getReviewedFiles(changeNum, patchNum) .then(files => files.has(path)); - }, + } _paramsChanged(value) { if (value.view !== Gerrit.Nav.View.DIFF) { return; } @@ -681,7 +695,7 @@ // If diff view displayed has not ended yet, it ends here. this.$.reporting.diffViewDisplayed(); }); - }, + } _changeViewStateChanged(changeViewState) { if (changeViewState.diffMode === null) { @@ -690,7 +704,7 @@ this.set('changeViewState.diffMode', prefs.default_diff_view); }); } - }, + } _setReviewedObserver(_loggedIn, paramsRecord, _prefs) { // Polymer 2: check for undefined @@ -706,15 +720,15 @@ // is specified. this._getReviewedStatus(this.editMode, this._changeNum, this._patchRange.patchNum, this._path).then(status => { - this.$.reviewed.checked = status; - }); + this.$.reviewed.checked = status; + }); return; } if (params.view === Gerrit.Nav.View.DIFF) { this._setReviewed(true); } - }, + } /** * If the params specify a diff address then configure the diff cursor. @@ -727,14 +741,14 @@ this.$.cursor.side = DiffSides.RIGHT; } this.$.cursor.initialLineNumber = params.lineNum; - }, + } _getLineOfInterest(params) { // If there is a line number specified, pass it along to the diff so that // it will not get collapsed. if (!params.lineNum) { return null; } return {number: params.lineNum, leftSide: params.leftSide}; - }, + } _pathChanged(path) { if (path) { @@ -746,7 +760,7 @@ this.set('changeViewState.selectedFileIndex', this._fileList.indexOf(path)); - }, + } _getDiffUrl(change, patchRange, path) { if ([change, patchRange, path].some(arg => arg === undefined)) { @@ -754,7 +768,7 @@ } return Gerrit.Nav.getUrlForDiff(change, path, patchRange.patchNum, patchRange.basePatchNum); - }, + } _patchRangeStr(patchRange) { let patchStr = patchRange.patchNum; @@ -763,7 +777,7 @@ patchStr = patchRange.basePatchNum + '..' + patchRange.patchNum; } return patchStr; - }, + } /** * When the latest patch of the change is selected (and there is no base @@ -787,7 +801,7 @@ basePatchNum = patchRange.basePatchNum; } return {patchNum, basePatchNum}; - }, + } _getChangePath(change, patchRange, revisions) { if ([change, patchRange].some(arg => arg === undefined)) { @@ -796,16 +810,16 @@ const range = this._getChangeUrlRange(patchRange, revisions); return Gerrit.Nav.getUrlForChange(change, range.patchNum, range.basePatchNum); - }, + } _navigateToChange(change, patchRange, revisions) { const range = this._getChangeUrlRange(patchRange, revisions); Gerrit.Nav.navigateToChange(change, range.patchNum, range.basePatchNum); - }, + } _computeChangePath(change, patchRangeRecord, revisions) { return this._getChangePath(change, patchRangeRecord.base, revisions); - }, + } _formatFilesForDropdown(fileList, patchNum, changeComments) { // Polymer 2: check for undefined @@ -829,7 +843,7 @@ }); } return dropdownContent; - }, + } _computeCommentString(changeComments, patchNum, path) { const unresolvedCount = changeComments.computeUnresolvedNum(patchNum, @@ -845,11 +859,11 @@ (commentString && unresolvedString ? ', ' : '') + // Add parentheses around unresolved if it exists. (unresolvedString ? `${unresolvedString}` : ''); - }, + } _computePrefsButtonHidden(prefs, prefsDisabled) { return prefsDisabled || !prefs; - }, + } _handleFileChange(e) { // This is when it gets set initially. @@ -860,7 +874,7 @@ Gerrit.Nav.navigateToDiff(this._change, path, this._patchRange.patchNum, this._patchRange.basePatchNum); - }, + } _handleFileTap(e) { // async is needed so that that the click event is fired before the @@ -868,7 +882,7 @@ this.async(() => { this.$.dropdown.close(); }, 1); - }, + } _handlePatchChange(e) { const {basePatchNum, patchNum} = e.detail; @@ -876,12 +890,12 @@ this.patchNumEquals(patchNum, this._patchRange.patchNum)) { return; } Gerrit.Nav.navigateToDiff( this._change, this._path, patchNum, basePatchNum); - }, + } _handlePrefsTap(e) { e.preventDefault(); this.$.diffPreferencesDialog.open(); - }, + } /** * _getDiffViewMode: Get the diff view (side-by-side or unified) based on @@ -906,11 +920,11 @@ } else { return 'SIDE_BY_SIDE'; } - }, + } _computeModeSelectHideClass(isImageDiff) { return isImageDiff ? 'hide' : ''; - }, + } _onLineSelected(e, detail) { this.$.cursor.moveToLineNumber(detail.number, detail.side); @@ -922,7 +936,7 @@ this._change.project, this._path, this._patchRange.patchNum, this._patchRange.basePatchNum, number, leftSide); history.replaceState(null, '', url); - }, + } _computeDownloadDropdownLinks( project, changeNum, patchRange, path, diff) { @@ -961,7 +975,7 @@ } return links; - }, + } _computeDownloadFileLink( project, changeNum, patchRange, path, isBase) { @@ -981,13 +995,13 @@ } return url; - }, + } _computeDownloadPatchLink(project, changeNum, patchRange, path) { let url = this.changeBaseURL(project, changeNum, patchRange.patchNum); url += '/patch?zip&path=' + encodeURIComponent(path); return url; - }, + } _loadComments() { return this.$.commentAPI.loadAll(this._changeNum).then(comments => { @@ -997,20 +1011,20 @@ this._commentsForDiff = this._getCommentsForPath(this._path, this._patchRange, this._projectConfig); }); - }, + } _getPaths(patchRange) { return this._changeComments.getPaths(patchRange); - }, + } _getCommentsForPath(path, patchRange, projectConfig) { return this._changeComments.getCommentsBySideForPath(path, patchRange, projectConfig); - }, + } _getDiffDrafts() { return this.$.restAPI.getDiffDrafts(this._changeNum); - }, + } _computeCommentSkips(commentMap, fileList, path) { // Polymer 2: check for undefined @@ -1043,13 +1057,13 @@ } return skips; - }, + } _computeDiffClass(panelFloatingDisabled) { if (panelFloatingDisabled) { return 'noOverflow'; } - }, + } /** * @param {!Object} patchRangeRecord @@ -1057,19 +1071,19 @@ _computeEditMode(patchRangeRecord) { const patchRange = patchRangeRecord.base || {}; return this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME); - }, + } /** * @param {boolean} editMode */ _computeContainerClass(editMode) { return editMode ? 'editMode' : ''; - }, + } _computeBlameToggleLabel(loaded, loading) { if (loaded) { return 'Hide blame'; } return 'Show blame'; - }, + } /** * Load and display blame information if it has not already been loaded. @@ -1091,15 +1105,15 @@ .catch(() => { this._isBlameLoading = false; }); - }, + } - _computeBlameLoaderClass(isImageDiff) { - return !isImageDiff ? 'show' : ''; - }, + _computeBlameLoaderClass(isImageDiff, path) { + return !this.isMagicPath(path) && !isImageDiff ? 'show' : ''; + } _getRevisionInfo(change) { return new Gerrit.RevisionInfo(change); - }, + } _computeFileNum(file, files) { // Polymer 2: check for undefined @@ -1108,7 +1122,7 @@ } return files.findIndex(({value}) => value === file) + 1; - }, + } /** * @param {number} fileNum @@ -1120,16 +1134,16 @@ return 'show'; } return ''; - }, + } _handleExpandAllDiffContext(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } this.$.diffHost.expandAllContext(); - }, + } _computeDiffPrefsDisabled(disableDiffPrefs, loggedIn) { return disableDiffPrefs || !loggedIn; - }, + } _handleNextUnreviewedFile(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } @@ -1138,12 +1152,14 @@ // so we resolve the right "next" file. const unreviewedFiles = this._fileList .filter(file => - (file === this._path || !this._reviewedFiles.has(file))); + (file === this._path || !this._reviewedFiles.has(file))); this._navToFile(this._path, unreviewedFiles, 1); - }, + } _handleReloadingDiffPreference() { this._getDiffPreferences(); - }, - }); + } + } + + customElements.define(GrDiffView.is, GrDiffView); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html index 6427844..3a5c0bc 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff-view</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -269,28 +269,28 @@ assert.isTrue(element._loading); assert(diffNavStub.lastCall.calledWithExactly(element._change, 'wheatley.md', '10', '5'), - 'Should navigate to /c/42/5..10/wheatley.md'); + 'Should navigate to /c/42/5..10/wheatley.md'); element._path = 'wheatley.md'; MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); assert.isTrue(element._loading); assert(diffNavStub.lastCall.calledWithExactly(element._change, 'glados.txt', '10', '5'), - 'Should navigate to /c/42/5..10/glados.txt'); + 'Should navigate to /c/42/5..10/glados.txt'); element._path = 'glados.txt'; MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); assert.isTrue(element._loading); assert(diffNavStub.lastCall.calledWithExactly(element._change, 'chell.go', '10', '5'), - 'Should navigate to /c/42/5..10/chell.go'); + 'Should navigate to /c/42/5..10/chell.go'); element._path = 'chell.go'; MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); assert.isTrue(element._loading); assert(changeNavStub.lastCall.calledWithExactly(element._change, '10', '5'), - 'Should navigate to /c/42/5..10'); + 'Should navigate to /c/42/5..10'); }); test('keyboard shortcuts with old patch number', () => { @@ -332,13 +332,13 @@ MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']'); assert(diffNavStub.lastCall.calledWithExactly(element._change, 'wheatley.md', '1', PARENT), - 'Should navigate to /c/42/1/wheatley.md'); + 'Should navigate to /c/42/1/wheatley.md'); element._path = 'wheatley.md'; MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); assert(diffNavStub.lastCall.calledWithExactly(element._change, 'glados.txt', '1', PARENT), - 'Should navigate to /c/42/1/glados.txt'); + 'Should navigate to /c/42/1/glados.txt'); element._path = 'glados.txt'; MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js index 02ca7e5..46ea159 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
@@ -36,6 +36,7 @@ /** * True means all changes in this line are whitespace changes that should * not be highlighted as changed as per the user settings. + * * @type{boolean} */ this.ignoredWhitespaceOnly = false; @@ -79,7 +80,6 @@ DELTA: 'delta', }; - /** * Hides lines in the given range behind a context control group. * @@ -185,11 +185,11 @@ if (before.length) { beforeGroups.push(before.length === group.lines.length ? - group : group.cloneWithLines(before)); + group : group.cloneWithLines(before)); } if (after.length) { afterGroups.push(after.length === group.lines.length ? - group : group.cloneWithLines(after)); + group : group.cloneWithLines(after)); } } return [beforeGroups, afterGroups];
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html index 16e8036..686410c 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff-group</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/web-component-tester/browser.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js index b64385d..16d234b 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
@@ -37,7 +37,7 @@ /** @type {boolean} */ this.hasIntralineInfo = false; - /** @type Array<GrDiffLine.Highlights> */ + /** @type {!Array<GrDiffLine.Highlights>} */ this.highlights = []; /** @type {?Array<Object>} ?Array<!GrDiffGroup> */
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html index aa69248..92c2c37 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -273,6 +273,8 @@ } td.blame .sha { font-family: var(--monospace-font-family); + color: var(--link-color); + cursor: pointer; } .full-width td.blame { overflow: hidden; @@ -294,13 +296,16 @@ display: none; } .lineNum.COVERED { - background-color: #E0F2F1; + background-color: var(--coverage-covered, #e0f2f1); } .lineNum.NOT_COVERED { - background-color: #FFD1A4; + background-color: var(--coverage-not-covered, #ffd1a4); } .lineNum.PARTIALLY_COVERED { - background: linear-gradient(to right bottom, #FFD1A4 0%, #FFD1A4 50%, #E0F2F1 50%, #E0F2F1 100%); + background: linear-gradient(to right bottom, var(--coverage-not-covered, #ffd1a4) 0%, + var(--coverage-not-covered, #ffd1a4) 50%, + var(--coverage-covered, #e0f2f1) 50%, + var(--coverage-covered, #e0f2f1) 100%); } /** BEGIN: Select and copy for Polymer 2 */ @@ -344,8 +349,12 @@ text-align: center; } </style> - <style include="gr-syntax-theme"></style> - <style include="gr-ranged-comment-theme"></style> + <style include="gr-syntax-theme"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="gr-ranged-comment-theme"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <div id="diffHeader" hidden$="[[_computeDiffHeaderHidden(_diffHeaderItems)]]"> <template is="dom-repeat" @@ -366,11 +375,10 @@ coverage-ranges="[[coverageRanges]]" project-name="[[projectName]]" diff="[[diff]]" - diff-path="[[path]]" + path="[[path]]" change-num="[[changeNum]]" patch-num="[[patchRange.patchNum]]" view-mode="[[viewMode]]" - line-wrapping="[[lineWrapping]]" is-image-diff="[[isImageDiff]]" base-image="[[baseImage]]" layers="[[layers]]"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js index fc3862b..de23c37 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -91,11 +91,20 @@ const RENDER_DIFF_TABLE_DEBOUNCE_NAME = 'renderDiffTable'; - Polymer({ - is: 'gr-diff', - + /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.PatchSetMixin + */ + class GrDiff extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.PatchSetBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-diff'; } /** * Fired when the user selects a line. + * * @event line-selected */ @@ -118,173 +127,187 @@ * @event render */ - properties: { - changeNum: String, - noAutoRender: { - type: Boolean, - value: false, - }, - /** @type {?} */ - patchRange: Object, - path: { - type: String, - observer: '_pathObserver', - }, - prefs: { - type: Object, - observer: '_prefsObserver', - }, - projectName: String, - displayLine: { - type: Boolean, - value: false, - }, - isImageDiff: { - type: Boolean, - }, - commitRange: Object, - hidden: { - type: Boolean, - reflectToAttribute: true, - }, - noRenderOnPrefsChange: Boolean, - /** @type {!Array<!Gerrit.HoveredRange>} */ - _commentRanges: { - type: Array, - value: () => [], - }, - /** @type {!Array<!Gerrit.CoverageRange>} */ - coverageRanges: { - type: Array, - value: () => [], - }, - lineWrapping: { - type: Boolean, - value: false, - observer: '_lineWrappingObserver', - }, - viewMode: { - type: String, - value: DiffViewMode.SIDE_BY_SIDE, - observer: '_viewModeObserver', - }, + /** + * Fired for interaction reporting when a diff context is expanded. + * Contains an event.detail with numLines about the number of lines that + * were expanded. + * + * @event diff-context-expanded + */ - /** @type ?Gerrit.LineOfInterest */ - lineOfInterest: Object, + static get properties() { + return { + changeNum: String, + noAutoRender: { + type: Boolean, + value: false, + }, + /** @type {?} */ + patchRange: Object, + path: { + type: String, + observer: '_pathObserver', + }, + prefs: { + type: Object, + observer: '_prefsObserver', + }, + projectName: String, + displayLine: { + type: Boolean, + value: false, + }, + isImageDiff: { + type: Boolean, + }, + commitRange: Object, + hidden: { + type: Boolean, + reflectToAttribute: true, + }, + noRenderOnPrefsChange: Boolean, + /** @type {!Array<!Gerrit.HoveredRange>} */ + _commentRanges: { + type: Array, + value: () => [], + }, + /** @type {!Array<!Gerrit.CoverageRange>} */ + coverageRanges: { + type: Array, + value: () => [], + }, + lineWrapping: { + type: Boolean, + value: false, + observer: '_lineWrappingObserver', + }, + viewMode: { + type: String, + value: DiffViewMode.SIDE_BY_SIDE, + observer: '_viewModeObserver', + }, - loading: { - type: Boolean, - value: false, - observer: '_loadingChanged', - }, + /** @type {?Gerrit.LineOfInterest} */ + lineOfInterest: Object, - loggedIn: { - type: Boolean, - value: false, - }, - diff: { - type: Object, - observer: '_diffChanged', - }, - _diffHeaderItems: { - type: Array, - value: [], - computed: '_computeDiffHeaderItems(diff.*)', - }, - _diffTableClass: { - type: String, - value: '', - }, - /** @type {?Object} */ - baseImage: Object, - /** @type {?Object} */ - revisionImage: Object, + loading: { + type: Boolean, + value: false, + observer: '_loadingChanged', + }, - /** - * Whether the safety check for large diffs when whole-file is set has - * been bypassed. If the value is null, then the safety has not been - * bypassed. If the value is a number, then that number represents the - * context preference to use when rendering the bypassed diff. - * - * @type (number|null) - */ - _safetyBypass: { - type: Number, - value: null, - }, + loggedIn: { + type: Boolean, + value: false, + }, + diff: { + type: Object, + observer: '_diffChanged', + }, + _diffHeaderItems: { + type: Array, + value: [], + computed: '_computeDiffHeaderItems(diff.*)', + }, + _diffTableClass: { + type: String, + value: '', + }, + /** @type {?Object} */ + baseImage: Object, + /** @type {?Object} */ + revisionImage: Object, - _showWarning: Boolean, + /** + * Whether the safety check for large diffs when whole-file is set has + * been bypassed. If the value is null, then the safety has not been + * bypassed. If the value is a number, then that number represents the + * context preference to use when rendering the bypassed diff. + * + * @type {number|null} + */ + _safetyBypass: { + type: Number, + value: null, + }, - /** @type {?string} */ - errorMessage: { - type: String, - value: null, - }, + _showWarning: Boolean, - /** @type {?Object} */ - blame: { - type: Object, - value: null, - observer: '_blameChanged', - }, + /** @type {?string} */ + errorMessage: { + type: String, + value: null, + }, - parentIndex: Number, + /** @type {?Object} */ + blame: { + type: Object, + value: null, + observer: '_blameChanged', + }, - _newlineWarning: { - type: String, - computed: '_computeNewlineWarning(diff)', - }, + parentIndex: Number, - _diffLength: Number, + _newlineWarning: { + type: String, + computed: '_computeNewlineWarning(diff)', + }, - /** - * Observes comment nodes added or removed after the initial render. - * Can be used to unregister when the entire diff is (re-)rendered or upon - * detachment. - * @type {?PolymerDomApi.ObserveHandle} - */ - _incrementalNodeObserver: Object, + _diffLength: Number, - /** - * Observes comment nodes added or removed at any point. - * Can be used to unregister upon detachment. - * @type {?PolymerDomApi.ObserveHandle} - */ - _nodeObserver: Object, + /** + * Observes comment nodes added or removed after the initial render. + * Can be used to unregister when the entire diff is (re-)rendered or upon + * detachment. + * + * @type {?PolymerDomApi.ObserveHandle} + */ + _incrementalNodeObserver: Object, - /** Set by Polymer. */ - isAttached: Boolean, - layers: Array, - }, + /** + * Observes comment nodes added or removed at any point. + * Can be used to unregister upon detachment. + * + * @type {?PolymerDomApi.ObserveHandle} + */ + _nodeObserver: Object, - behaviors: [ - Gerrit.FireBehavior, - Gerrit.PatchSetBehavior, - ], + /** Set by Polymer. */ + isAttached: Boolean, + layers: Array, + }; + } - listeners: { - 'create-range-comment': '_handleCreateRangeComment', - 'render-content': '_handleRenderContent', - }, + static get observers() { + return [ + '_enableSelectionObserver(loggedIn, isAttached)', + ]; + } - observers: [ - '_enableSelectionObserver(loggedIn, isAttached)', - ], + created() { + super.created(); + this.addEventListener('create-range-comment', + e => this._handleCreateRangeComment(e)); + this.addEventListener('render-content', + () => this._handleRenderContent()); + } attached() { + super.attached(); this._observeNodes(); - }, + } detached() { + super.detached(); this._unobserveIncrementalNodes(); this._unobserveNodes(); - }, + } showNoChangeMessage(loading, prefs, diffLength) { return !loading && - prefs && prefs.ignore_whitespace !== 'IGNORE_NONE' - && diffLength === 0; - }, + prefs && prefs.ignore_whitespace !== 'IGNORE_NONE' && + diffLength === 0; + } _enableSelectionObserver(loggedIn, isAttached) { // Polymer 2: check for undefined @@ -299,7 +322,7 @@ this.unlisten(document, 'selectionchange', '_handleSelectionChange'); this.unlisten(document, 'mouseup', '_handleMouseUp'); } - }, + } _handleSelectionChange() { // Because of shadow DOM selections, we handle the selectionchange here, @@ -307,7 +330,7 @@ // corresponding range is determined and normalized. const selection = this._getShadowOrDocumentSelection(); this.$.highlights.handleSelectionChange(selection, false); - }, + } _handleMouseUp(e) { // To handle double-click outside of text creating comments, we check on @@ -315,7 +338,7 @@ // can't do that on selection change since the user may still be dragging. const selection = this._getShadowOrDocumentSelection(); this.$.highlights.handleSelectionChange(selection, true); - }, + } /** Gets the current selection, preferring the shadow DOM selection. */ _getShadowOrDocumentSelection() { @@ -324,9 +347,9 @@ // up the diff, because they are in the shadow DOM of the gr-diff element. // This takes the shadow DOM selection if one exists. return this.root.getSelection ? - this.root.getSelection() : - document.getSelection(); - }, + this.root.getSelection() : + document.getSelection(); + } _observeNodes() { this._nodeObserver = Polymer.dom(this).observeNodes(info => { @@ -335,7 +358,7 @@ this._updateRanges(addedThreadEls, removedThreadEls); this._redispatchHoverEvents(addedThreadEls); }); - }, + } _updateRanges(addedThreadEls, removedThreadEls) { function commentRangeFromThreadEl(threadEl) { @@ -361,7 +384,7 @@ if (addedCommentRanges && addedCommentRanges.length) { this.push('_commentRanges', ...addedCommentRanges); } - }, + } /** * The key locations based on the comments and line of interests, @@ -392,7 +415,7 @@ } } return keyLocations; - }, + } // Dispatch events that are handled by the gr-diff-highlight. _redispatchHoverEvents(addedThreadEls) { @@ -406,13 +429,13 @@ 'comment-thread-mouseleave', {bubbles: true, composed: true})); }); } - }, + } /** Cancel any remaining diff builder rendering work. */ cancel() { this.$.diffBuilder.cancel(); this.cancelDebouncer(RENDER_DIFF_TABLE_DEBOUNCE_NAME); - }, + } /** @return {!Array<!HTMLElement>} */ getCursorStops() { @@ -422,16 +445,16 @@ return Array.from( Polymer.dom(this.root).querySelectorAll('.diff-row')); - }, + } /** @return {boolean} */ isRangeSelected() { - return this.$.highlights.isRangeSelected(); - }, + return !!this.$.highlights.selectedRange; + } toggleLeftDiff() { this.toggleClass('no-left'); - }, + } _blameChanged(newValue) { this.$.diffBuilder.setBlame(newValue); @@ -440,7 +463,7 @@ } else { this.classList.remove('showBlame'); } - }, + } /** @return {string} */ _computeContainerClass(loggedIn, viewMode, displayLine) { @@ -465,12 +488,15 @@ classes.push('displayLine'); } return classes.join(' '); - }, + } _handleTap(e) { const el = Polymer.dom(e).localTarget; if (el.classList.contains('showContext')) { + this.fire('diff-context-expanded', { + numLines: e.detail.numLines, + }); this.$.diffBuilder.showContext(e.detail.groups, e.detail.section); } else if (el.classList.contains('lineNum')) { this.addDraftAtLine(el); @@ -480,7 +506,7 @@ const target = this.$.diffBuilder.getLineElByChild(el); if (target) { this._selectLine(target); } } - }, + } _selectLine(el) { this.fire('line-selected', { @@ -488,7 +514,7 @@ number: el.getAttribute('data-value'), path: this.path, }); - }, + } addDraftAtLine(el) { this._selectLine(el); @@ -504,18 +530,29 @@ } } this._createComment(el, lineNum); - }, + } + + createRangeComment() { + if (!this.isRangeSelected()) { + throw Error('Selection is needed for new range comment'); + } + const {side, range} = this.$.highlights.selectedRange; + this._createCommentForSelection(side, range); + } + + _createCommentForSelection(side, range) { + const lineNum = range.end_line; + const lineEl = this.$.diffBuilder.getLineElByNumber(lineNum, side); + if (this._isValidElForComment(lineEl)) { + this._createComment(lineEl, lineNum, side, range); + } + } _handleCreateRangeComment(e) { const range = e.detail.range; const side = e.detail.side; - const lineNum = range.end_line; - const lineEl = this.$.diffBuilder.getLineElByNumber(lineNum, side); - - if (this._isValidElForComment(lineEl)) { - this._createComment(lineEl, lineNum, side, range); - } - }, + this._createCommentForSelection(side, range); + } /** @return {boolean} */ _isValidElForComment(el) { @@ -524,8 +561,8 @@ return false; } const patchNum = el.classList.contains(DiffSide.LEFT) ? - this.patchRange.basePatchNum : - this.patchRange.patchNum; + this.patchRange.basePatchNum : + this.patchRange.patchNum; const isEdit = this.patchNumEquals(patchNum, this.EDIT_NAME); const isEditBase = this.patchNumEquals(patchNum, this.PARENT_NAME) && @@ -539,7 +576,7 @@ return false; } return true; - }, + } /** * @param {!Object} lineEl @@ -567,15 +604,16 @@ range, }, })); - }, + } _getThreadGroupForLine(contentEl) { return contentEl.querySelector('.thread-group'); - }, + } /** * Gets or creates a comment thread group for a specific line and side on a * diff. + * * @param {!Object} contentEl * @param {!Gerrit.DiffSide} commentSide * @return {!Node} @@ -590,7 +628,7 @@ contentEl.appendChild(threadGroupEl); } return threadGroupEl; - }, + } /** * The value to be used for the patch number of new comments created at the @@ -616,7 +654,7 @@ patchNum = this.patchRange.basePatchNum; } return patchNum; - }, + } /** @return {boolean} */ _getIsParentCommentByLineAndContent(lineEl, contentEl) { @@ -627,7 +665,7 @@ return true; } return false; - }, + } /** @return {string} */ _getCommentSideByLineAndContent(lineEl, contentEl) { @@ -637,7 +675,7 @@ side = 'left'; } return side; - }, + } _prefsObserver(newPrefs, oldPrefs) { // Scan the preference objects one level deep to see if they differ. @@ -653,16 +691,16 @@ if (differ) { this._prefsChanged(newPrefs); } - }, + } _pathObserver() { // Call _prefsChanged(), because line-limit style value depends on path. this._prefsChanged(this.prefs); - }, + } _viewModeObserver() { this._prefsChanged(this.prefs); - }, + } /** @param {boolean} newValue */ _loadingChanged(newValue) { @@ -673,11 +711,11 @@ this._showWarning = false; this.clearDiffContent(); } - }, + } _lineWrappingObserver() { this._prefsChanged(this.prefs); - }, + } _prefsChanged(prefs) { if (!prefs) { return; } @@ -708,14 +746,14 @@ if (this.diff && !this.noRenderOnPrefsChange) { this._debounceRenderDiffTable(); } - }, + } _diffChanged(newValue) { if (newValue) { this._diffLength = this.getDiffLength(newValue); this._debounceRenderDiffTable(); } - }, + } /** * When called multiple times from the same microtask, will call @@ -730,7 +768,7 @@ _debounceRenderDiffTable() { this.debounce( RENDER_DIFF_TABLE_DEBOUNCE_NAME, () => this._renderDiffTable()); - }, + } _renderDiffTable() { this._unobserveIncrementalNodes(); @@ -760,7 +798,7 @@ detail: {contentRendered: true}, })); }); - }, + } _handleRenderContent() { this._incrementalNodeObserver = Polymer.dom(this).observeNodes(info => { @@ -799,19 +837,19 @@ lastEl.replaceWith(lastEl); } }); - }, + } _unobserveIncrementalNodes() { if (this._incrementalNodeObserver) { Polymer.dom(this).unobserveNodes(this._incrementalNodeObserver); } - }, + } _unobserveNodes() { if (this._nodeObserver) { Polymer.dom(this).unobserveNodes(this._nodeObserver); } - }, + } /** * Get the preferences object including the safety bypass context (if any). @@ -821,12 +859,12 @@ return Object.assign({}, this.prefs, {context: this._safetyBypass}); } return this.prefs; - }, + } clearDiffContent() { this._unobserveIncrementalNodes(); this.$.diffTable.innerHTML = null; - }, + } /** @return {!Array} */ _computeDiffHeaderItems(diffInfoRecord) { @@ -839,27 +877,27 @@ item.startsWith('--- ') || item === 'Binary files differ'); }); - }, + } /** @return {boolean} */ _computeDiffHeaderHidden(items) { return items.length === 0; - }, + } _handleFullBypass() { this._safetyBypass = FULL_CONTEXT; this._debounceRenderDiffTable(); - }, + } _handleLimitedBypass() { this._safetyBypass = LIMITED_CONTEXT; this._debounceRenderDiffTable(); - }, + } /** @return {string} */ _computeWarningClass(showWarning) { return showWarning ? 'warn' : ''; - }, + } /** * @param {string} errorMessage @@ -867,14 +905,15 @@ */ _computeErrorClass(errorMessage) { return errorMessage ? 'showError' : ''; - }, + } expandAllContext() { this._handleFullBypass(); - }, + } /** * Find the last chunk for the given side. + * * @param {!Object} diff * @param {boolean} leftSide true if checking the base of the diff, * false if testing the revision. @@ -892,8 +931,8 @@ chunkIndex--; chunk = diff.content[chunkIndex]; } while ( - // We haven't reached the beginning. - chunkIndex >= 0 && + // We haven't reached the beginning. + chunkIndex >= 0 && // The chunk doesn't have both sides. !chunk.ab && @@ -907,10 +946,11 @@ if (chunkIndex === -1) { return null; } return chunk; - }, + } /** * Check whether the specified side of the diff has a trailing newline. + * * @param {!Object} diff * @param {boolean} leftSide true if checking the base of the diff, * false if testing the revision. @@ -928,7 +968,7 @@ lines = leftSide ? chunk.a : chunk.b; } return lines[lines.length - 1] === ''; - }, + } /** * @param {!Object} diff @@ -946,7 +986,7 @@ } if (!messages.length) { return null; } return messages.join(' — '); - }, + } /** * @param {string} warning @@ -956,11 +996,12 @@ _computeNewlineWarningClass(warning, loading) { if (loading || !warning) { return 'newlineWarning hidden'; } return 'newlineWarning'; - }, + } /** * Get the approximate length of the diff as the sum of the maximum * length of the chunks. + * * @param {Object} diff object * @return {number} */ @@ -975,6 +1016,8 @@ sec.hasOwnProperty('b') ? sec.b.length : 0); } }, 0); - }, - }); + } + } + + customElements.define(GrDiff.is, GrDiff); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html index 09342e1..2805eb4 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -76,7 +76,6 @@ }); }); - test('cancel', () => { element = fixture('basic'); const cancelStub = sandbox.stub(element.$.diffBuilder, 'cancel'); @@ -868,7 +867,6 @@ assert.isNull(element._lastChunkForSide(diff, true)); }); - test('deletion with b undefined', () => { const diff = {content: [ {a: ['foo', 'bar', 'baz']}, @@ -1070,7 +1068,7 @@ /* loading= */ false, element.prefs, element._diffLength - )); + )); }); test('do not show the message if still loading', () => { @@ -1079,7 +1077,7 @@ /* loading= */ true, element.prefs, element._diffLength - )); + )); }); test('do not show the message if contains valid changes', () => { @@ -1098,7 +1096,7 @@ /* loading= */ false, element.prefs, element._diffLength - )); + )); }); test('do not show message if ignore whitespace is disabled', () => { @@ -1116,7 +1114,7 @@ /* loading= */ false, element.prefs, element._diffLength - )); + )); }); }); @@ -1129,7 +1127,7 @@ element = fixture('basic'); element.prefs = {}; renderStub = sandbox.stub(element.$.diffBuilder, 'render') - .returns(Promise.resolve()); + .returns(Promise.resolve()); element.addEventListener('render', event => { assert.isTrue(event.detail.contentRendered); done();
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js index cdc5f30..88c3a0e 100644 --- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js +++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -21,6 +21,9 @@ const PATCH_DESC_MAX_LENGTH = 500; /** + * @appliesMixin Gerrit.PatchSetMixin + */ + /** * Fired when the patch range changes * * @event patch-range-change @@ -28,42 +31,47 @@ * @property {string} patchNum * @property {string} basePatchNum */ + class GrPatchRangeSelect extends Polymer.mixinBehaviors( [ + Gerrit.PatchSetBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-patch-range-select'; } - Polymer({ - is: 'gr-patch-range-select', - - properties: { - availablePatches: Array, - _baseDropdownContent: { - type: Object, - computed: '_computeBaseDropdownContent(availablePatches, patchNum,' + + static get properties() { + return { + availablePatches: Array, + _baseDropdownContent: { + type: Object, + computed: '_computeBaseDropdownContent(availablePatches, patchNum,' + '_sortedRevisions, changeComments, revisionInfo)', - }, - _patchDropdownContent: { - type: Object, - computed: '_computePatchDropdownContent(availablePatches,' + + }, + _patchDropdownContent: { + type: Object, + computed: '_computePatchDropdownContent(availablePatches,' + 'basePatchNum, _sortedRevisions, changeComments)', - }, - changeNum: String, - changeComments: Object, - /** @type {{ meta_a: !Array, meta_b: !Array}} */ - filesWeblinks: Object, - patchNum: String, - basePatchNum: String, - revisions: Object, - revisionInfo: Object, - _sortedRevisions: Array, - }, + }, + changeNum: String, + changeComments: Object, + /** @type {{ meta_a: !Array, meta_b: !Array}} */ + filesWeblinks: Object, + patchNum: String, + basePatchNum: String, + revisions: Object, + revisionInfo: Object, + _sortedRevisions: Array, + }; + } - observers: [ - '_updateSortedRevisions(revisions.*)', - ], - - behaviors: [Gerrit.PatchSetBehavior], + static get observers() { + return [ + '_updateSortedRevisions(revisions.*)', + ]; + } _getShaForPatch(patch) { return patch.sha.substring(0, 10); - }, + } _computeBaseDropdownContent(availablePatches, patchNum, _sortedRevisions, changeComments, revisionInfo) { @@ -80,7 +88,7 @@ const parentCounts = revisionInfo.getParentCountMap(); const currentParentCount = parentCounts.hasOwnProperty(patchNum) ? - parentCounts[patchNum] : 1; + parentCounts[patchNum] : 1; const maxParents = revisionInfo.getMaxParents(); const isMerge = currentParentCount > 1; @@ -111,13 +119,13 @@ } return dropdownContent; - }, + } _computeMobileText(patchNum, changeComments, revisions) { return `${patchNum}` + `${this._computePatchSetCommentsString(changeComments, patchNum)}` + `${this._computePatchSetDescription(revisions, patchNum, true)}`; - }, + } _computePatchDropdownContent(availablePatches, basePatchNum, _sortedRevisions, changeComments) { @@ -143,13 +151,13 @@ })); } return dropdownContent; - }, + } _computeText(patchNum, prefix, changeComments, sha) { return `${prefix}${patchNum}` + - `${this._computePatchSetCommentsString(changeComments, patchNum)}` - + (` | ${sha}`); - }, + `${this._computePatchSetCommentsString(changeComments, patchNum)}` + + (` | ${sha}`); + } _createDropdownEntry(patchNum, prefix, sortedRevisions, changeComments, sha) { @@ -167,17 +175,18 @@ entry['date'] = date; } return entry; - }, + } _updateSortedRevisions(revisionsRecord) { const revisions = revisionsRecord.base; this._sortedRevisions = this.sortRevisions(Object.values(revisions)); - }, + } /** * The basePatchNum should always be <= patchNum -- because sortedRevisions * is sorted in reverse order (higher patchset nums first), invalid base * patch nums have an index greater than the index of patchNum. + * * @param {number|string} basePatchNum The possible base patch num. * @param {number|string} patchNum The current selected patch num. * @param {!Array} sortedRevisions @@ -185,7 +194,7 @@ _computeLeftDisabled(basePatchNum, patchNum, sortedRevisions) { return this.findSortedIndex(basePatchNum, sortedRevisions) <= this.findSortedIndex(patchNum, sortedRevisions); - }, + } /** * The basePatchNum should always be <= patchNum -- because sortedRevisions @@ -214,8 +223,7 @@ return this.findSortedIndex(basePatchNum, sortedRevisions) <= this.findSortedIndex(patchNum, sortedRevisions); - }, - + } _computePatchSetCommentsString(changeComments, patchNum) { if (!changeComments) { return; } @@ -236,7 +244,7 @@ // Add a comma + space if both comments and unresolved (commentString && unresolvedString ? ', ' : '') + `${unresolvedString})`; - }, + } /** * @param {!Array} revisions @@ -246,9 +254,9 @@ _computePatchSetDescription(revisions, patchNum, opt_addFrontSpace) { const rev = this.getRevisionByPatchNum(revisions, patchNum); return (rev && rev.description) ? - (opt_addFrontSpace ? ' ' : '') + + (opt_addFrontSpace ? ' ' : '') + rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : ''; - }, + } /** * @param {!Array} revisions @@ -257,7 +265,7 @@ _computePatchSetDate(revisions, patchNum) { const rev = this.getRevisionByPatchNum(revisions, patchNum); return rev ? rev.created : undefined; - }, + } /** * Catches value-change events from the patchset dropdowns and determines @@ -275,6 +283,8 @@ this.dispatchEvent( new CustomEvent('patch-range-change', {detail, bubbles: false})); - }, - }); + } + } + + customElements.define(GrPatchRangeSelect.is, GrPatchRangeSelect); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html index 66fa974..81897cd 100644 --- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html +++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-patch-range-select</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -185,7 +185,7 @@ assert.deepEqual(element._computeBaseDropdownContent(availablePatches, patchNum, sortedRevisions, element.changeComments, element.revisionInfo), - expectedResult); + expectedResult); }); test('_computeBaseDropdownContent called when patchNum updates', () => { @@ -344,7 +344,7 @@ assert.deepEqual(element._computePatchDropdownContent(availablePatches, basePatchNum, sortedRevisions, element.changeComments), - expectedResult); + expectedResult); }); test('filesWeblinks', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js index c7c9b9d..8fc16f5 100644 --- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js +++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
@@ -23,9 +23,10 @@ const RANGE_HIGHLIGHT = 'style-scope gr-diff range'; const HOVER_HIGHLIGHT = 'style-scope gr-diff rangeHighlight'; - Polymer({ - is: 'gr-ranged-comment-layer', - + class GrRangedCommentLayer extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-ranged-comment-layer'; } /** * Fired when the range in a range comment was malformed and had to be * normalized. @@ -35,29 +36,34 @@ * @event normalize-range */ - properties: { + static get properties() { + return { /** @type {!Array<!Gerrit.HoveredRange>} */ - commentRanges: Array, - _listeners: { - type: Array, - value() { return []; }, - }, - _rangesMap: { - type: Object, - value() { return {left: {}, right: {}}; }, - }, - }, + commentRanges: Array, + _listeners: { + type: Array, + value() { return []; }, + }, + _rangesMap: { + type: Object, + value() { return {left: {}, right: {}}; }, + }, + }; + } - observers: [ - '_handleCommentRangesChange(commentRanges.*)', - ], + static get observers() { + return [ + '_handleCommentRangesChange(commentRanges.*)', + ]; + } get styleModuleName() { return 'gr-ranged-comment-styles'; - }, + } /** * Layer method to add annotations to a line. + * * @param {!HTMLElement} el The DIV.contentText element to apply the * annotation to. * @param {!HTMLElement} lineNumberEl @@ -66,12 +72,12 @@ annotate(el, lineNumberEl, line) { let ranges = []; if (line.type === GrDiffLine.Type.REMOVE || ( - line.type === GrDiffLine.Type.BOTH && + line.type === GrDiffLine.Type.BOTH && el.getAttribute('data-side') !== 'right')) { ranges = ranges.concat(this._getRangesForLine(line, 'left')); } if (line.type === GrDiffLine.Type.ADD || ( - line.type === GrDiffLine.Type.BOTH && + line.type === GrDiffLine.Type.BOTH && el.getAttribute('data-side') !== 'left')) { ranges = ranges.concat(this._getRangesForLine(line, 'right')); } @@ -81,20 +87,22 @@ range.end - range.start, range.hovering ? HOVER_HIGHLIGHT : RANGE_HIGHLIGHT); } - }, + } /** * Register a listener for layer updates. + * * @param {function(number, number, string)} fn The update handler function. * Should accept as arguments the line numbers for the start and end of * the update and the side as a string. */ addListener(fn) { this._listeners.push(fn); - }, + } /** * Notify Layer listeners of changes to annotations. + * * @param {number} start The line where the update starts. * @param {number} end The line where the update ends. * @param {string} side The side of the update. ('left' or 'right') @@ -103,11 +111,12 @@ for (const listener of this._listeners) { listener(start, end, side); } - }, + } /** * Handle change in the ranges by updating the ranges maps and by * emitting appropriate update notifications. + * * @param {Object} record The change record. */ _handleCommentRangesChange(record) { @@ -134,7 +143,7 @@ this._updateRangesMap( side, range, hovering, (forLine, start, end, hovering) => { const index = forLine.findIndex(lineRange => - lineRange.start === start && lineRange.end === end); + lineRange.start === start && lineRange.end === end); forLine[index].hovering = hovering; }); } @@ -147,7 +156,7 @@ this._updateRangesMap( side, range, hovering, (forLine, start, end) => { const index = forLine.findIndex(lineRange => - lineRange.start === start && lineRange.end === end); + lineRange.start === start && lineRange.end === end); forLine.splice(index, 1); }); } @@ -161,7 +170,7 @@ } } } - }, + } _updateRangesMap(side, range, hovering, operation) { const forSide = this._rangesMap[side] || (this._rangesMap[side] = {}); @@ -172,7 +181,7 @@ operation(forLine, start, end, hovering); } this._notifyUpdateRange(range.start_line, range.end_line, side); - }, + } _getRangesForLine(line, side) { const lineNum = side === 'left' ? line.beforeNumber : line.afterNumber; @@ -200,6 +209,8 @@ }) // Sort the ranges so that hovering highlights are on top. .sort((a, b) => a.hovering && !b.hovering ? 1 : 0); - }, - }); + } + } + + customElements.define(GrRangedCommentLayer.is, GrRangedCommentLayer); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html index 9d207a5..4ed744b 100644 --- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html +++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-ranged-comment-layer</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js index b1b3e0f..261cd4d 100644 --- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js +++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
@@ -17,48 +17,38 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-selection-action-box', - + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrSelectionActionBox extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-selection-action-box'; } /** - * Fired when the comment creation action was taken (hotkey, click). + * Fired when the comment creation action was taken (click). * - * @event create-range-comment + * @event create-comment-requested */ - properties: { - keyEventTarget: { - type: Object, - value() { return document.body; }, - }, - range: { - type: Object, - value: { - start_line: NaN, - start_character: NaN, - end_line: NaN, - end_character: NaN, + static get properties() { + return { + keyEventTarget: { + type: Object, + value() { return document.body; }, }, - }, - positionBelow: Boolean, - side: { - type: String, - value: '', - }, - }, + positionBelow: Boolean, + }; + } - behaviors: [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - ], + created() { + super.created(); - listeners: { - mousedown: '_handleMouseDown', // See https://crbug.com/gerrit/4767 - }, - - keyBindings: { - c: '_handleCKey', - }, + // See https://crbug.com/gerrit/4767 + this.addEventListener('mousedown', + e => this._handleMouseDown(e)); + } placeAbove(el) { Polymer.dom.flush(); @@ -69,7 +59,7 @@ rect.top - parentRect.top - boxRect.height - 6 + 'px'; this.style.left = rect.left - parentRect.left + (rect.width - boxRect.width) / 2 + 'px'; - }, + } placeBelow(el) { Polymer.dom.flush(); @@ -80,14 +70,14 @@ rect.top - parentRect.top + boxRect.height - 6 + 'px'; this.style.left = rect.left - parentRect.left + (rect.width - boxRect.width) / 2 + 'px'; - }, + } _getParentBoundingClientRect() { // With native shadow DOM, the parent is the shadow root, not the gr-diff // element const parent = this.parentElement || this.parentNode.host; return parent.getBoundingClientRect(); - }, + } _getTargetBoundingRect(el) { let rect; @@ -100,25 +90,15 @@ rect = el.getBoundingClientRect(); } return rect; - }, - - _handleCKey(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - - e.preventDefault(); - this._fireCreateComment(); - }, + } _handleMouseDown(e) { if (e.button !== 0) { return; } // 0 = main button e.preventDefault(); e.stopPropagation(); - this._fireCreateComment(); - }, + this.fire('create-comment-requested'); + } + } - _fireCreateComment() { - this.fire('create-range-comment', {side: this.side, range: this.range}); - }, - }); + customElements.define(GrSelectionActionBox.is, GrSelectionActionBox); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html index b950e7b..57adf32e 100644 --- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html +++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-selection-action-box</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -59,11 +59,6 @@ assert.isFalse(element.fire.called); }); - test('reacts to hotkey', () => { - MockInteractions.pressAndReleaseKeyOn(document.body, 67, null, 'c'); - assert.isTrue(element.fire.called); - }); - suite('mousedown reacts only to main button', () => { let e; @@ -73,40 +68,22 @@ preventDefault: sandbox.stub(), stopPropagation: sandbox.stub(), }; - sandbox.stub(element, '_fireCreateComment'); }); test('event handled if main button', () => { element._handleMouseDown(e); assert.isTrue(e.preventDefault.called); + assert(element.fire.calledWithExactly('create-comment-requested')); }); test('event ignored if not main button', () => { e.button = 1; element._handleMouseDown(e); assert.isFalse(e.preventDefault.called); + assert.isFalse(element.fire.called); }); }); - test('event fired contains playload', () => { - const side = 'left'; - const range = { - start_line: 1, - start_character: 11, - end_line: 2, - end_character: 42, - }; - element.side = 'left'; - element.range = range; - MockInteractions.pressAndReleaseKeyOn(document.body, 67, null, 'c'); - assert(element.fire.calledWithExactly( - 'create-range-comment', - { - side, - range, - })); - }); - suite('placeAbove', () => { let target;
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js index 3151a12..2e7325c 100644 --- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js +++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -131,58 +131,64 @@ const GO_BACKSLASH_LITERAL = '\'\\\\\''; const GLOBAL_LT_PATTERN = /</g; - Polymer({ - is: 'gr-syntax-layer', + class GrSyntaxLayer extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-syntax-layer'; } - properties: { - diff: { - type: Object, - observer: '_diffChanged', - }, - enabled: { - type: Boolean, - value: true, - }, - _baseRanges: { - type: Array, - value() { return []; }, - }, - _revisionRanges: { - type: Array, - value() { return []; }, - }, - _baseLanguage: String, - _revisionLanguage: String, - _listeners: { - type: Array, - value() { return []; }, - }, - /** @type {?number} */ - _processHandle: Number, - /** - * The promise last returned from `process()` while the asynchronous - * processing is running - `null` otherwise. Provides a `cancel()` - * method that rejects it with `{isCancelled: true}`. - * @type {?Object} - */ - _processPromise: { - type: Object, - value: null, - }, - _hljs: Object, - }, + static get properties() { + return { + diff: { + type: Object, + observer: '_diffChanged', + }, + enabled: { + type: Boolean, + value: true, + }, + _baseRanges: { + type: Array, + value() { return []; }, + }, + _revisionRanges: { + type: Array, + value() { return []; }, + }, + _baseLanguage: String, + _revisionLanguage: String, + _listeners: { + type: Array, + value() { return []; }, + }, + /** @type {?number} */ + _processHandle: Number, + /** + * The promise last returned from `process()` while the asynchronous + * processing is running - `null` otherwise. Provides a `cancel()` + * method that rejects it with `{isCancelled: true}`. + * + * @type {?Object} + */ + _processPromise: { + type: Object, + value: null, + }, + _hljs: Object, + }; + } addListener(fn) { this.push('_listeners', fn); - }, + } removeListener(fn) { this._listeners = this._listeners.filter(f => f != fn); - }, + } /** * Annotation layer method to add syntax annotations to the given element * for the given line. + * * @param {!HTMLElement} el * @param {!HTMLElement} lineNumberEl * @param {!Object} line (GrDiffLine) @@ -193,11 +199,11 @@ // Determine the side. let side; if (line.type === GrDiffLine.Type.REMOVE || ( - line.type === GrDiffLine.Type.BOTH && + line.type === GrDiffLine.Type.BOTH && el.getAttribute('data-side') !== 'right')) { side = 'left'; } else if (line.type === GrDiffLine.Type.ADD || ( - el.getAttribute('data-side') !== 'left')) { + el.getAttribute('data-side') !== 'left')) { side = 'right'; } @@ -215,18 +221,19 @@ GrAnnotation.annotateElement( el, range.start, range.length, range.className); } - }, + } _getLanguage(diffFileMetaInfo) { // The Gerrit API provides only content-type, but for other users of // gr-diff it may be more convenient to specify the language directly. return diffFileMetaInfo.language || LANGUAGE_MAP[diffFileMetaInfo.content_type]; - }, + } /** * Start processing syntax for the loaded diff and notify layer listeners * as syntax info comes online. + * * @return {Promise} */ process() { @@ -261,12 +268,14 @@ lastNotify: {left: 1, right: 1}, }; + const rangesCache = new Map(); + this._processPromise = util.makeCancelable(this._loadHLJS() .then(() => { return new Promise(resolve => { const nextStep = () => { this._processHandle = null; - this._processNextLine(state); + this._processNextLine(state, rangesCache); // Move to the next line in the section. state.lineIndex++; @@ -297,7 +306,7 @@ })); return this._processPromise .finally(() => { this._processPromise = null; }); - }, + } /** * Cancel any asynchronous syntax processing jobs. @@ -310,26 +319,36 @@ if (this._processPromise) { this._processPromise.cancel(); } - }, + } _diffChanged() { this._cancel(); this._baseRanges = []; this._revisionRanges = []; - }, + } /** * Take a string of HTML with the (potentially nested) syntax markers * Highlight.js emits and emit a list of text ranges and classes for the * markers. + * * @param {string} str The string of HTML. + * @param {Map<string, !Array<!Object>>} rangesCache A map for caching + * ranges for each string. A cache is read and written by this method. + * Since diff is mostly comparing same file on two sides, there is good rate + * of duplication at least for parts that are on left and right parts. * @return {!Array<!Object>} The list of ranges. */ - _rangesFromString(str) { + _rangesFromString(str, rangesCache) { + const cached = rangesCache.get(str); + if (cached) return cached; + const div = document.createElement('div'); div.innerHTML = str; - return this._rangesFromElement(div, 0); - }, + const ranges = this._rangesFromElement(div, 0); + rangesCache.set(str, ranges); + return ranges; + } _rangesFromElement(elem, offset) { let result = []; @@ -352,14 +371,15 @@ offset += nodeLength; } return result; - }, + } /** * For a given state, process the syntax for the next line (or pair of * lines). + * * @param {!Object} state The processing state for the layer. */ - _processNextLine(state) { + _processNextLine(state, rangesCache) { let baseLine; let revisionLine; @@ -388,7 +408,8 @@ baseLine = this._workaround(this._baseLanguage, baseLine); result = this._hljs.highlight(this._baseLanguage, baseLine, true, state.baseContext); - this.push('_baseRanges', this._rangesFromString(result.value)); + this.push('_baseRanges', + this._rangesFromString(result.value, rangesCache)); state.baseContext = result.top; } @@ -397,10 +418,11 @@ revisionLine = this._workaround(this._revisionLanguage, revisionLine); result = this._hljs.highlight(this._revisionLanguage, revisionLine, true, state.revisionContext); - this.push('_revisionRanges', this._rangesFromString(result.value)); + this.push('_revisionRanges', + this._rangesFromString(result.value, rangesCache)); state.revisionContext = result.top; } - }, + } /** * Ad hoc fixes for HLJS parsing bugs. Rewrite lines of code in constrained @@ -467,10 +489,11 @@ } return line; - }, + } /** * Tells whether the state has exhausted its current section. + * * @param {!Object} state * @return {boolean} */ @@ -482,11 +505,12 @@ return (!section.a || state.lineIndex >= section.a.length) && (!section.b || state.lineIndex >= section.b.length); } - }, + } /** * For a given state, notify layer listeners of any processed line ranges * that have not yet been notified. + * * @param {!Object} state */ _notify(state) { @@ -504,18 +528,20 @@ 'right'); state.lastNotify.right = state.lineNums.right; } - }, + } _notifyRange(start, end, side) { for (const fn of this._listeners) { fn(start, end, side); } - }, + } _loadHLJS() { return this.$.libLoader.getHLJS().then(hljs => { this._hljs = hljs; }); - }, - }); + } + } + + customElements.define(GrSyntaxLayer.is, GrSyntaxLayer); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html index 472db21..926a6bc 100644 --- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html +++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-syntax-layer</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -388,10 +388,20 @@ '<span class="non-whtelisted-class">', '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>', '</span>'].join(''); - const result = element._rangesFromString(str); + const result = element._rangesFromString(str, new Map()); assert.notEqual(result.length, 0); }); + test('_rangesFromString cache same syntax markers', () => { + sandbox.spy(element, '_rangesFromElement'); + const str = + '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>'; + const cacheMap = new Map(); + element._rangesFromString(str, cacheMap); + element._rangesFromString(str, cacheMap); + assert.isTrue(element._rangesFromElement.calledOnce); + }); + test('_isSectionDone', () => { let state = {sectionIndex: 0, lineIndex: 0}; assert.isFalse(element._isSectionDone(state));
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.html b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.html index 5072b9d..5ae679e 100644 --- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.html +++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.html
@@ -25,8 +25,12 @@ <dom-module id="gr-documentation-search"> <template> - <style include="shared-styles"></style> - <style include="gr-table-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="gr-table-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <gr-list-view filter="[[_filter]]" items=false
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js index f850b9d..f317459 100644 --- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js +++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
@@ -17,50 +17,56 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-documentation-search', + /** + * @appliesMixin Gerrit.ListViewMixin + */ + class GrDocumentationSearch extends Polymer.mixinBehaviors( [ + Gerrit.ListViewBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-documentation-search'; } - properties: { + static get properties() { + return { /** * URL params passed from the router. */ - params: { - type: Object, - observer: '_paramsChanged', - }, + params: { + type: Object, + observer: '_paramsChanged', + }, - _path: { - type: String, - readOnly: true, - value: '/Documentation', - }, - _documentationSearches: Array, + _path: { + type: String, + readOnly: true, + value: '/Documentation', + }, + _documentationSearches: Array, - _loading: { - type: Boolean, - value: true, - }, - _filter: { - type: String, - value: '', - }, - }, - - behaviors: [ - Gerrit.ListViewBehavior, - ], + _loading: { + type: Boolean, + value: true, + }, + _filter: { + type: String, + value: '', + }, + }; + } attached() { + super.attached(); this.dispatchEvent( new CustomEvent('title-change', {title: 'Documentation Search'})); - }, + } _paramsChanged(params) { this._loading = true; this._filter = this.getFilterValue(params); return this._getDocumentationSearches(this._filter); - }, + } _getDocumentationSearches(filter) { this._documentationSearches = []; @@ -71,11 +77,13 @@ this._documentationSearches = searches; this._loading = false; }); - }, + } _computeSearchUrl(url) { if (!url) { return ''; } return this.getBaseUrl() + '/' + url; - }, - }); + } + } + + customElements.define(GrDocumentationSearch.is, GrDocumentationSearch); })();
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html index 84298e2..688a9e6 100644 --- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html +++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-documentation-search</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/page/page.js"></script>
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js index ed96bb2..ce9db61 100644 --- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js +++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
@@ -17,23 +17,28 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-default-editor', - + class GrDefaultEditor extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-default-editor'; } /** * Fired when the content of the editor changes. * * @event content-change */ - properties: { - fileContent: String, - }, + static get properties() { + return { + fileContent: String, + }; + } _handleTextareaInput(e) { this.dispatchEvent(new CustomEvent( 'content-change', {detail: {value: e.target.value}, bubbles: true, composed: true})); - }, - }); + } + } + + customElements.define(GrDefaultEditor.is, GrDefaultEditor); })();
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html index c986e7c..4635b1c 100644 --- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html +++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html
@@ -17,7 +17,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-default-editor</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-constants.html b/polygerrit-ui/app/elements/edit/gr-edit-constants.html index d526ccd..5895124 100644 --- a/polygerrit-ui/app/elements/edit/gr-edit-constants.html +++ b/polygerrit-ui/app/elements/edit/gr-edit-constants.html
@@ -22,7 +22,7 @@ // Order corresponds to order in the UI. GrEditConstants.Actions = { - OPEN: {label: 'Open', id: 'open'}, + OPEN: {label: 'Add/Open', id: 'open'}, DELETE: {label: 'Delete', id: 'delete'}, RENAME: {label: 'Rename', id: 'rename'}, RESTORE: {label: 'Restore', id: 'restore'},
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html index 52692a7..cb950da 100644 --- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html +++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html
@@ -55,21 +55,13 @@ gr-dialog .main > iron-input{ width: 100%; } - gr-autocomplete { - --gr-autocomplete: { - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - height: 2em; - padding: 0 var(--spacing-xs); - } - } input { border: 1px solid var(--border-color); border-radius: var(--border-radius); - height: 2em; margin: var(--spacing-m) 0; - padding: 0 var(--spacing-xs); + padding: var(--spacing-s); width: 100%; + box-sizing: content-box; } @media screen and (max-width: 50em) { gr-dialog { @@ -89,12 +81,12 @@ id="openDialog" class="invisible dialog" disabled$="[[!_isValidPath(_path)]]" - confirm-label="Open" + confirm-label="Confirm" confirm-on-enter on-confirm="_handleOpenConfirm" on-cancel="_handleDialogCancel"> <div class="header" slot="header"> - Open an existing or new file + Add a new file or open an existing file </div> <div class="main" slot="main"> <gr-autocomplete
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js index 5aed3e4..052c51c 100644 --- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js +++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
@@ -17,47 +17,52 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-edit-controls', + /** + * @appliesMixin Gerrit.PatchSetMixin + */ + class GrEditControls extends Polymer.mixinBehaviors( [ + Gerrit.PatchSetBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-edit-controls'; } - properties: { - change: Object, - patchNum: String, + static get properties() { + return { + change: Object, + patchNum: String, - /** - * TODO(kaspern): by default, the RESTORE action should be hidden in the - * file-list as it is a per-file action only. Remove this default value - * when the Actions dictionary is moved to a shared constants file and - * use the hiddenActions property in the parent component. - */ - hiddenActions: { - type: Array, - value() { return [GrEditConstants.Actions.RESTORE.id]; }, - }, - - _actions: { - type: Array, - value() { return Object.values(GrEditConstants.Actions); }, - }, - _path: { - type: String, - value: '', - }, - _newPath: { - type: String, - value: '', - }, - _query: { - type: Function, - value() { - return this._queryFiles.bind(this); + /** + * TODO(kaspern): by default, the RESTORE action should be hidden in the + * file-list as it is a per-file action only. Remove this default value + * when the Actions dictionary is moved to a shared constants file and + * use the hiddenActions property in the parent component. + */ + hiddenActions: { + type: Array, + value() { return [GrEditConstants.Actions.RESTORE.id]; }, }, - }, - }, - behaviors: [ - Gerrit.PatchSetBehavior, - ], + _actions: { + type: Array, + value() { return Object.values(GrEditConstants.Actions); }, + }, + _path: { + type: String, + value: '', + }, + _newPath: { + type: String, + value: '', + }, + _query: { + type: Function, + value() { + return this._queryFiles.bind(this); + }, + }, + }; + } _handleTap(e) { e.preventDefault(); @@ -76,7 +81,7 @@ this.openRestoreDialog(); return; } - }, + } /** * @param {string=} opt_path @@ -84,7 +89,7 @@ openOpenDialog(opt_path) { if (opt_path) { this._path = opt_path; } return this._showDialog(this.$.openDialog); - }, + } /** * @param {string=} opt_path @@ -92,7 +97,7 @@ openDeleteDialog(opt_path) { if (opt_path) { this._path = opt_path; } return this._showDialog(this.$.deleteDialog); - }, + } /** * @param {string=} opt_path @@ -100,7 +105,7 @@ openRenameDialog(opt_path) { if (opt_path) { this._path = opt_path; } return this._showDialog(this.$.renameDialog); - }, + } /** * @param {string=} opt_path @@ -108,24 +113,26 @@ openRestoreDialog(opt_path) { if (opt_path) { this._path = opt_path; } return this._showDialog(this.$.restoreDialog); - }, + } /** * Given a path string, checks that it is a valid file path. + * * @param {string} path * @return {boolean} */ _isValidPath(path) { // Double negation needed for strict boolean return type. return !!path.length && !path.endsWith('/'); - }, + } _computeRenameDisabled(path, newPath) { return this._isValidPath(path) && this._isValidPath(newPath); - }, + } /** * Given a dom event, gets the dialog that lies along this event path. + * * @param {!Event} e * @return {!Element|undefined} */ @@ -134,7 +141,7 @@ if (!element.classList) { return false; } return element.classList.contains('dialog'); }); - }, + } _showDialog(dialog) { // Some dialogs may not fire their on-close event when closed in certain @@ -148,12 +155,12 @@ if (autocomplete) { autocomplete.focus(); } this.async(() => { this.$.overlay.center(); }, 1); }); - }, + } _hideAllDialogs() { const dialogs = Polymer.dom(this.root).querySelectorAll('.dialog'); for (const dialog of dialogs) { this._closeDialog(dialog); } - }, + } /** * @param {Element|undefined} dialog @@ -175,18 +182,18 @@ dialog.classList.toggle('invisible', true); return this.$.overlay.close(); - }, + } _handleDialogCancel(e) { this._closeDialog(this._getDialogFromEvent(e)); - }, + } _handleOpenConfirm(e) { const url = Gerrit.Nav.getEditUrlForDiff(this.change, this._path, this.patchNum); Gerrit.Nav.navigateToRelativeUrl(url); this._closeDialog(this._getDialogFromEvent(e), true); - }, + } _handleDeleteConfirm(e) { // Get the dialog before the api call as the event will change during bubbling @@ -198,7 +205,7 @@ this._closeDialog(dialog, true); Gerrit.Nav.navigateToChange(this.change); }); - }, + } _handleRestoreConfirm(e) { const dialog = this._getDialogFromEvent(e); @@ -208,27 +215,29 @@ this._closeDialog(dialog, true); Gerrit.Nav.navigateToChange(this.change); }); - }, + } _handleRenameConfirm(e) { const dialog = this._getDialogFromEvent(e); return this.$.restAPI.renameFileInChangeEdit(this.change._number, this._path, this._newPath).then(res => { - if (!res.ok) { return; } - this._closeDialog(dialog, true); - Gerrit.Nav.navigateToChange(this.change); - }); - }, + if (!res.ok) { return; } + this._closeDialog(dialog, true); + Gerrit.Nav.navigateToChange(this.change); + }); + } _queryFiles(input) { return this.$.restAPI.queryChangeFiles(this.change._number, this.patchNum, input).then(res => res.map(file => { - return {name: file}; - })); - }, + return {name: file}; + })); + } _computeIsInvisible(id, hiddenActions) { return hiddenActions.includes(id) ? 'invisible' : ''; - }, - }); + } + } + + customElements.define(GrEditControls.is, GrEditControls); })();
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html index df029ed..b7e52a1 100644 --- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html +++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
@@ -17,7 +17,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-edit-controls</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -82,7 +82,7 @@ }); test('open', () => { - MockInteractions.tap(element.$$('#open')); + MockInteractions.tap(element.shadowRoot.querySelector('#open')); element.patchNum = 1; return showDialogSpy.lastCall.returnValue.then(() => { assert.isTrue(element._hideAllDialogs.called); @@ -101,7 +101,7 @@ }); test('cancel', () => { - MockInteractions.tap(element.$$('#open')); + MockInteractions.tap(element.shadowRoot.querySelector('#open')); return showDialogSpy.lastCall.returnValue.then(() => { assert.isTrue(element.$.openDialog.disabled); openAutoCcmplete.noDebounce = true; @@ -129,7 +129,7 @@ test('delete', () => { deleteStub.returns(Promise.resolve({ok: true})); - MockInteractions.tap(element.$$('#delete')); + MockInteractions.tap(element.shadowRoot.querySelector('#delete')); return showDialogSpy.lastCall.returnValue.then(() => { assert.isTrue(element.$.deleteDialog.disabled); assert.isFalse(queryStub.called); @@ -152,7 +152,7 @@ test('delete fails', () => { deleteStub.returns(Promise.resolve({ok: false})); - MockInteractions.tap(element.$$('#delete')); + MockInteractions.tap(element.shadowRoot.querySelector('#delete')); return showDialogSpy.lastCall.returnValue.then(() => { assert.isTrue(element.$.deleteDialog.disabled); assert.isFalse(queryStub.called); @@ -173,7 +173,7 @@ }); test('cancel', () => { - MockInteractions.tap(element.$$('#delete')); + MockInteractions.tap(element.shadowRoot.querySelector('#delete')); return showDialogSpy.lastCall.returnValue.then(() => { assert.isTrue(element.$.deleteDialog.disabled); element.$.deleteDialog.querySelector('gr-autocomplete').text = @@ -192,8 +192,8 @@ let renameStub; let renameAutocomplete; const inputSelector = Polymer.Element ? - '.newPathIronInput' : - '.newPathInput'; + '.newPathIronInput' : + '.newPathInput'; setup(() => { navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); @@ -204,7 +204,7 @@ test('rename', () => { renameStub.returns(Promise.resolve({ok: true})); - MockInteractions.tap(element.$$('#rename')); + MockInteractions.tap(element.shadowRoot.querySelector('#rename')); return showDialogSpy.lastCall.returnValue.then(() => { assert.isTrue(element.$.renameDialog.disabled); assert.isFalse(queryStub.called); @@ -232,7 +232,7 @@ test('rename fails', () => { renameStub.returns(Promise.resolve({ok: false})); - MockInteractions.tap(element.$$('#rename')); + MockInteractions.tap(element.shadowRoot.querySelector('#rename')); return showDialogSpy.lastCall.returnValue.then(() => { assert.isTrue(element.$.renameDialog.disabled); assert.isFalse(queryStub.called); @@ -258,7 +258,7 @@ }); test('cancel', () => { - MockInteractions.tap(element.$$('#rename')); + MockInteractions.tap(element.shadowRoot.querySelector('#rename')); return showDialogSpy.lastCall.returnValue.then(() => { assert.isTrue(element.$.renameDialog.disabled); element.$.renameDialog.querySelector('gr-autocomplete').text = @@ -285,13 +285,14 @@ }); test('restore hidden by default', () => { - assert.isTrue(element.$$('#restore').classList.contains('invisible')); + assert.isTrue(element.shadowRoot + .querySelector('#restore').classList.contains('invisible')); }); test('restore', () => { restoreStub.returns(Promise.resolve({ok: true})); element._path = 'src/test.cpp'; - MockInteractions.tap(element.$$('#restore')); + MockInteractions.tap(element.shadowRoot.querySelector('#restore')); return showDialogSpy.lastCall.returnValue.then(() => { MockInteractions.tap(element.$.restoreDialog.$$('gr-button[primary]')); flushAsynchronousOperations(); @@ -309,7 +310,7 @@ test('restore fails', () => { restoreStub.returns(Promise.resolve({ok: false})); element._path = 'src/test.cpp'; - MockInteractions.tap(element.$$('#restore')); + MockInteractions.tap(element.shadowRoot.querySelector('#restore')); return showDialogSpy.lastCall.returnValue.then(() => { MockInteractions.tap(element.$.restoreDialog.$$('gr-button[primary]')); flushAsynchronousOperations(); @@ -325,7 +326,7 @@ test('cancel', () => { element._path = 'src/test.cpp'; - MockInteractions.tap(element.$$('#restore')); + MockInteractions.tap(element.shadowRoot.querySelector('#restore')); return showDialogSpy.lastCall.returnValue.then(() => { MockInteractions.tap(element.$.restoreDialog.$$('gr-button')); assert.isFalse(navStub.called);
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js index 250816b..ddd2d29 100644 --- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js +++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
@@ -17,38 +17,41 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-edit-file-controls', - + class GrEditFileControls extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-edit-file-controls'; } /** * Fired when an action in the overflow menu is tapped. * * @event file-action-tap */ - properties: { - filePath: String, - _allFileActions: { - type: Array, - value: () => Object.values(GrEditConstants.Actions), - }, - _fileActions: { - type: Array, - computed: '_computeFileActions(_allFileActions)', - }, - }, + static get properties() { + return { + filePath: String, + _allFileActions: { + type: Array, + value: () => Object.values(GrEditConstants.Actions), + }, + _fileActions: { + type: Array, + computed: '_computeFileActions(_allFileActions)', + }, + }; + } _handleActionTap(e) { e.preventDefault(); e.stopPropagation(); this._dispatchFileAction(e.detail.id, this.filePath); - }, + } _dispatchFileAction(action, path) { this.dispatchEvent(new CustomEvent( 'file-action-tap', {detail: {action, path}, bubbles: true, composed: true})); - }, + } _computeFileActions(actions) { // TODO(kaspern): conditionally disable some actions based on file status. @@ -58,6 +61,8 @@ id: action.id, }; }); - }, - }); + } + } + + customElements.define(GrEditFileControls.is, GrEditFileControls); })();
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html index 7979e57..dc26468 100644 --- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html +++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
@@ -17,7 +17,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-edit-file-controls</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js index 95cfaf6..aaafa46 100644 --- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js +++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
@@ -24,9 +24,21 @@ const STORAGE_DEBOUNCE_INTERVAL_MS = 100; - Polymer({ - is: 'gr-editor-view', - + /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.PathListMixin + */ + class GrEditorView extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + Gerrit.PatchSetBehavior, + Gerrit.PathListBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-editor-view'; } /** * Fired when the title of the page should change. * @@ -39,69 +51,69 @@ * @event show-alert */ - properties: { + static get properties() { + return { /** * URL params passed from the router. */ - params: { - type: Object, - observer: '_paramsChanged', - }, + params: { + type: Object, + observer: '_paramsChanged', + }, - _change: Object, - _changeEditDetail: Object, - _changeNum: String, - _patchNum: String, - _path: String, - _type: String, - _content: String, - _newContent: String, - _saving: { - type: Boolean, - value: false, - }, - _successfulSave: { - type: Boolean, - value: false, - }, - _saveDisabled: { - type: Boolean, - value: true, - computed: '_computeSaveDisabled(_content, _newContent, _saving)', - }, - _prefs: Object, - }, + _change: Object, + _changeEditDetail: Object, + _changeNum: String, + _patchNum: String, + _path: String, + _type: String, + _content: String, + _newContent: String, + _saving: { + type: Boolean, + value: false, + }, + _successfulSave: { + type: Boolean, + value: false, + }, + _saveDisabled: { + type: Boolean, + value: true, + computed: '_computeSaveDisabled(_content, _newContent, _saving)', + }, + _prefs: Object, + }; + } - behaviors: [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - Gerrit.PatchSetBehavior, - Gerrit.PathListBehavior, - ], + get keyBindings() { + return { + 'ctrl+s meta+s': '_handleSaveShortcut', + }; + } - listeners: { - 'content-change': '_handleContentChange', - }, - - keyBindings: { - 'ctrl+s meta+s': '_handleSaveShortcut', - }, + created() { + super.created(); + this.addEventListener('content-change', + e => this._handleContentChange(e)); + } attached() { + super.attached(); this._getEditPrefs().then(prefs => { this._prefs = prefs; }); - }, + } get storageKey() { return `c${this._changeNum}_ps${this._patchNum}_${this._path}`; - }, + } _getLoggedIn() { return this.$.restAPI.getLoggedIn(); - }, + } _getEditPrefs() { return this.$.restAPI.getEditPreferences(); - }, + } _paramsChanged(value) { if (value.view !== Gerrit.Nav.View.EDIT) { @@ -126,13 +138,13 @@ promises.push( this._getFileData(this._changeNum, this._path, this._patchNum)); return Promise.all(promises); - }, + } _getChangeDetail(changeNum) { return this.$.restAPI.getDiffChangeDetail(changeNum).then(change => { this._change = change; }); - }, + } _handlePathChanged(e) { const path = e.detail; @@ -141,18 +153,18 @@ } return this.$.restAPI.renameFileInChangeEdit(this._changeNum, this._path, path).then(res => { - if (!res.ok) { return; } + if (!res.ok) { return; } - this._successfulSave = true; - this._viewEditInChangeView(); - }); - }, + this._successfulSave = true; + this._viewEditInChangeView(); + }); + } _viewEditInChangeView() { const patch = this._successfulSave ? this.EDIT_NAME : this._patchNum; Gerrit.Nav.navigateToChange(this._change, patch, null, patch !== this.EDIT_NAME); - }, + } _getFileData(changeNum, path, patchNum) { const storedContent = @@ -183,7 +195,7 @@ this._type = ''; } }); - }, + } _saveEdit() { this._saving = true; @@ -191,14 +203,14 @@ this.$.storage.eraseEditableContentItem(this.storageKey); return this.$.restAPI.saveChangeEdit(this._changeNum, this._path, this._newContent).then(res => { - this._saving = false; - this._showAlert(res.ok ? SAVED_MESSAGE : SAVE_FAILED_MSG); - if (!res.ok) { return; } + this._saving = false; + this._showAlert(res.ok ? SAVED_MESSAGE : SAVE_FAILED_MSG); + if (!res.ok) { return; } - this._content = this._newContent; - this._successfulSave = true; - }); - }, + this._content = this._newContent; + this._successfulSave = true; + }); + } _showAlert(message) { this.dispatchEvent(new CustomEvent('show-alert', { @@ -206,7 +218,7 @@ bubbles: true, composed: true, })); - }, + } _computeSaveDisabled(content, newContent, saving) { // Polymer 2: check for undefined @@ -222,12 +234,12 @@ return true; } return content === newContent; - }, + } _handleCloseTap() { // TODO(kaspern): Add a confirm dialog if there are unsaved changes. this._viewEditInChangeView(); - }, + } _handleContentChange(e) { this.debounce('store', () => { @@ -239,13 +251,15 @@ this.$.storage.eraseEditableContentItem(this.storageKey); } }, STORAGE_DEBOUNCE_INTERVAL_MS); - }, + } _handleSaveShortcut(e) { e.preventDefault(); if (!this._saveDisabled) { this._saveEdit(); } - }, - }); + } + } + + customElements.define(GrEditorView.is, GrEditorView); })();
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html index abb8131..ebd624e 100644 --- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html +++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
@@ -17,7 +17,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-editor-view</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -106,7 +106,7 @@ // Calling with the same path should not navigate. return element._handlePathChanged({detail: mockParams.path}).then(() => { assert.isFalse(savePathStub.called); - // !ok response + // !ok response element._handlePathChanged({detail: 'newPath'}).then(() => { assert.isTrue(savePathStub.called); assert.isFalse(navigateStub.called);
diff --git a/polygerrit-ui/app/elements/gr-app-element.html b/polygerrit-ui/app/elements/gr-app-element.html index 046e5ff..f758280 100644 --- a/polygerrit-ui/app/elements/gr-app-element.html +++ b/polygerrit-ui/app/elements/gr-app-element.html
@@ -211,7 +211,6 @@ </footer> <gr-overlay id="keyboardShortcuts" with-backdrop> <gr-keyboard-shortcuts-dialog - view="[[params.view]]" on-close="_handleKeyboardShortcutDialogClose"></gr-keyboard-shortcuts-dialog> </gr-overlay> <gr-overlay id="registrationOverlay" with-backdrop>
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js index d5d944a..14570f2 100644 --- a/polygerrit-ui/app/elements/gr-app-element.js +++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -17,90 +17,91 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-app-element', - + /** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + */ + class GrAppElement extends Polymer.mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.KeyboardShortcutBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-app-element'; } /** * Fired when the URL location changes. * * @event location-change */ - properties: { + static get properties() { + return { /** * @type {{ query: string, view: string, screen: string }} */ - params: Object, - keyEventTarget: { - type: Object, - value() { return document.body; }, - }, + params: Object, + keyEventTarget: { + type: Object, + value() { return document.body; }, + }, - _account: { - type: Object, - observer: '_accountChanged', - }, + _account: { + type: Object, + observer: '_accountChanged', + }, - /** - * The last time the g key was pressed in milliseconds (or a keydown event - * was handled if the key is held down). - * @type {number|null} - */ - _lastGKeyPressTimestamp: { - type: Number, - value: null, - }, + /** + * The last time the g key was pressed in milliseconds (or a keydown event + * was handled if the key is held down). + * + * @type {number|null} + */ + _lastGKeyPressTimestamp: { + type: Number, + value: null, + }, - /** - * @type {{ plugin: Object }} - */ - _serverConfig: Object, - _version: String, - _showChangeListView: Boolean, - _showDashboardView: Boolean, - _showChangeView: Boolean, - _showDiffView: Boolean, - _showSettingsView: Boolean, - _showAdminView: Boolean, - _showCLAView: Boolean, - _showEditorView: Boolean, - _showPluginScreen: Boolean, - _showDocumentationSearch: Boolean, - /** @type {?} */ - _viewState: Object, - /** @type {?} */ - _lastError: Object, - _lastSearchPage: String, - _path: String, - _pluginScreenName: { - type: String, - computed: '_computePluginScreenName(params)', - }, - _settingsUrl: String, - _feedbackUrl: String, - // Used to allow searching on mobile - mobileSearch: { - type: Boolean, - value: false, - }, - }, + /** + * @type {{ plugin: Object }} + */ + _serverConfig: Object, + _version: String, + _showChangeListView: Boolean, + _showDashboardView: Boolean, + _showChangeView: Boolean, + _showDiffView: Boolean, + _showSettingsView: Boolean, + _showAdminView: Boolean, + _showCLAView: Boolean, + _showEditorView: Boolean, + _showPluginScreen: Boolean, + _showDocumentationSearch: Boolean, + /** @type {?} */ + _viewState: Object, + /** @type {?} */ + _lastError: Object, + _lastSearchPage: String, + _path: String, + _pluginScreenName: { + type: String, + computed: '_computePluginScreenName(params)', + }, + _settingsUrl: String, + _feedbackUrl: String, + // Used to allow searching on mobile + mobileSearch: { + type: Boolean, + value: false, + }, + }; + } - listeners: { - 'page-error': '_handlePageError', - 'title-change': '_handleTitleChange', - 'location-change': '_handleLocationChange', - 'rpc-log': '_handleRpcLog', - }, - - observers: [ - '_viewChanged(params.view)', - '_paramsChanged(params.*)', - ], - - behaviors: [ - Gerrit.BaseUrlBehavior, - Gerrit.KeyboardShortcutBehavior, - ], + static get observers() { + return [ + '_viewChanged(params.view)', + '_paramsChanged(params.*)', + ]; + } keyboardShortcuts() { return { @@ -111,14 +112,24 @@ [this.Shortcut.GO_TO_ABANDONED_CHANGES]: '_goToAbandonedChanges', [this.Shortcut.GO_TO_WATCHED_CHANGES]: '_goToWatchedChanges', }; - }, + } created() { + super.created(); this._bindKeyboardShortcuts(); - }, + this.addEventListener('page-error', + e => this._handlePageError(e)); + this.addEventListener('title-change', + e => this._handleTitleChange(e)); + this.addEventListener('location-change', + e => this._handleLocationChange(e)); + this.addEventListener('rpc-log', + e => this._handleRpcLog(e)); + } ready() { - this.$.reporting.appStarted(document.visibilityState === 'hidden'); + super.ready(); + this.$.reporting.appStarted(); this.$.router.start(); this.$.restAPI.getAccount().then(account => { @@ -165,7 +176,7 @@ selectedChangeIndex: 0, }, }; - }, + } _bindKeyboardShortcuts() { this.bindShortcut(this.Shortcut.SEND_REPLY, @@ -201,7 +212,7 @@ this.bindShortcut( this.Shortcut.TOGGLE_CHANGE_STAR, 's:keyup'); this.bindShortcut( - this.Shortcut.REFRESH_CHANGE_LIST, 'shift+r'); + this.Shortcut.REFRESH_CHANGE_LIST, 'shift+r:keyup'); this.bindShortcut( this.Shortcut.EDIT_TOPIC, 't'); @@ -214,7 +225,7 @@ this.bindShortcut( this.Shortcut.COLLAPSE_ALL_MESSAGES, 'z'); this.bindShortcut( - this.Shortcut.REFRESH_CHANGE, 'shift+r'); + this.Shortcut.REFRESH_CHANGE, 'shift+r:keyup'); this.bindShortcut( this.Shortcut.UP_TO_DASHBOARD, 'u'); this.bindShortcut( @@ -287,7 +298,7 @@ this.bindShortcut( this.Shortcut.SEARCH, '/'); - }, + } _accountChanged(account) { if (!account) { return; } @@ -298,7 +309,7 @@ this.$.restAPI.getEditPreferences(); this.$.errorManager.knownAccountId = this._account && this._account._account_id || null; - }, + } _viewChanged(view) { this.$.errorView.classList.remove('show'); @@ -328,7 +339,7 @@ }); } this.$.header.unfloat(); - }, + } _handlePageError(e) { const props = [ @@ -356,7 +367,7 @@ this._lastError = err; }); } - }, + } _handleLocationChange(e) { const hash = e.detail.hash.substring(1); @@ -365,7 +376,7 @@ pathname += '@' + hash; } this.set('_path', pathname); - }, + } _paramsChanged(paramsRecord) { const params = paramsRecord.base; @@ -373,7 +384,7 @@ if (viewsToCheck.includes(params.view)) { this.set('_lastSearchPage', location.pathname); } - }, + } _handleTitleChange(e) { if (e.detail.title) { @@ -381,54 +392,54 @@ } else { document.title = ''; } - }, + } _showKeyboardShortcuts(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } this.$.keyboardShortcuts.open(); - }, + } _handleKeyboardShortcutDialogClose() { this.$.keyboardShortcuts.close(); - }, + } _handleAccountDetailUpdate(e) { this.$.mainHeader.reload(); if (this.params.view === Gerrit.Nav.View.SETTINGS) { this.$$('gr-settings-view').reloadAccountDetail(); } - }, + } _handleRegistrationDialogClose(e) { this.params.justRegistered = false; this.$.registrationOverlay.close(); - }, + } _goToOpenedChanges() { Gerrit.Nav.navigateToStatusSearch('open'); - }, + } _goToUserDashboard() { Gerrit.Nav.navigateToUserDashboard(); - }, + } _goToMergedChanges() { Gerrit.Nav.navigateToStatusSearch('merged'); - }, + } _goToAbandonedChanges() { Gerrit.Nav.navigateToStatusSearch('abandoned'); - }, + } _goToWatchedChanges() { // The query is hardcoded, and doesn't respect custom menu entries Gerrit.Nav.navigateToSearchQuery('is:watched is:open'); - }, + } _computePluginScreenName({plugin, screen}) { if (!plugin || !screen) return ''; return `${plugin}-screen-${screen}`; - }, + } _logWelcome() { console.group('Runtime Info'); @@ -441,7 +452,7 @@ console.log(`Please file bugs and feedback at: ${this._feedbackUrl}`); } console.groupEnd(); - }, + } /** * Intercept RPC log events emitted by REST API interfaces. @@ -451,17 +462,19 @@ _handleRpcLog(e) { this.$.reporting.reportRpcTiming(e.detail.anonymizedUrl, e.detail.elapsed); - }, + } _mobileSearchToggle(e) { this.mobileSearch = !this.mobileSearch; - }, + } getThemeEndpoint() { // For now, we only have dark mode and light mode return window.localStorage.getItem('dark-theme') ? 'app-theme-dark' : 'app-theme-light'; - }, - }); + } + } + + customElements.define(GrAppElement.is, GrAppElement); })();
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js index ac8ea1a..46a81ee 100644 --- a/polygerrit-ui/app/elements/gr-app.js +++ b/polygerrit-ui/app/elements/gr-app.js
@@ -17,7 +17,11 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-app', - }); + class GrApp extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-app'; } + } + + customElements.define(GrApp.is, GrApp); })();
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html index 9f1b7f8..c069f8b 100644 --- a/polygerrit-ui/app/elements/gr-app_test.html +++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-app</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html index 2537a37..10ba710 100644 --- a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-admin-api</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js index df71da6..358fba7 100644 --- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js +++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
@@ -28,6 +28,7 @@ /** * Returns true if the property is defined on wrapped element. + * * @param {string} name * @return {boolean} */
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html index 0c4149c..c95128b 100644 --- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-attribute-helper</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js index b550f73..0454767 100644 --- a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js +++ b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js
@@ -31,7 +31,7 @@ this._createHook(); } this._hook.onAttached(element => - this.plugin.attributeHelper(element).bind('labels', callback)); + this.plugin.attributeHelper(element).bind('labels', callback)); return this; };
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js index 230be0e..2d07382 100644 --- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js +++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
@@ -76,6 +76,7 @@ /** * Get instance of last DOM hook element attached into the endpoint. * Returns a Promise, that's resolved when attachment is done. + * * @return {!Promise<!Element>} */ GrDomHook.prototype.getLastAttached = function() { @@ -108,6 +109,7 @@ /** * Install a new callback to invoke when a new instance of DOM hook element * is attached. + * * @param {function(Element)} callback */ GrDomHook.prototype.onAttached = function(callback) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html index 9e657fa..a00dc68 100644 --- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-dom-hooks</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js index 448e090..b98833e 100644 --- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js +++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
@@ -19,33 +19,39 @@ const INIT_PROPERTIES_TIMEOUT_MS = 10000; - Polymer({ - is: 'gr-endpoint-decorator', + class GrEndpointDecorator extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-endpoint-decorator'; } - properties: { - name: String, - /** @type {!Map} */ - _domHooks: { - type: Map, - value() { return new Map(); }, - }, - /** - * This map prevents importing the same endpoint twice. - * Without caching, if a plugin is loaded after the loaded plugins - * callback fires, it will be imported twice and appear twice on the page. - * @type {!Map} - */ - _initializedPlugins: { - type: Map, - value() { return new Map(); }, - }, - }, + static get properties() { + return { + name: String, + /** @type {!Map} */ + _domHooks: { + type: Map, + value() { return new Map(); }, + }, + /** + * This map prevents importing the same endpoint twice. + * Without caching, if a plugin is loaded after the loaded plugins + * callback fires, it will be imported twice and appear twice on the page. + * + * @type {!Map} + */ + _initializedPlugins: { + type: Map, + value() { return new Map(); }, + }, + }; + } detached() { + super.detached(); for (const [el, domHook] of this._domHooks) { domHook.handleInstanceDetached(el); } - }, + } /** * @suppress {checkTypes} @@ -54,7 +60,7 @@ return new Promise((resolve, reject) => { (this.importHref || Polymer.importHref)(url, resolve, reject); }); - }, + } _initDecoration(name, plugin) { const el = document.createElement(name); @@ -62,7 +68,7 @@ this.getContentChildren().find( el => el.nodeName !== 'GR-ENDPOINT-PARAM')) .then(el => this._appendChild(el)); - }, + } _initReplacement(name, plugin) { this.getContentChildNodes() @@ -71,12 +77,12 @@ const el = document.createElement(name); return this._initProperties(el, plugin).then( el => this._appendChild(el)); - }, + } _getEndpointParams() { return Array.from( Polymer.dom(this).querySelectorAll('gr-endpoint-param')); - }, + } /** * @param {!Element} el @@ -95,25 +101,25 @@ return helper.get('value').then( value => helper.bind('value', value => plugin.attributeHelper(el).set(paramName, value)) - ); + ); }); let timeoutId; const timeout = new Promise( - resolve => timeoutId = setTimeout(() => { - console.warn( - 'Timeout waiting for endpoint properties initialization: ' + + resolve => timeoutId = setTimeout(() => { + console.warn( + 'Timeout waiting for endpoint properties initialization: ' + `plugin ${plugin.getPluginName()}, endpoint ${this.name}`); - }, INIT_PROPERTIES_TIMEOUT_MS)); + }, INIT_PROPERTIES_TIMEOUT_MS)); return Promise.race([timeout, Promise.all(expectProperties)]) .then(() => { clearTimeout(timeoutId); return el; }); - }, + } _appendChild(el) { return Polymer.dom(this.root).appendChild(el); - }, + } _initModule({moduleName, plugin, type, domHook}) { const name = plugin.getPluginName() + '.' + moduleName; @@ -137,9 +143,10 @@ domHook.handleInstanceAttached(el); this._domHooks.set(el, domHook); }); - }, + } ready() { + super.ready(); Gerrit._endpoints.onNewEndpoint(this.name, this._initModule.bind(this)); Gerrit.awaitPluginsLoaded().then(() => Promise.all( Gerrit._endpoints.getPlugins(this.name).map( @@ -149,6 +156,8 @@ .getDetails(this.name) .forEach(this._initModule, this) ); - }, - }); + } + } + + customElements.define(GrEndpointDecorator.is, GrEndpointDecorator); })();
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html index b0ad585..58e2074 100644 --- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-endpoint-decorator</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js index c7a2d9a..3010750 100644 --- a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js +++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
@@ -17,17 +17,21 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-endpoint-param', + class GrEndpointParam extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-endpoint-param'; } - properties: { - name: String, - value: { - type: Object, - notify: true, - observer: '_valueChanged', - }, - }, + static get properties() { + return { + name: String, + value: { + type: Object, + notify: true, + observer: '_valueChanged', + }, + }; + } _valueChanged(newValue, oldValue) { /* In polymer 2 the following change was made: @@ -42,6 +46,8 @@ value: newValue, }; this.dispatchEvent(new CustomEvent('value-changed', {detail})); - }, - }); + } + } + + customElements.define(GrEndpointParam.is, GrEndpointParam); })();
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js index 9da9c86..845c1e1 100644 --- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js +++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
@@ -25,6 +25,7 @@ /** * Add a callback to arbitrary event. * The callback may return false to prevent event bubbling. + * * @param {string} event Event name * @param {function(Event):boolean} callback * @return {function()} Unsubscribe function. @@ -35,6 +36,7 @@ /** * Alias of onClick + * * @see onClick */ GrEventHelper.prototype.onTap = function(callback) { @@ -44,6 +46,7 @@ /** * Add a callback to element click or touch. * The callback may return false to prevent event bubbling. + * * @param {function(Event):boolean} callback * @return {function()} Unsubscribe function. */ @@ -53,6 +56,7 @@ /** * Alias of captureClick + * * @see captureClick */ GrEventHelper.prototype.captureTap = function(callback) { @@ -64,6 +68,7 @@ * Callback is installed on parent during capture phase. * https://www.w3.org/TR/DOM-Level-3-Events/#event-flow * The callback may return false to cancel regular event listeners. + * * @param {function(Event):boolean} callback * @return {function()} Unsubscribe function. */
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html index bd76bd4..a98a0d2 100644 --- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-event-helper</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js index e90ff30..aa9b13e 100644 --- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js +++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
@@ -17,20 +17,24 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-external-style', + class GrExternalStyle extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-external-style'; } - properties: { - name: String, - _urlsImported: { - type: Array, - value() { return []; }, - }, - _stylesApplied: { - type: Array, - value() { return []; }, - }, - }, + static get properties() { + return { + name: String, + _urlsImported: { + type: Array, + value() { return []; }, + }, + _stylesApplied: { + type: Array, + value() { return []; }, + }, + }; + } /** * @suppress {checkTypes} @@ -41,14 +45,13 @@ return new Promise((resolve, reject) => { (this.importHref || Polymer.importHref)(url, resolve, reject); }); - }, + } _applyStyle(name) { if (this._stylesApplied.includes(name)) { return; } this._stylesApplied.push(name); - // Hybrid custom-style syntax: - // https://polymer-library.polymer-project.org/2.0/docs/devguide/style-shadow-dom - const s = document.createElement('style', 'custom-style'); + + const s = document.createElement('style'); s.setAttribute('include', name); const cs = document.createElement('custom-style'); cs.appendChild(s); @@ -57,7 +60,7 @@ const topEl = document.getElementsByTagName('body')[0]; topEl.insertBefore(cs, topEl.firstChild); Polymer.updateStyles(); - }, + } _importAndApply() { Promise.all(Gerrit._endpoints.getPlugins(this.name).map( @@ -68,14 +71,18 @@ this._applyStyle(name); } }); - }, + } attached() { + super.attached(); this._importAndApply(); - }, + } ready() { + super.ready(); Gerrit.awaitPluginsLoaded().then(() => this._importAndApply()); - }, - }); + } + } + + customElements.define(GrExternalStyle.is, GrExternalStyle); })();
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html index 9566067..9f0c950 100644 --- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-external-style</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js index 21da106..30adcc8 100644 --- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js +++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -17,15 +17,19 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-plugin-host', + class GrPluginHost extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-plugin-host'; } - properties: { - config: { - type: Object, - observer: '_configChanged', - }, - }, + static get properties() { + return { + config: { + type: Object, + observer: '_configChanged', + }, + }; + } _configChanged(config) { const plugins = config.plugin; @@ -50,7 +54,7 @@ } Gerrit._loadPlugins(pluginsPending, pluginOpts); - }, + } /** * Omit .js plugins that have .html counterparts. @@ -61,6 +65,8 @@ const counterpart = url.replace(/\.js$/, '.html'); return !htmlPlugins.includes(counterpart); }); - }, - }); + } + } + + customElements.define(GrPluginHost.is, GrPluginHost); })();
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html index 3a8e4d8..1f748b8 100644 --- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-plugin-host</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html index 402d988..d084445 100644 --- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html +++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html
@@ -20,7 +20,9 @@ <dom-module id="gr-plugin-popup"> <template> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <gr-overlay id="overlay" with-backdrop> <slot></slot> </gr-overlay>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js index 2e7a2b7..412e788a 100644 --- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js +++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
@@ -17,19 +17,23 @@ (function(window) { 'use strict'; - Polymer({ - is: 'gr-plugin-popup', + class GrPluginPopup extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-plugin-popup'; } get opened() { return this.$.overlay.opened; - }, + } open() { return this.$.overlay.open(); - }, + } close() { this.$.overlay.close(); - }, - }); + } + } + + customElements.define(GrPluginPopup.is, GrPluginPopup); })(window);
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html index 1f1e81e..2ff2aba 100644 --- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-plugin-popup</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js index 556cfd8..5418267 100644 --- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js +++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
@@ -22,6 +22,7 @@ * Provides method for opening and closing popups from plugin. * opt_moduleName is a name of custom element that will be automatically * inserted on popup opening. + * * @param {!Object} plugin * @param {opt_moduleName=} string */ @@ -40,23 +41,24 @@ * Opens the popup, inserts it into DOM over current UI. * Creates the popup if not previously created. Creates popup content element, * if it was provided with constructor. + * * @returns {!Promise<!Object>} */ GrPopupInterface.prototype.open = function() { if (!this._openingPromise) { this._openingPromise = this.plugin.hook('plugin-overlay').getLastAttached() - .then(hookEl => { - const popup = document.createElement('gr-plugin-popup'); - if (this._moduleName) { - const el = Polymer.dom(popup).appendChild( - document.createElement(this._moduleName)); - el.plugin = this.plugin; - } - this._popup = Polymer.dom(hookEl).appendChild(popup); - Polymer.dom.flush(); - return this._popup.open().then(() => this); - }); + .then(hookEl => { + const popup = document.createElement('gr-plugin-popup'); + if (this._moduleName) { + const el = Polymer.dom(popup).appendChild( + document.createElement(this._moduleName)); + el.plugin = this.plugin; + } + this._popup = Polymer.dom(hookEl).appendChild(popup); + Polymer.dom.flush(); + return this._popup.open().then(() => this); + }); } return this._openingPromise; };
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html index 53370e2..0be07d4 100644 --- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-popup-interface</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html index 0b32f8a..5a47a85 100644 --- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo-api</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html index cbc2de6..6248d78 100644 --- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-settings-api</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js index 1de8283..aedd658 100644 --- a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js +++ b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js
@@ -33,12 +33,13 @@ * if it hasn't been added yet. A root node is an document or is the * associated shadowRoot. This class can be added to any element with the same * root node. + * * @param {HTMLElement} element The element to get class name for. * @return {string} Appropriate class name for the element is returned */ GrStyleObject.prototype.getClassName = function(element) { - let rootNode = Polymer.Settings.useShadow - ? element.getRootNode() : document.body; + let rootNode = Polymer.Settings.useShadow ? + element.getRootNode() : document.body; if (rootNode === document) { rootNode = document.head; } @@ -56,25 +57,25 @@ /** * Apply shared style to the element. + * * @param {HTMLElement} element The element to apply style for */ GrStyleObject.prototype.apply = function(element) { element.classList.add(this.getClassName(element)); }; - function GrStylesApi() { } /** * Creates a new GrStyleObject with specified style properties. + * * @param {string} String with style properties. * @return {GrStyleObject} - */ + */ GrStylesApi.prototype.css = function(ruleStr) { return new GrStyleObject(ruleStr); }; - window.GrStylesApi = GrStylesApi; })(window);
diff --git a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html index 46bda6d..7d14e21 100644 --- a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-admin-api</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -105,7 +105,6 @@ return [element1, element2, element3]; } - test('getClassName - body level elements', () => { const bodyLevelElements = createNestedElements(document.body); @@ -161,7 +160,6 @@ assertDisplayPropertyValues(elements, ['inline', 'none', 'inline']); } - function assertAllElementsHaveDefaultStyle(elements) { for (const element of elements) { assert.equal(getComputedStyle(element).getPropertyValue('display'),
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html index 6332b91..9401a15 100644 --- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-theme-api</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html index 662c6f1..e685030 100644 --- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html +++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
@@ -40,7 +40,9 @@ display: none; } </style> - <style include="gr-form-styles"></style> + <style include="gr-form-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <div class="gr-form-styles"> <section> <span class="title"></span> @@ -82,7 +84,6 @@ hidden$="[[!usernameMutable]]" class="value"> <iron-input - disabled="[[_saving]]" on-keydown="_handleKeydown" bind-value="{{_username}}"> <input @@ -103,7 +104,6 @@ hidden$="[[!nameMutable]]" class="value"> <iron-input - disabled="[[_saving]]" on-keydown="_handleKeydown" bind-value="{{_account.name}}"> <input @@ -119,7 +119,6 @@ <span class="title">Status (e.g. "Vacation")</span> <span class="value"> <iron-input - disabled="[[_saving]]" on-keydown="_handleKeydown" bind-value="{{_account.status}}"> <input
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js index b0c7661..9b17fd7 100644 --- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js +++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
@@ -17,65 +17,71 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-account-info', - + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrAccountInfo extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-account-info'; } /** * Fired when account details are changed. * * @event account-detail-update */ - properties: { - usernameMutable: { - type: Boolean, - notify: true, - computed: '_computeUsernameMutable(_serverConfig, _account.username)', - }, - nameMutable: { - type: Boolean, - notify: true, - computed: '_computeNameMutable(_serverConfig)', - }, - hasUnsavedChanges: { - type: Boolean, - notify: true, - computed: '_computeHasUnsavedChanges(_hasNameChange, ' + + static get properties() { + return { + usernameMutable: { + type: Boolean, + notify: true, + computed: '_computeUsernameMutable(_serverConfig, _account.username)', + }, + nameMutable: { + type: Boolean, + notify: true, + computed: '_computeNameMutable(_serverConfig)', + }, + hasUnsavedChanges: { + type: Boolean, + notify: true, + computed: '_computeHasUnsavedChanges(_hasNameChange, ' + '_hasUsernameChange, _hasStatusChange)', - }, + }, - _hasNameChange: Boolean, - _hasUsernameChange: Boolean, - _hasStatusChange: Boolean, - _loading: { - type: Boolean, - value: false, - }, - _saving: { - type: Boolean, - value: false, - }, - /** @type {?} */ - _account: Object, - _serverConfig: Object, - _username: { - type: String, - observer: '_usernameChanged', - }, - _avatarChangeUrl: { - type: String, - value: '', - }, - }, + _hasNameChange: Boolean, + _hasUsernameChange: Boolean, + _hasStatusChange: Boolean, + _loading: { + type: Boolean, + value: false, + }, + _saving: { + type: Boolean, + value: false, + }, + /** @type {?} */ + _account: Object, + _serverConfig: Object, + _username: { + type: String, + observer: '_usernameChanged', + }, + _avatarChangeUrl: { + type: String, + value: '', + }, + }; + } - behaviors: [ - Gerrit.FireBehavior, - ], - - observers: [ - '_nameChanged(_account.name)', - '_statusChanged(_account.status)', - ], + static get observers() { + return [ + '_nameChanged(_account.name)', + '_statusChanged(_account.status)', + ]; + } loadData() { const promises = []; @@ -104,7 +110,7 @@ return Promise.all(promises).then(() => { this._loading = false; }); - }, + } save() { if (!this.hasUnsavedChanges) { @@ -123,29 +129,29 @@ this._saving = false; this.fire('account-detail-update'); }); - }, + } _maybeSetName() { return this._hasNameChange && this.nameMutable ? - this.$.restAPI.setAccountName(this._account.name) : - Promise.resolve(); - }, + this.$.restAPI.setAccountName(this._account.name) : + Promise.resolve(); + } _maybeSetUsername() { return this._hasUsernameChange && this.usernameMutable ? - this.$.restAPI.setAccountUsername(this._username) : - Promise.resolve(); - }, + this.$.restAPI.setAccountUsername(this._username) : + Promise.resolve(); + } _maybeSetStatus() { return this._hasStatusChange ? - this.$.restAPI.setAccountStatus(this._account.status) : - Promise.resolve(); - }, + this.$.restAPI.setAccountStatus(this._account.status) : + Promise.resolve(); + } _computeHasUnsavedChanges(nameChanged, usernameChanged, statusChanged) { return nameChanged || usernameChanged || statusChanged; - }, + } _computeUsernameMutable(config, username) { // Polymer 2: check for undefined @@ -159,34 +165,34 @@ // Username may not be changed once it is set. return config.auth.editable_account_fields.includes('USER_NAME') && !username; - }, + } _computeNameMutable(config) { return config.auth.editable_account_fields.includes('FULL_NAME'); - }, + } _statusChanged() { if (this._loading) { return; } this._hasStatusChange = true; - }, + } _usernameChanged() { if (this._loading || !this._account) { return; } this._hasUsernameChange = (this._account.username || '') !== (this._username || ''); - }, + } _nameChanged() { if (this._loading) { return; } this._hasNameChange = true; - }, + } _handleKeydown(e) { if (e.keyCode === 13) { // Enter e.stopPropagation(); this.save(); } - }, + } _hideAvatarChangeUrl(avatarChangeUrl) { if (!avatarChangeUrl) { @@ -194,6 +200,8 @@ } return ''; - }, - }); + } + } + + customElements.define(GrAccountInfo.is, GrAccountInfo); })();
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html index de222a9..a9056b91 100644 --- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html +++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-account-info</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -152,7 +152,7 @@ usernameChangedSpy = sandbox.spy(element, '_usernameChanged'); statusChangedSpy = sandbox.spy(element, '_statusChanged'); element.set('_serverConfig', - {auth: {editable_account_fields: ['FULL_NAME', 'USER_NAME']}}); + {auth: {editable_account_fields: ['FULL_NAME', 'USER_NAME']}}); nameStub = sandbox.stub(element.$.restAPI, 'setAccountName', name => Promise.resolve()); @@ -280,7 +280,7 @@ setup(() => { statusChangedSpy = sandbox.spy(element, '_statusChanged'); element.set('_serverConfig', - {auth: {editable_account_fields: []}}); + {auth: {editable_account_fields: []}}); statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus', status => Promise.resolve());
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html index 852161c..74d92d3 100644 --- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html +++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html
@@ -32,7 +32,9 @@ width: auto; } </style> - <style include="gr-form-styles"></style> + <style include="gr-form-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <div class="gr-form-styles"> <table id="agreements"> <thead>
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js index 41595a9..2886c87 100644 --- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js +++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
@@ -17,33 +17,41 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-agreements-list', + /** + * @appliesMixin Gerrit.BaseUrlMixin + */ + class GrAgreementsList extends Polymer.mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-agreements-list'; } - properties: { - _agreements: Array, - }, - - behaviors: [ - Gerrit.BaseUrlBehavior, - ], + static get properties() { + return { + _agreements: Array, + }; + } attached() { + super.attached(); this.loadData(); - }, + } loadData() { return this.$.restAPI.getAccountAgreements().then(agreements => { this._agreements = agreements; }); - }, + } getUrl() { return this.getBaseUrl() + '/settings/new-agreement'; - }, + } getUrlBase(item) { return this.getBaseUrl() + '/' + item; - }, - }); + } + } + + customElements.define(GrAgreementsList.is, GrAgreementsList); })();
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html index 14cf97c..cd5bc09 100644 --- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html +++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-settings-view</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html index 88a53ee..09a9dbc 100644 --- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html +++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
@@ -24,7 +24,9 @@ <dom-module id="gr-change-table-editor"> <template> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="gr-form-styles"> #changeCols { width: auto;
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js index 85eed9c..aa4e0c5 100644 --- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js +++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
@@ -17,27 +17,33 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-change-table-editor', + /** + * @appliesMixin Gerrit.ChangeTableMixin + */ + class GrChangeTableEditor extends Polymer.mixinBehaviors( [ + Gerrit.ChangeTableBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-change-table-editor'; } - properties: { - displayedColumns: { - type: Array, - notify: true, - }, - showNumber: { - type: Boolean, - notify: true, - }, - }, - - behaviors: [ - Gerrit.ChangeTableBehavior, - ], + static get properties() { + return { + displayedColumns: { + type: Array, + notify: true, + }, + showNumber: { + type: Boolean, + notify: true, + }, + }; + } /** * Get the list of enabled column names from whichever checkboxes are * checked (excluding the number checkbox). + * * @return {!Array<string>} */ _getDisplayedColumns() { @@ -45,7 +51,7 @@ .querySelectorAll('.checkboxContainer input:not([name=number])')) .filter(checkbox => checkbox.checked) .map(checkbox => checkbox.name); - }, + } /** * Handle a click on a checkbox container and relay the click to the checkbox it @@ -55,7 +61,7 @@ const checkbox = Polymer.dom(e.target).querySelector('input'); if (!checkbox) { return; } checkbox.click(); - }, + } /** * Handle a click on the number checkbox and update the showNumber property @@ -63,7 +69,7 @@ */ _handleNumberCheckboxClick(e) { this.showNumber = Polymer.dom(e).rootTarget.checked; - }, + } /** * Handle a click on a displayed column checkboxes (excluding number) and @@ -71,6 +77,8 @@ */ _handleTargetClick(e) { this.set('displayedColumns', this._getDisplayedColumns()); - }, - }); + } + } + + customElements.define(GrChangeTableEditor.is, GrChangeTableEditor); })();
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html index 29a7081..ec37c03 100644 --- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html +++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-settings-view</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html index c29153e..fb5d64f 100644 --- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html +++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html
@@ -66,7 +66,9 @@ max-width: 50em; } </style> - <style include="gr-form-styles"></style> + <style include="gr-form-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <main> <h1>New Contributor Agreement</h1> <h3>Select an agreement type:</h3>
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js index 9abd08d..ca81544 100644 --- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js +++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
@@ -17,33 +17,40 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-cla-view', + /** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + */ + class GrClaView extends Polymer.mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-cla-view'; } - properties: { - _groups: Object, - /** @type {?} */ - _serverConfig: Object, - _agreementsText: String, - _agreementName: String, - _signedAgreements: Array, - _showAgreements: { - type: Boolean, - value: false, - }, - _agreementsUrl: String, - }, - - behaviors: [ - Gerrit.BaseUrlBehavior, - Gerrit.FireBehavior, - ], + static get properties() { + return { + _groups: Object, + /** @type {?} */ + _serverConfig: Object, + _agreementsText: String, + _agreementName: String, + _signedAgreements: Array, + _showAgreements: { + type: Boolean, + value: false, + }, + _agreementsUrl: String, + }; + } attached() { + super.attached(); this.loadData(); this.fire('title-change', {title: 'New Contributor Agreement'}); - }, + } loadData() { const promises = []; @@ -62,7 +69,7 @@ })); return Promise.all(promises); - }, + } _getAgreementsUrl(configUrl) { let url; @@ -76,14 +83,14 @@ } return url; - }, + } _handleShowAgreement(e) { this._agreementName = e.target.getAttribute('data-name'); this._agreementsUrl = this._getAgreementsUrl(e.target.getAttribute('data-url')); this._showAgreements = true; - }, + } _handleSaveAgreements(e) { this._createToast('Agreement saving...'); @@ -99,16 +106,16 @@ this._agreementsText = ''; this._showAgreements = false; }); - }, + } _createToast(message) { this.dispatchEvent(new CustomEvent( 'show-alert', {detail: {message}, bubbles: true, composed: true})); - }, + } _computeShowAgreementsClass(agreements) { return agreements ? 'show' : ''; - }, + } _disableAgreements(item, groups, signedAgreements) { if (!groups) return false; @@ -120,16 +127,16 @@ } } return false; - }, + } _hideAgreements(item, groups, signedAgreements) { return this._disableAgreements(item, groups, signedAgreements) ? - '' : 'hide'; - }, + '' : 'hide'; + } _disableAgreementsText(text) { return text.toLowerCase() === 'i agree' ? false : true; - }, + } // This checks for auto_verify_group, // if specified it returns 'hideAgreementsTextBox' which @@ -150,6 +157,8 @@ } } } - }, - }); + } + } + + customElements.define(GrClaView.is, GrClaView); })();
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html index f1b65d9..13c4de4 100644 --- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html +++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-cla-view</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html index 53a30c3..80440c7 100644 --- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html +++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html
@@ -24,8 +24,12 @@ <dom-module id="gr-edit-preferences"> <template> - <style include="shared-styles"></style> - <style include="gr-form-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="gr-form-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <div id="editPreferences" class="gr-form-styles"> <section> <span class="title">Tab width</span>
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js index 86350f9..36e53b0 100644 --- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js +++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
@@ -17,66 +17,72 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-edit-preferences', + class GrEditPreferences extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-edit-preferences'; } - properties: { - hasUnsavedChanges: { - type: Boolean, - notify: true, - value: false, - }, + static get properties() { + return { + hasUnsavedChanges: { + type: Boolean, + notify: true, + value: false, + }, - /** @type {?} */ - editPrefs: Object, - }, + /** @type {?} */ + editPrefs: Object, + }; + } loadData() { return this.$.restAPI.getEditPreferences().then(prefs => { this.editPrefs = prefs; }); - }, + } _handleEditPrefsChanged() { this.hasUnsavedChanges = true; - }, + } _handleEditSyntaxHighlightingChanged() { this.set('editPrefs.syntax_highlighting', this.$.editSyntaxHighlighting.checked); this._handleEditPrefsChanged(); - }, + } _handleEditShowTabsChanged() { this.set('editPrefs.show_tabs', this.$.editShowTabs.checked); this._handleEditPrefsChanged(); - }, + } _handleMatchBracketsChanged() { this.set('editPrefs.match_brackets', this.$.showMatchBrackets.checked); this._handleEditPrefsChanged(); - }, + } _handleEditLineWrappingChanged() { this.set('editPrefs.line_wrapping', this.$.editShowLineWrapping.checked); this._handleEditPrefsChanged(); - }, + } _handleIndentWithTabsChanged() { this.set('editPrefs.indent_with_tabs', this.$.showIndentWithTabs.checked); this._handleEditPrefsChanged(); - }, + } _handleAutoCloseBracketsChanged() { this.set('editPrefs.auto_close_brackets', this.$.showAutoCloseBrackets.checked); this._handleEditPrefsChanged(); - }, + } save() { return this.$.restAPI.saveEditPreferences(this.editPrefs).then(res => { this.hasUnsavedChanges = false; }); - }, - }); + } + } + + customElements.define(GrEditPreferences.is, GrEditPreferences); })();
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html index c1c5c52..8b0bf86 100644 --- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html +++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-edit-preferences</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html index caaf18b..041b2a7 100644 --- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html +++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
@@ -23,7 +23,9 @@ <dom-module id="gr-email-editor"> <template> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="gr-form-styles"> th { color: var(--deemphasized-text-color);
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js index 8490b26..8ec1067 100644 --- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js +++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
@@ -17,33 +17,37 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-email-editor', + class GrEmailEditor extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-email-editor'; } - properties: { - hasUnsavedChanges: { - type: Boolean, - notify: true, - value: false, - }, + static get properties() { + return { + hasUnsavedChanges: { + type: Boolean, + notify: true, + value: false, + }, - _emails: Array, - _emailsToRemove: { - type: Array, - value() { return []; }, - }, - /** @type {?string} */ - _newPreferred: { - type: String, - value: null, - }, - }, + _emails: Array, + _emailsToRemove: { + type: Array, + value() { return []; }, + }, + /** @type {?string} */ + _newPreferred: { + type: String, + value: null, + }, + }; + } loadData() { return this.$.restAPI.getAccountEmails().then(emails => { this._emails = emails; }); - }, + } save() { const promises = []; @@ -62,7 +66,7 @@ this._newPreferred = null; this.hasUnsavedChanges = false; }); - }, + } _handleDeleteButton(e) { const index = parseInt(Polymer.dom(e).localTarget @@ -71,13 +75,13 @@ this.push('_emailsToRemove', email); this.splice('_emails', index, 1); this.hasUnsavedChanges = true; - }, + } _handlePreferredControlClick(e) { if (e.target.classList.contains('preferredControl')) { e.target.firstElementChild.click(); } - }, + } _handlePreferredChange(e) { const preferred = e.target.value; @@ -90,6 +94,8 @@ this.set(['_emails', i, 'preferred'], false); } } - }, - }); + } + } + + customElements.define(GrEmailEditor.is, GrEmailEditor); })();
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html index 8d3f2d2..e55ac97 100644 --- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html +++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-email-editor</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html index cf73d99..7b8a191 100644 --- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html +++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html
@@ -26,7 +26,9 @@ <dom-module id="gr-gpg-editor"> <template> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="gr-form-styles"> .keyHeader { width: 9em;
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js index 78025d1..14d7a4c 100644 --- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js +++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
@@ -17,27 +17,31 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-gpg-editor', + class GrGpgEditor extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-gpg-editor'; } - properties: { - hasUnsavedChanges: { - type: Boolean, - value: false, - notify: true, - }, - _keys: Array, - /** @type {?} */ - _keyToView: Object, - _newKey: { - type: String, - value: '', - }, - _keysToRemove: { - type: Array, - value() { return []; }, - }, - }, + static get properties() { + return { + hasUnsavedChanges: { + type: Boolean, + value: false, + notify: true, + }, + _keys: Array, + /** @type {?} */ + _keyToView: Object, + _newKey: { + type: String, + value: '', + }, + _keysToRemove: { + type: Array, + value() { return []; }, + }, + }; + } loadData() { this._keys = []; @@ -46,13 +50,13 @@ return; } this._keys = Object.keys(keys) - .map(key => { - const gpgKey = keys[key]; - gpgKey.id = key; - return gpgKey; - }); + .map(key => { + const gpgKey = keys[key]; + gpgKey.id = key; + return gpgKey; + }); }); - }, + } save() { const promises = this._keysToRemove.map(key => { @@ -63,18 +67,18 @@ this._keysToRemove = []; this.hasUnsavedChanges = false; }); - }, + } _showKey(e) { const el = Polymer.dom(e).localTarget; const index = parseInt(el.getAttribute('data-index'), 10); this._keyToView = this._keys[index]; this.$.viewKeyOverlay.open(); - }, + } _closeOverlay() { this.$.viewKeyOverlay.close(); - }, + } _handleDeleteKey(e) { const el = Polymer.dom(e).localTarget; @@ -82,7 +86,7 @@ this.push('_keysToRemove', this._keys[index]); this.splice('_keys', index, 1); this.hasUnsavedChanges = true; - }, + } _handleAddKey() { this.$.addButton.disabled = true; @@ -96,10 +100,12 @@ this.$.addButton.disabled = false; this.$.newKey.disabled = false; }); - }, + } _computeAddButtonDisabled(newKey) { return !newKey.length; - }, - }); + } + } + + customElements.define(GrGpgEditor.is, GrGpgEditor); })();
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html index 9cfbde5f..575285b 100644 --- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html +++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-gpg-editor</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html index ca500c8..e51294d 100644 --- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html +++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html
@@ -23,7 +23,9 @@ <dom-module id="gr-group-list"> <template> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="gr-form-styles"> #groups .nameColumn { min-width: 11em;
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js index d62a241..87fb852 100644 --- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js +++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
@@ -17,12 +17,16 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-group-list', + class GrGroupList extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-group-list'; } - properties: { - _groups: Array, - }, + static get properties() { + return { + _groups: Array, + }; + } loadData() { return this.$.restAPI.getAccountGroups().then(groups => { @@ -30,11 +34,11 @@ return a.name.localeCompare(b.name); }); }); - }, + } _computeVisibleToAll(group) { return group.options.visible_to_all ? 'Yes' : 'No'; - }, + } _computeGroupPath(group) { if (!group || !group.id) { return; } @@ -42,6 +46,8 @@ // Group ID is already encoded from the API // Decode it here to match with our router encoding behavior return Gerrit.Nav.getUrlForGroup(decodeURIComponent(group.id)); - }, - }); + } + } + + customElements.define(GrGroupList.is, GrGroupList); })();
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html index 3c3ece36..d8bf888 100644 --- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html +++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-settings-view</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html index 0cb9695..22ba457 100644 --- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html +++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html
@@ -56,7 +56,9 @@ right: 2em; } </style> - <style include="gr-form-styles"></style> + <style include="gr-form-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <div class="gr-form-styles"> <div hidden$="[[_passwordUrl]]"> <section>
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js index 003e471..5c0a059 100644 --- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js +++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
@@ -17,18 +17,23 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-http-password', + class GrHttpPassword extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-http-password'; } - properties: { - _username: String, - _generatedPassword: String, - _passwordUrl: String, - }, + static get properties() { + return { + _username: String, + _generatedPassword: String, + _passwordUrl: String, + }; + } attached() { + super.attached(); this.loadData(); - }, + } loadData() { const promises = []; @@ -42,7 +47,7 @@ })); return Promise.all(promises); - }, + } _handleGenerateTap() { this._generatedPassword = 'Generating...'; @@ -50,14 +55,16 @@ this.$.restAPI.generateAccountHttpPassword().then(newPassword => { this._generatedPassword = newPassword; }); - }, + } _closeOverlay() { this.$.generatedPasswordOverlay.close(); - }, + } _generatedPasswordOverlayClosed() { this._generatedPassword = ''; - }, - }); + } + } + + customElements.define(GrHttpPassword.is, GrHttpPassword); })();
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html index 8924058..89da766 100644 --- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html +++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-settings-view</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.html b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.html index ee855cc..53d74f2 100644 --- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.html +++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.html
@@ -26,7 +26,9 @@ <dom-module id="gr-identities"> <template> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="gr-form-styles"> tr th.emailAddressHeader, tr th.identityHeader {
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js index c927f1e..8b74130 100644 --- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js +++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
@@ -22,61 +22,66 @@ 'OAUTH', ]; - Polymer({ - is: 'gr-identities', + /** + * @appliesMixin Gerrit.BaseUrlMixin + */ + class GrIdentities extends Polymer.mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-identities'; } - properties: { - _identities: Object, - _idName: String, - serverConfig: Object, - _showLinkAnotherIdentity: { - type: Boolean, - computed: '_computeShowLinkAnotherIdentity(serverConfig)', - }, - }, - - behaviors: [ - Gerrit.BaseUrlBehavior, - ], + static get properties() { + return { + _identities: Object, + _idName: String, + serverConfig: Object, + _showLinkAnotherIdentity: { + type: Boolean, + computed: '_computeShowLinkAnotherIdentity(serverConfig)', + }, + }; + } loadData() { return this.$.restAPI.getExternalIds().then(id => { this._identities = id; }); - }, + } _computeIdentity(id) { return id && id.startsWith('mailto:') ? '' : id; - }, + } _computeHideDeleteClass(canDelete) { return canDelete ? 'show' : ''; - }, + } _handleDeleteItemConfirm() { this.$.overlay.close(); return this.$.restAPI.deleteAccountIdentity([this._idName]) .then(() => { this.loadData(); }); - }, + } _handleConfirmDialogCancel() { this.$.overlay.close(); - }, + } _handleDeleteItem(e) { const name = e.model.get('item.identity'); if (!name) { return; } this._idName = name; this.$.overlay.open(); - }, + } _computeIsTrusted(item) { return item ? '' : 'Untrusted'; - }, + } filterIdentities(item) { return !item.identity.startsWith('username:'); - }, + } _computeShowLinkAnotherIdentity(config) { if (config && config.auth && @@ -86,7 +91,7 @@ } return false; - }, + } _computeLinkAnotherIdentity() { const baseUrl = this.getBaseUrl() || ''; @@ -95,6 +100,8 @@ pathname = '/' + pathname.substring(baseUrl.length); } return baseUrl + '/login/' + encodeURIComponent(pathname) + '?link'; - }, - }); + } + } + + customElements.define(GrIdentities.is, GrIdentities); })();
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html index 1277424..e2f8cad 100644 --- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html +++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-identities</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html index 1485628..46fc165 100644 --- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html +++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html
@@ -44,7 +44,9 @@ min-width: 23em; } </style> - <style include="gr-form-styles"></style> + <style include="gr-form-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <div class="gr-form-styles"> <table> <thead>
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js index 4f3c0c7..5b6c978 100644 --- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js +++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
@@ -17,14 +17,18 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-menu-editor', + class GrMenuEditor extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-menu-editor'; } - properties: { - menuItems: Array, - _newName: String, - _newUrl: String, - }, + static get properties() { + return { + menuItems: Array, + _newName: String, + _newUrl: String, + }; + } _handleMoveUpButton(e) { const index = Number(Polymer.dom(e).localTarget.dataset.index); @@ -32,7 +36,7 @@ const row = this.menuItems[index]; const prev = this.menuItems[index - 1]; this.splice('menuItems', index - 1, 2, row, prev); - }, + } _handleMoveDownButton(e) { const index = Number(Polymer.dom(e).localTarget.dataset.index); @@ -40,12 +44,12 @@ const row = this.menuItems[index]; const next = this.menuItems[index + 1]; this.splice('menuItems', index, 2, next, row); - }, + } _handleDeleteButton(e) { const index = Number(Polymer.dom(e).localTarget.dataset.index); this.splice('menuItems', index, 1); - }, + } _handleAddButton() { if (this._computeAddDisabled(this._newName, this._newUrl)) { return; } @@ -58,17 +62,19 @@ this._newName = ''; this._newUrl = ''; - }, + } _computeAddDisabled(newName, newUrl) { return !newName.length || !newUrl.length; - }, + } _handleInputKeydown(e) { if (e.keyCode === 13) { e.stopPropagation(); this._handleAddButton(); } - }, - }); + } + } + + customElements.define(GrMenuEditor.is, GrMenuEditor); })();
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html index 134e018..380e00a 100644 --- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html +++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-settings-view</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html index f366d2a..c289a49 100644 --- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html +++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
@@ -26,7 +26,9 @@ <dom-module id="gr-registration-dialog"> <template> - <style include="gr-form-styles"></style> + <style include="gr-form-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="shared-styles"> :host { display: block; @@ -84,8 +86,7 @@ <section> <div class="title">Full Name</div> <iron-input - bind-value="{{_account.name}}" - disabled="[[_saving]]"> + bind-value="{{_account.name}}"> <input is="iron-input" id="name" @@ -96,8 +97,7 @@ <section class$="[[_computeUsernameClass(_usernameMutable)]]"> <div class="title">Username</div> <iron-input - bind-value="{{_account.username}}" - disabled="[[_saving]]"> + bind-value="{{_account.username}}"> <input is="iron-input" id="username"
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js index 0633416..25499d6 100644 --- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js +++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
@@ -17,9 +17,15 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-registration-dialog', - + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrRegistrationDialog extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-registration-dialog'; } /** * Fired when account details are changed. * @@ -32,40 +38,39 @@ * @event close */ - properties: { - settingsUrl: String, - /** @type {?} */ - _account: { - type: Object, - value: () => { + static get properties() { + return { + settingsUrl: String, + /** @type {?} */ + _account: { + type: Object, + value: () => { // Prepopulate possibly undefined fields with values to trigger // computed bindings. - return {email: null, name: null, username: null}; + return {email: null, name: null, username: null}; + }, }, - }, - _usernameMutable: { - type: Boolean, - computed: '_computeUsernameMutable(_serverConfig, _account.username)', - }, - _loading: { - type: Boolean, - value: true, - observer: '_loadingChanged', - }, - _saving: { - type: Boolean, - value: false, - }, - _serverConfig: Object, - }, + _usernameMutable: { + type: Boolean, + computed: '_computeUsernameMutable(_serverConfig, _account.username)', + }, + _loading: { + type: Boolean, + value: true, + observer: '_loadingChanged', + }, + _saving: { + type: Boolean, + value: false, + }, + _serverConfig: Object, + }; + } - behaviors: [ - Gerrit.FireBehavior, - ], - - hostAttributes: { - role: 'dialog', - }, + ready() { + super.ready(); + this._ensureAttribute('role', 'dialog'); + } loadData() { this._loading = true; @@ -84,7 +89,7 @@ return Promise.all([loadAccount, loadConfig]).then(() => { this._loading = false; }); - }, + } _save() { this._saving = true; @@ -101,26 +106,26 @@ this._saving = false; this.fire('account-detail-update'); }); - }, + } _handleSave(e) { e.preventDefault(); this._save().then(this.close.bind(this)); - }, + } _handleClose(e) { e.preventDefault(); this.close(); - }, + } close() { this._saving = true; // disable buttons indefinitely this.fire('close'); - }, + } _computeSaveDisabled(name, email, saving) { return !name || !email || saving; - }, + } _computeUsernameMutable(config, username) { // Polymer 2: check for undefined @@ -133,14 +138,16 @@ return config.auth.editable_account_fields.includes('USER_NAME') && !username; - }, + } _computeUsernameClass(usernameMutable) { return usernameMutable ? '' : 'hide'; - }, + } _loadingChanged() { this.classList.toggle('loading', this._loading); - }, - }); + } + } + + customElements.define(GrRegistrationDialog.is, GrRegistrationDialog); })();
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html index d1b5c80..a987a26 100644 --- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html +++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-registration-dialog</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js index dae3b68..9702280 100644 --- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js +++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
@@ -17,12 +17,18 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-settings-item', + class GrSettingsItem extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-settings-item'; } - properties: { - anchor: String, - title: String, - }, - }); + static get properties() { + return { + anchor: String, + title: String, + }; + } + } + + customElements.define(GrSettingsItem.is, GrSettingsItem); })();
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.html index 846f776..c356e80 100644 --- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.html +++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.html
@@ -20,8 +20,12 @@ <dom-module id="gr-settings-menu-item"> <template> - <style include="shared-styles"></style> - <style include="gr-page-nav-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="gr-page-nav-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <div class="navStyles"> <li><a href$="[[href]]">[[title]]</a></li> </div>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js index 5db0031..e08e3d8 100644 --- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js +++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
@@ -17,12 +17,18 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-settings-menu-item', + class GrSettingsMenuItem extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-settings-menu-item'; } - properties: { - href: String, - title: String, - }, - }); + static get properties() { + return { + href: String, + title: String, + }; + } + } + + customElements.define(GrSettingsMenuItem.is, GrSettingsMenuItem); })();
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html index 74971cf..e71aef5 100644 --- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html +++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -74,9 +74,15 @@ margin-right: var(--spacing-l); } </style> - <style include="gr-form-styles"></style> - <style include="gr-menu-page-styles"></style> - <style include="gr-page-nav-styles"></style> + <style include="gr-form-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="gr-menu-page-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="gr-page-nav-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <div class="loading" hidden$="[[!_loading]]">Loading...</div> <div hidden$="[[_loading]]" hidden> <gr-page-nav class="navStyles"> @@ -131,7 +137,6 @@ <fieldset id="profile"> <gr-account-info id="accountInfo" - mutable="{{_accountNameMutable}}" has-unsaved-changes="{{_accountInfoChanged}}"></gr-account-info> <gr-button on-click="_handleSaveAccountInfo" @@ -373,7 +378,6 @@ class="newEmailInput" bind-value="{{_newEmail}}" type="text" - disabled="[[_addingEmail]]" on-keydown="_handleNewEmailKeydown" placeholder="email@example.com"> <input
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js index 714faab..0236261 100644 --- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js +++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -45,9 +45,19 @@ 'HTTP_LDAP', ]; - Polymer({ - is: 'gr-settings-view', - + /** + * @appliesMixin Gerrit.DocsUrlMixin + * @appliesMixin Gerrit.ChangeTableMixin + * @appliesMixin Gerrit.FireMixin + */ + class GrSettingsView extends Polymer.mixinBehaviors( [ + Gerrit.DocsUrlBehavior, + Gerrit.ChangeTableBehavior, + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-settings-view'; } /** * Fired when the title of the page should change. * @@ -60,104 +70,102 @@ * @event show-alert */ - properties: { - prefs: { - type: Object, - value() { return {}; }, - }, - params: { - type: Object, - value() { return {}; }, - }, - _accountNameMutable: Boolean, - _accountInfoChanged: Boolean, - _changeTableColumnsNotDisplayed: Array, - /** @type {?} */ - _localPrefs: { - type: Object, - value() { return {}; }, - }, - _localChangeTableColumns: { - type: Array, - value() { return []; }, - }, - _localMenu: { - type: Array, - value() { return []; }, - }, - _loading: { - type: Boolean, - value: true, - }, - _changeTableChanged: { - type: Boolean, - value: false, - }, - _prefsChanged: { - type: Boolean, - value: false, - }, - /** @type {?} */ - _diffPrefsChanged: Boolean, - /** @type {?} */ - _editPrefsChanged: Boolean, - _menuChanged: { - type: Boolean, - value: false, - }, - _watchedProjectsChanged: { - type: Boolean, - value: false, - }, - _keysChanged: { - type: Boolean, - value: false, - }, - _gpgKeysChanged: { - type: Boolean, - value: false, - }, - _newEmail: String, - _addingEmail: { - type: Boolean, - value: false, - }, - _lastSentVerificationEmail: { - type: String, - value: null, - }, - /** @type {?} */ - _serverConfig: Object, - /** @type {?string} */ - _docsBaseUrl: String, - _emailsChanged: Boolean, + static get properties() { + return { + prefs: { + type: Object, + value() { return {}; }, + }, + params: { + type: Object, + value() { return {}; }, + }, + _accountInfoChanged: Boolean, + _changeTableColumnsNotDisplayed: Array, + /** @type {?} */ + _localPrefs: { + type: Object, + value() { return {}; }, + }, + _localChangeTableColumns: { + type: Array, + value() { return []; }, + }, + _localMenu: { + type: Array, + value() { return []; }, + }, + _loading: { + type: Boolean, + value: true, + }, + _changeTableChanged: { + type: Boolean, + value: false, + }, + _prefsChanged: { + type: Boolean, + value: false, + }, + /** @type {?} */ + _diffPrefsChanged: Boolean, + /** @type {?} */ + _editPrefsChanged: Boolean, + _menuChanged: { + type: Boolean, + value: false, + }, + _watchedProjectsChanged: { + type: Boolean, + value: false, + }, + _keysChanged: { + type: Boolean, + value: false, + }, + _gpgKeysChanged: { + type: Boolean, + value: false, + }, + _newEmail: String, + _addingEmail: { + type: Boolean, + value: false, + }, + _lastSentVerificationEmail: { + type: String, + value: null, + }, + /** @type {?} */ + _serverConfig: Object, + /** @type {?string} */ + _docsBaseUrl: String, + _emailsChanged: Boolean, - /** - * For testing purposes. - */ - _loadingPromise: Object, + /** + * For testing purposes. + */ + _loadingPromise: Object, - _showNumber: Boolean, + _showNumber: Boolean, - _isDark: { - type: Boolean, - value: false, - }, - }, + _isDark: { + type: Boolean, + value: false, + }, + }; + } - behaviors: [ - Gerrit.DocsUrlBehavior, - Gerrit.ChangeTableBehavior, - Gerrit.FireBehavior, - ], - - observers: [ - '_handlePrefsChanged(_localPrefs.*)', - '_handleMenuChanged(_localMenu.splices)', - '_handleChangeTableChanged(_localChangeTableColumns, _showNumber)', - ], + static get observers() { + return [ + '_handlePrefsChanged(_localPrefs.*)', + '_handleMenuChanged(_localMenu.splices)', + '_handleChangeTableChanged(_localChangeTableColumns, _showNumber)', + ]; + } attached() { + super.attached(); // Polymer 2: anchor tag won't work on shadow DOM // we need to manually calling scrollIntoView when hash changed this.listen(window, 'location-change', '_handleLocationChange'); @@ -221,11 +229,12 @@ // Handle anchor tag for initial load this._handleLocationChange(); }); - }, + } detached() { + super.detached(); this.unlisten(window, 'location-change', '_handleLocationChange'); - }, + } _handleLocationChange() { // Handle anchor tag after dom attached @@ -237,25 +246,25 @@ elem.scrollIntoView(); } } - }, + } reloadAccountDetail() { Promise.all([ this.$.accountInfo.loadData(), this.$.emailEditor.loadData(), ]); - }, + } _isLoading() { return this._loading || this._loading === undefined; - }, + } _copyPrefs(to, from) { for (let i = 0; i < PREFS_SECTION_FIELDS.length; i++) { this.set([to, PREFS_SECTION_FIELDS[i]], this[from][PREFS_SECTION_FIELDS[i]]); } - }, + } _cloneMenu(prefs) { const menu = []; @@ -267,7 +276,7 @@ }); } this._localMenu = menu; - }, + } _cloneChangeTableColumns() { let columns = this.getVisibleColumns(this.prefs.change_table); @@ -280,56 +289,56 @@ this.prefs.change_table); } this._localChangeTableColumns = columns; - }, + } _formatChangeTableColumns(changeTableArray) { return changeTableArray.map(item => { return {column: item}; }); - }, + } _handleChangeTableChanged() { if (this._isLoading()) { return; } this._changeTableChanged = true; - }, + } _handlePrefsChanged(prefs) { if (this._isLoading()) { return; } this._prefsChanged = true; - }, + } _handleRelativeDateInChangeTable() { this.set('_localPrefs.relative_date_in_change_table', this.$.relativeDateInChangeTable.checked); - }, + } _handleShowSizeBarsInFileListChanged() { this.set('_localPrefs.size_bar_in_change_table', this.$.showSizeBarsInFileList.checked); - }, + } _handlePublishCommentsOnPushChanged() { this.set('_localPrefs.publish_comments_on_push', this.$.publishCommentsOnPush.checked); - }, + } _handleWorkInProgressByDefault() { this.set('_localPrefs.work_in_progress_by_default', this.$.workInProgressByDefault.checked); - }, + } _handleInsertSignedOff() { this.set('_localPrefs.signed_off_by', this.$.insertSignedOff.checked); - }, + } _handleMenuChanged() { if (this._isLoading()) { return; } this._menuChanged = true; - }, + } _handleSaveAccountInfo() { this.$.accountInfo.save(); - }, + } _handleSavePreferences() { this._copyPrefs('prefs', '_localPrefs'); @@ -337,7 +346,7 @@ return this.$.restAPI.savePreferences(this.prefs).then(() => { this._prefsChanged = false; }); - }, + } _handleSaveChangeTable() { this.set('prefs.change_table', this._localChangeTableColumns); @@ -346,15 +355,15 @@ return this.$.restAPI.savePreferences(this.prefs).then(() => { this._changeTableChanged = false; }); - }, + } _handleSaveDiffPreferences() { this.$.diffPrefs.save(); - }, + } _handleSaveEditPreferences() { this.$.editPrefs.save(); - }, + } _handleSaveMenu() { this.set('prefs.my', this._localMenu); @@ -362,7 +371,7 @@ return this.$.restAPI.savePreferences(this.prefs).then(() => { this._menuChanged = false; }); - }, + } _handleResetMenuButton() { return this.$.restAPI.getDefaultPreferences().then(data => { @@ -370,34 +379,34 @@ this._cloneMenu(data.my); } }); - }, + } _handleSaveWatchedProjects() { this.$.watchedProjectsEditor.save(); - }, + } _computeHeaderClass(changed) { return changed ? 'edited' : ''; - }, + } _handleSaveEmails() { this.$.emailEditor.save(); - }, + } _handleNewEmailKeydown(e) { if (e.keyCode === 13) { // Enter e.stopPropagation(); this._handleAddEmailButton(); } - }, + } _isNewEmailValid(newEmail) { return newEmail && newEmail.includes('@'); - }, + } _computeAddEmailButtonEnabled(newEmail, addingEmail) { return this._isNewEmailValid(newEmail) && !addingEmail; - }, + } _handleAddEmailButton() { if (!this._isNewEmailValid(this._newEmail)) { return; } @@ -412,7 +421,7 @@ this._lastSentVerificationEmail = this._newEmail; this._newEmail = ''; }); - }, + } _getFilterDocsLink(docsBaseUrl) { let base = docsBaseUrl; @@ -424,7 +433,7 @@ base = base.replace(TRAILING_SLASH_PATTERN, ''); return base + GERRIT_DOCS_FILTER_PATH; - }, + } _handleToggleDark() { if (this._isDark) { @@ -440,7 +449,7 @@ this.async(() => { window.location.reload(); }, 1); - }, + } _showHttpAuth(config) { if (config && config.auth && @@ -450,6 +459,8 @@ } return false; - }, - }); + } + } + + customElements.define(GrSettingsView.is, GrSettingsView); })();
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html index 6dcf124..24ee69b 100644 --- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html +++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-settings-view</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html index 2a27194..dd02ccd 100644 --- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html +++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html
@@ -26,7 +26,9 @@ <dom-module id="gr-ssh-editor"> <template> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="gr-form-styles"> .statusHeader { width: 4em;
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js index 874173a..6e17817 100644 --- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js +++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
@@ -17,33 +17,37 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-ssh-editor', + class GrSshEditor extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-ssh-editor'; } - properties: { - hasUnsavedChanges: { - type: Boolean, - value: false, - notify: true, - }, - _keys: Array, - /** @type {?} */ - _keyToView: Object, - _newKey: { - type: String, - value: '', - }, - _keysToRemove: { - type: Array, - value() { return []; }, - }, - }, + static get properties() { + return { + hasUnsavedChanges: { + type: Boolean, + value: false, + notify: true, + }, + _keys: Array, + /** @type {?} */ + _keyToView: Object, + _newKey: { + type: String, + value: '', + }, + _keysToRemove: { + type: Array, + value() { return []; }, + }, + }; + } loadData() { return this.$.restAPI.getAccountSSHKeys().then(keys => { this._keys = keys; }); - }, + } save() { const promises = this._keysToRemove.map(key => { @@ -54,22 +58,22 @@ this._keysToRemove = []; this.hasUnsavedChanges = false; }); - }, + } _getStatusLabel(isValid) { return isValid ? 'Valid' : 'Invalid'; - }, + } _showKey(e) { const el = Polymer.dom(e).localTarget; const index = parseInt(el.getAttribute('data-index'), 10); this._keyToView = this._keys[index]; this.$.viewKeyOverlay.open(); - }, + } _closeOverlay() { this.$.viewKeyOverlay.close(); - }, + } _handleDeleteKey(e) { const el = Polymer.dom(e).localTarget; @@ -77,7 +81,7 @@ this.push('_keysToRemove', this._keys[index]); this.splice('_keys', index, 1); this.hasUnsavedChanges = true; - }, + } _handleAddKey() { this.$.addButton.disabled = true; @@ -91,10 +95,12 @@ this.$.addButton.disabled = false; this.$.newKey.disabled = false; }); - }, + } _computeAddButtonDisabled(newKey) { return !newKey.length; - }, - }); + } + } + + customElements.define(GrSshEditor.is, GrSshEditor); })();
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html index d313f5a..912c1fd 100644 --- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html +++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-ssh-editor</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html index 360ea2d..b1ecb2e 100644 --- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html +++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
@@ -24,7 +24,9 @@ <dom-module id="gr-watched-projects-editor"> <template> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="gr-form-styles"> #watchedProjects .notifType { text-align: center;
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js index a40094d..f96aa76 100644 --- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js +++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
@@ -25,34 +25,38 @@ {name: 'Abandons', key: 'notify_abandoned_changes'}, ]; - Polymer({ - is: 'gr-watched-projects-editor', + class GrWatchedProjectsEditor extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-watched-projects-editor'; } - properties: { - hasUnsavedChanges: { - type: Boolean, - value: false, - notify: true, - }, - - _projects: Array, - _projectsToRemove: { - type: Array, - value() { return []; }, - }, - _query: { - type: Function, - value() { - return this._getProjectSuggestions.bind(this); + static get properties() { + return { + hasUnsavedChanges: { + type: Boolean, + value: false, + notify: true, }, - }, - }, + + _projects: Array, + _projectsToRemove: { + type: Array, + value() { return []; }, + }, + _query: { + type: Function, + value() { + return this._getProjectSuggestions.bind(this); + }, + }, + }; + } loadData() { return this.$.restAPI.getWatchedProjects().then(projs => { this._projects = projs; }); - }, + } save() { let deletePromise; @@ -72,19 +76,19 @@ this._projectsToRemove = []; this.hasUnsavedChanges = false; }); - }, + } _getTypes() { return NOTIFICATION_TYPES; - }, + } _getTypeCount() { return this._getTypes().length; - }, + } _computeCheckboxChecked(project, key) { return project.hasOwnProperty(key); - }, + } _getProjectSuggestions(input) { return this.$.restAPI.getSuggestedProjects(input) @@ -99,7 +103,7 @@ } return projects; }); - }, + } _handleRemoveProject(e) { const el = Polymer.dom(e).localTarget; @@ -108,7 +112,7 @@ this.splice('_projects', index, 1); this.push('_projectsToRemove', project); this.hasUnsavedChanges = true; - }, + } _canAddProject(project, text, filter) { if ((!project || !project.id) && !text) { return false; } @@ -126,7 +130,7 @@ } return true; - }, + } _getNewProjectIndex(name, filter) { let i; @@ -138,7 +142,7 @@ } } return i; - }, + } _handleAddProject() { const newProject = this.$.newProject.value; @@ -158,7 +162,7 @@ this.$.newProject.clear(); this.$.newFilter.bindValue = ''; this.hasUnsavedChanges = true; - }, + } _handleCheckboxChange(e) { const el = Polymer.dom(e).localTarget; @@ -167,11 +171,13 @@ const checked = el.checked; this.set(['_projects', index, key], !!checked); this.hasUnsavedChanges = true; - }, + } _handleNotifCellClick(e) { const checkbox = Polymer.dom(e.target).querySelector('input'); if (checkbox) { checkbox.click(); } - }, - }); + } + } + + customElements.define(GrWatchedProjectsEditor.is, GrWatchedProjectsEditor); })();
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html index 7a238ec..4bdb00f 100644 --- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html +++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-settings-view</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js index 10c876d..7a683dc 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js +++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
@@ -14,13 +14,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - (function() { 'use strict'; - Polymer({ - is: 'gr-account-chip', - + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrAccountChip extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-account-chip'; } /** * Fired to indicate a key was pressed while this chip was focused. * @@ -34,51 +39,52 @@ * @event remove */ - properties: { - account: Object, - additionalText: String, - disabled: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - removable: { - type: Boolean, - value: false, - }, - showAvatar: { - type: Boolean, - reflectToAttribute: true, - }, - transparentBackground: { - type: Boolean, - value: false, - }, - }, - - behaviors: [ - Gerrit.FireBehavior, - ], + static get properties() { + return { + account: Object, + additionalText: String, + disabled: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + removable: { + type: Boolean, + value: false, + }, + showAvatar: { + type: Boolean, + reflectToAttribute: true, + }, + transparentBackground: { + type: Boolean, + value: false, + }, + }; + } ready() { + super.ready(); this._getHasAvatars().then(hasAvatars => { this.showAvatar = hasAvatars; }); - }, + } _getBackgroundClass(transparent) { return transparent ? 'transparentBackground' : ''; - }, + } _handleRemoveTap(e) { e.preventDefault(); this.fire('remove', {account: this.account}); - }, + } _getHasAvatars() { return this.$.restAPI.getConfig().then(cfg => { return Promise.resolve(!!(cfg && cfg.plugin && cfg.plugin.has_avatars)); }); - }, - }); + } + } + + customElements.define(GrAccountChip.is, GrAccountChip); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.html b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.html index ae656fd..992ea8407 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.html +++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.html
@@ -40,7 +40,8 @@ on-commit="_handleInputCommit" clear-on-commit warn-uncommitted - text="{{_inputText}}"> + text="{{_inputText}}" + vertical-offset="24"> </gr-autocomplete> </template> <script src="gr-account-entry.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js index 92836a8..0ed3f19 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js +++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
@@ -21,9 +21,10 @@ * gr-account-entry is an element for entering account * and/or group with autocomplete support. */ - Polymer({ - is: 'gr-account-entry', - + class GrAccountEntry extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-account-entry'; } /** * Fired when an account is entered. * @@ -37,65 +38,70 @@ * * @event account-text-changed */ - properties: { - allowAnyInput: Boolean, - borderless: Boolean, - placeholder: String, - // suggestFrom = 0 to enable default suggestions. - suggestFrom: { - type: Number, - value: 0, - }, + static get properties() { + return { + allowAnyInput: Boolean, + borderless: Boolean, + placeholder: String, - /** @type {!function(string): !Promise<Array<{name, value}>>} */ - querySuggestions: { - type: Function, - notify: true, - value() { - return input => Promise.resolve([]); + // suggestFrom = 0 to enable default suggestions. + suggestFrom: { + type: Number, + value: 0, }, - }, - _config: Object, - /** The value of the autocomplete entry. */ - _inputText: { - type: String, - observer: '_inputTextChanged', - }, + /** @type {!function(string): !Promise<Array<{name, value}>>} */ + querySuggestions: { + type: Function, + notify: true, + value() { + return input => Promise.resolve([]); + }, + }, - }, + _config: Object, + /** The value of the autocomplete entry. */ + _inputText: { + type: String, + observer: '_inputTextChanged', + }, + + }; + } get focusStart() { return this.$.input.focusStart; - }, + } focus() { this.$.input.focus(); - }, + } clear() { this.$.input.clear(); - }, + } setText(text) { this.$.input.setText(text); - }, + } getText() { return this.$.input.text; - }, + } _handleInputCommit(e) { this.fire('add', {value: e.detail.value}); this.$.input.focus(); - }, + } _inputTextChanged(text) { if (text.length && this.allowAnyInput) { this.dispatchEvent(new CustomEvent( - 'account-text-changed', {bubbles: true, composed: true})); + 'account-text-changed', {bubbles: true, composed: true})); } - }, - }); + } + } + + customElements.define(GrAccountEntry.is, GrAccountEntry); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html index 59792a7..0bac7e0 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html +++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-account-entry</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -92,7 +92,7 @@ test('account-text-changed not fired when input text changed without ' + 'allowAnyInput', () => { - // Spy on query, as that is called when _updateSuggestions proceeds. + // Spy on query, as that is called when _updateSuggestions proceeds. const changeStub = sandbox.stub(); element.querySuggestions = input => Promise.resolve([]); element.addEventListener('account-text-changed', changeStub);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js index 418d2ea..1c0cb65 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js +++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
@@ -17,58 +17,65 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-account-label', + /** + * @appliesMixin Gerrit.DisplayNameMixin + * @appliesMixin Gerrit.TooltipMixin + */ + class GrAccountLabel extends Polymer.mixinBehaviors( [ + Gerrit.DisplayNameBehavior, + Gerrit.TooltipBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-account-label'; } - properties: { + static get properties() { + return { /** * @type {{ name: string, status: string }} */ - account: Object, - avatarImageSize: { - type: Number, - value: 32, - }, - title: { - type: String, - reflectToAttribute: true, - computed: '_computeAccountTitle(account, additionalText)', - }, - additionalText: String, - hasTooltip: { - type: Boolean, - reflectToAttribute: true, - computed: '_computeHasTooltip(account)', - }, - hideAvatar: { - type: Boolean, - value: false, - }, - _serverConfig: { - type: Object, - value: null, - }, - }, - - behaviors: [ - Gerrit.DisplayNameBehavior, - Gerrit.TooltipBehavior, - ], + account: Object, + avatarImageSize: { + type: Number, + value: 32, + }, + title: { + type: String, + reflectToAttribute: true, + computed: '_computeAccountTitle(account, additionalText)', + }, + additionalText: String, + hasTooltip: { + type: Boolean, + reflectToAttribute: true, + computed: '_computeHasTooltip(account)', + }, + hideAvatar: { + type: Boolean, + value: false, + }, + _serverConfig: { + type: Object, + value: null, + }, + }; + } ready() { + super.ready(); if (!this.additionalText) { this.additionalText = ''; } this.$.restAPI.getConfig() .then(config => { this._serverConfig = config; }); - }, + } _computeName(account, config) { return this.getUserName(config, account, false); - }, + } _computeStatusTextLength(account, config) { // 35 as the max length of the name + status return Math.max(10, 35 - this._computeName(account, config).length); - }, + } _computeAccountTitle(account, tooltip) { // Polymer 2: check for undefined @@ -98,12 +105,12 @@ } return result; - }, + } _computeShowEmailClass(account) { if (!account || account.name || !account.email) { return ''; } return 'showEmail'; - }, + } _computeEmailStr(account) { if (!account || !account.email) { @@ -113,11 +120,13 @@ return '(' + account.email + ')'; } return account.email; - }, + } _computeHasTooltip(account) { // If an account has loaded to fire this method, then set to true. return !!account; - }, - }); + } + } + + customElements.define(GrAccountLabel.is, GrAccountLabel); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html index ac34b44..f369ae2 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html +++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-account-label</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -64,19 +64,21 @@ }); test('computed fields', () => { - assert.equal(element._computeAccountTitle( - { + assert.equal( + element._computeAccountTitle({ name: 'Andrew Bonventre', email: 'andybons+gerrit@gmail.com', }, /* additionalText= */ ''), 'Andrew Bonventre <andybons+gerrit@gmail.com>'); - assert.equal(element._computeAccountTitle( - {name: 'Andrew Bonventre'}, /* additionalText= */ ''), + assert.equal( + element._computeAccountTitle({ + name: 'Andrew Bonventre', + }, /* additionalText= */ ''), 'Andrew Bonventre'); - assert.equal(element._computeAccountTitle( - { + assert.equal( + element._computeAccountTitle({ email: 'andybons+gerrit@gmail.com', }, /* additionalText= */ ''), 'Anonymous <andybons+gerrit@gmail.com>'); @@ -96,7 +98,7 @@ {name: 'Andrew Bonventre'}, /* additionalText= */ '' ), - ''); + ''); assert.equal(element._computeShowEmailClass(undefined), '');
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js index faaf9c3..f23959a 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js +++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
@@ -17,27 +17,34 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-account-link', + /** + * @appliesMixin Gerrit.BaseUrlMixin + */ + class GrAccountLink extends Polymer.mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-account-link'; } - properties: { - additionalText: String, - account: Object, - avatarImageSize: { - type: Number, - value: 32, - }, - }, - - behaviors: [ - Gerrit.BaseUrlBehavior, - ], + static get properties() { + return { + additionalText: String, + account: Object, + avatarImageSize: { + type: Number, + value: 32, + }, + }; + } _computeOwnerLink(account) { if (!account) { return; } return Gerrit.Nav.getUrlForOwner( account.email || account.username || account.name || account._account_id); - }, - }); + } + } + + customElements.define(GrAccountLink.is, GrAccountLink); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html index 134c579..e41304f 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html +++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-account-link</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.html b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.html index 2ce608be..6103d6f 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.html +++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.html
@@ -66,7 +66,6 @@ borderless hidden$="[[_computeEntryHidden(maxCount, accounts.*, readonly)]]" id="entry" - change="[[change]]" placeholder="[[placeholder]]" on-add="_handleAdd" on-input-keydown="_handleInputKeydown"
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js index 5f1d41a..e60782c 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js +++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
@@ -19,97 +19,119 @@ const VALID_EMAIL_ALERT = 'Please input a valid email.'; - Polymer({ - is: 'gr-account-list', - + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrAccountList extends Polymer.mixinBehaviors( [ + // Used in the tests for gr-account-list and other elements tests. + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-account-list'; } /** * Fired when user inputs an invalid email address. * * @event show-alert */ - properties: { - accounts: { - type: Array, - value() { return []; }, - notify: true, - }, - change: Object, - filter: Function, - placeholder: String, - disabled: { - type: Function, - value: false, - }, - - /** - * Returns suggestions and convert them to list item - * @type {Gerrit.GrSuggestionsProvider} - */ - suggestionsProvider: { - type: Object, - }, - - /** - * Needed for template checking since value is initially set to null. - * @type {?Object} */ - pendingConfirmation: { - type: Object, - value: null, - notify: true, - }, - readonly: { - type: Boolean, - value: false, - }, - /** - * When true, allows for non-suggested inputs to be added. - */ - allowAnyInput: { - type: Boolean, - value: false, - }, - - /** - * Array of values (groups/accounts) that are removable. When this prop is - * undefined, all values are removable. - */ - removableValues: Array, - maxCount: { - type: Number, - value: 0, - }, - - /** Returns suggestion items - * @type {!function(string): Promise<Array<Gerrit.GrSuggestionItem>>} - */ - _querySuggestions: { - type: Function, - value() { - return this._getSuggestions.bind(this); + static get properties() { + return { + accounts: { + type: Array, + value() { return []; }, + notify: true, }, - }, - }, + change: Object, + filter: Function, + placeholder: String, + disabled: { + type: Function, + value: false, + }, - behaviors: [ - // Used in the tests for gr-account-list and other elements tests. - Gerrit.FireBehavior, - ], + /** + * Returns suggestions and convert them to list item + * + * @type {Gerrit.GrSuggestionsProvider} + */ + suggestionsProvider: { + type: Object, + }, - listeners: { - remove: '_handleRemove', - }, + /** + * Needed for template checking since value is initially set to null. + * + * @type {?Object} + */ + pendingConfirmation: { + type: Object, + value: null, + notify: true, + }, + readonly: { + type: Boolean, + value: false, + }, + /** + * When true, allows for non-suggested inputs to be added. + */ + allowAnyInput: { + type: Boolean, + value: false, + }, + + /** + * Array of values (groups/accounts) that are removable. When this prop is + * undefined, all values are removable. + */ + removableValues: Array, + maxCount: { + type: Number, + value: 0, + }, + + /** + * Returns suggestion items + * + * @type {!function(string): Promise<Array<Gerrit.GrSuggestionItem>>} + */ + _querySuggestions: { + type: Function, + value() { + return this._getSuggestions.bind(this); + }, + }, + + /** + * Set to true to disable suggestions on empty input. + */ + skipSuggestOnEmpty: { + type: Boolean, + value: false, + }, + }; + } + + created() { + super.created(); + this.addEventListener('remove', + e => this._handleRemove(e)); + } get accountChips() { return Array.from( Polymer.dom(this.root).querySelectorAll('gr-account-chip')); - }, + } get focusStart() { return this.$.entry.focusStart; - }, + } _getSuggestions(input) { + if (this.skipSuggestOnEmpty && !input) { + return Promise.resolve([]); + } const provider = this.suggestionsProvider; if (!provider) { return Promise.resolve([]); @@ -120,13 +142,13 @@ suggestions = suggestions.filter(this.filter); } return suggestions.map(suggestion => - provider.makeSuggestionItem(suggestion)); + provider.makeSuggestionItem(suggestion)); }); - }, + } _handleAdd(e) { this._addAccountItem(e.detail.value); - }, + } _addAccountItem(item) { // Append new account or group to the accounts property. We add our own @@ -162,14 +184,14 @@ } this.pendingConfirmation = null; return true; - }, + } confirmGroup(group) { group = Object.assign( {}, group, {confirmed: true, _pendingAdd: true, _group: true}); this.push('accounts', group); this.pendingConfirmation = null; - }, + } _computeChipClass(account) { const classes = []; @@ -180,7 +202,7 @@ classes.push('pendingAdd'); } return classes.join(' '); - }, + } _accountMatches(a, b) { if (a && b) { @@ -192,7 +214,7 @@ } } return a === b; - }, + } _computeRemovable(account, readonly) { if (readonly) { return false; } @@ -205,13 +227,13 @@ return !!account._pendingAdd; } return true; - }, + } _handleRemove(e) { const toRemove = e.detail.account; this._removeAccount(toRemove); this.$.entry.focus(); - }, + } _removeAccount(toRemove) { if (!toRemove || !this._computeRemovable(toRemove, this.readonly)) { @@ -231,12 +253,12 @@ } } console.warn('received remove event for missing account', toRemove); - }, + } _getNativeInput(paperInput) { // In Polymer 2 inputElement isn't nativeInput anymore return paperInput.$.nativeInput || paperInput.inputElement; - }, + } _handleInputKeydown(e) { const input = this._getNativeInput(e.detail.input); @@ -254,7 +276,7 @@ } break; } - }, + } _handleChipKeydown(e) { const chip = e.target; @@ -292,7 +314,7 @@ } break; } - }, + } /** * Submit the text of the entry as a reviewer value, if it exists. If it is @@ -308,7 +330,7 @@ const wasSubmitted = this._addAccountItem(text); if (wasSubmitted) { this.$.entry.clear(); } return wasSubmitted; - }, + } additions() { return this.accounts.filter(account => { @@ -320,10 +342,12 @@ return {account}; } }); - }, + } _computeEntryHidden(maxCount, accountsRecord, readonly) { return (maxCount && maxCount <= accountsRecord.base.length) || readonly; - }, - }); + } + } + + customElements.define(GrAccountList.is, GrAccountList); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html index 6f265e7..7fea243 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html +++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-account-list</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -44,6 +44,7 @@ return item; } } + suite('gr-account-list tests', () => { let _nextAccountId = 0; const makeAccount = function() { @@ -186,7 +187,6 @@ }; }); - element._getSuggestions().then(suggestions => { // Default is no filtering. assert.equal(suggestions.length, 3); @@ -224,7 +224,6 @@ assert.isFalse(element._computeRemovable(existingAccount1, false)); assert.isTrue(element._computeRemovable(newAccount, false)); - element.removableValues = [existingAccount1]; assert.isTrue(element._computeRemovable(existingAccount1, false)); assert.isTrue(element._computeRemovable(newAccount, false)); @@ -383,6 +382,57 @@ }); }); + test('suggestion on empty', done => { + element.skipSuggestOnEmpty = false; + const suggestions = [ + { + email: 'abc@example.com', + text: 'abcd', + }, + { + email: 'qwe@example.com', + text: 'qwer', + }, + ]; + const getSuggestionsStub = + sandbox.stub(suggestionsProvider, 'getSuggestions') + .returns(Promise.resolve(suggestions)); + + const makeSuggestionItemStub = + sandbox.stub(suggestionsProvider, 'makeSuggestionItem', item => item); + + const input = element.$.entry.$.input; + + input.text = ''; + MockInteractions.focus(input.$.input); + input.noDebounce = true; + flushAsynchronousOperations(); + flush(() => { + assert.isTrue(getSuggestionsStub.calledOnce); + assert.equal(getSuggestionsStub.lastCall.args[0], ''); + assert.equal(makeSuggestionItemStub.getCalls().length, 2); + done(); + }); + }); + + test('skip suggestion on empty', done => { + element.skipSuggestOnEmpty = true; + const getSuggestionsStub = + sandbox.stub(suggestionsProvider, 'getSuggestions') + .returns(Promise.resolve([])); + + const input = element.$.entry.$.input; + + input.text = ''; + MockInteractions.focus(input.$.input); + input.noDebounce = true; + flushAsynchronousOperations(); + flush(() => { + assert.isTrue(getSuggestionsStub.notCalled); + done(); + }); + }); + suite('allowAnyInput', () => { setup(() => { element.allowAnyInput = true;
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js index e7c8b2c..433a57b 100644 --- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js +++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
@@ -17,46 +17,51 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-alert', - + class GrAlert extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-alert'; } /** * Fired when the action button is pressed. * * @event action */ - properties: { - text: String, - actionText: String, - shown: { - type: Boolean, - value: true, - readOnly: true, - reflectToAttribute: true, - }, - toast: { - type: Boolean, - value: true, - reflectToAttribute: true, - }, + static get properties() { + return { + text: String, + actionText: String, + shown: { + type: Boolean, + value: true, + readOnly: true, + reflectToAttribute: true, + }, + toast: { + type: Boolean, + value: true, + reflectToAttribute: true, + }, - _hideActionButton: Boolean, - _boundTransitionEndHandler: { - type: Function, - value() { return this._handleTransitionEnd.bind(this); }, - }, - _actionCallback: Function, - }, + _hideActionButton: Boolean, + _boundTransitionEndHandler: { + type: Function, + value() { return this._handleTransitionEnd.bind(this); }, + }, + _actionCallback: Function, + }; + } attached() { + super.attached(); this.addEventListener('transitionend', this._boundTransitionEndHandler); - }, + } detached() { + super.detached(); this.removeEventListener('transitionend', this._boundTransitionEndHandler); - }, + } show(text, opt_actionText, opt_actionCallback) { this.text = text; @@ -65,31 +70,33 @@ this._actionCallback = opt_actionCallback; Gerrit.getRootElement().appendChild(this); this._setShown(true); - }, + } hide() { this._setShown(false); if (this._hasZeroTransitionDuration()) { Gerrit.getRootElement().removeChild(this); } - }, + } _hasZeroTransitionDuration() { const style = window.getComputedStyle(this); // transitionDuration is always given in seconds. const duration = Math.round(parseFloat(style.transitionDuration) * 100); return duration === 0; - }, + } _handleTransitionEnd(e) { if (this.shown) { return; } Gerrit.getRootElement().removeChild(this); - }, + } _handleActionTap(e) { e.preventDefault(); if (this._actionCallback) { this._actionCallback(); } - }, - }); + } + } + + customElements.define(GrAlert.is, GrAlert); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html index 2338d55..bfcc431 100644 --- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html +++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-alert</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html index 9208068..cf32e28 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
@@ -22,6 +22,7 @@ <link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html"> <link rel="import" href="/bower_components/iron-fit-behavior/iron-fit-behavior.html"> <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html"> +<script src="../../../types/polymer-behaviors.js"></script> <script src="../../../scripts/rootElement.js"></script> <link rel="import" href="../../../styles/shared-styles.html"> @@ -59,6 +60,7 @@ .dropdown-content { background: var(--dropdown-background-color); box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px; + border-radius: var(--border-radius); max-height: 50vh; overflow: auto; }
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js index b8c76ff..f536666 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
@@ -17,9 +17,19 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-autocomplete-dropdown', - + /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @appliesMixin Polymer.IronFitMixin + */ + class GrAutocompleteDropdown extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + Polymer.IronFitBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-autocomplete-dropdown'; } /** * Fired when the dropdown is closed. * @@ -32,57 +42,55 @@ * @event item-selected */ - properties: { - index: Number, - isHidden: { - type: Boolean, - value: true, - reflectToAttribute: true, - }, - verticalOffset: { - type: Number, - value: null, - }, - horizontalOffset: { - type: Number, - value: null, - }, - suggestions: { - type: Array, - value: () => [], - observer: '_resetCursorStops', - }, - _suggestionEls: Array, - }, + static get properties() { + return { + index: Number, + isHidden: { + type: Boolean, + value: true, + reflectToAttribute: true, + }, + verticalOffset: { + type: Number, + value: null, + }, + horizontalOffset: { + type: Number, + value: null, + }, + suggestions: { + type: Array, + value: () => [], + observer: '_resetCursorStops', + }, + _suggestionEls: Array, + }; + } - behaviors: [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - Polymer.IronFitBehavior, - ], - - keyBindings: { - up: '_handleUp', - down: '_handleDown', - enter: '_handleEnter', - esc: '_handleEscape', - tab: '_handleTab', - }, + get keyBindings() { + return { + up: '_handleUp', + down: '_handleDown', + enter: '_handleEnter', + esc: '_handleEscape', + tab: '_handleTab', + }; + } close() { this.isHidden = true; - }, + } open() { this.isHidden = false; this._resetCursorStops(); // Refit should run after we call Polymer.flush inside _resetCursorStops this.refit(); - }, + } getCurrentText() { return this.getCursorTarget().dataset.value; - }, + } _handleUp(e) { if (!this.isHidden) { @@ -90,7 +98,7 @@ e.stopPropagation(); this.cursorUp(); } - }, + } _handleDown(e) { if (!this.isHidden) { @@ -98,19 +106,19 @@ e.stopPropagation(); this.cursorDown(); } - }, + } cursorDown() { if (!this.isHidden) { this.$.cursor.next(); } - }, + } cursorUp() { if (!this.isHidden) { this.$.cursor.previous(); } - }, + } _handleTab(e) { e.preventDefault(); @@ -119,7 +127,7 @@ trigger: 'tab', selected: this.$.cursor.target, }); - }, + } _handleEnter(e) { e.preventDefault(); @@ -128,12 +136,12 @@ trigger: 'enter', selected: this.$.cursor.target, }); - }, + } _handleEscape() { this._fireClose(); this.close(); - }, + } _handleClickItem(e) { e.preventDefault(); @@ -147,15 +155,15 @@ trigger: 'click', selected, }); - }, + } _fireClose() { this.fire('dropdown-closed'); - }, + } getCursorTarget() { return this.$.cursor.target; - }, + } _resetCursorStops() { if (this.suggestions.length > 0) { @@ -168,14 +176,16 @@ } else { this._suggestionEls = []; } - }, + } _resetCursorIndex() { this.$.cursor.setCursorAtIndex(0); - }, + } _computeLabelClass(item) { return item.label ? '' : 'hide'; - }, - }); + } + } + + customElements.define(GrAutocompleteDropdown.is, GrAutocompleteDropdown); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html index a7b59d7..5f2ce94 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-autocomplete-dropdown</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html index c9d12ce..e47b661 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -36,13 +36,14 @@ margin: 0 var(--spacing-xs); vertical-align: top; } - paper-input:not(.borderless) { - border: 1px solid var(--border-color); + paper-input.borderless { + border: none; + padding: 0; } paper-input { - height: var(--line-height-normal); - width: 100%; - @apply --gr-autocomplete; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: var(--spacing-s); --paper-input-container: { padding: 0; }; @@ -50,13 +51,25 @@ font-size: var(--font-size-normal); line-height: var(--line-height-normal); }; + /* This is a hack for not being able to set height:0 on the underline + of a paper-input 2.2.3 element. All the underline fixes below only + actually work in 3.x.x, so the height must be adjusted directly as + a workaround until we are on Polymer 3. */ + height: var(--line-height-normal); + --paper-input-container-underline-height: 0; + --paper-input-container-underline-wrapper-height: 0; + --paper-input-container-underline-focus-height: 0; + --paper-input-container-underline-legacy-height: 0; --paper-input-container-underline: { + height: 0; display: none; }; --paper-input-container-underline-focus: { + height: 0; display: none; }; --paper-input-container-underline-disabled: { + height: 0; display: none; }; }
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js index da16e2b..e2f42b3 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -20,9 +20,17 @@ const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g; const DEBOUNCE_WAIT_MS = 200; - Polymer({ - is: 'gr-autocomplete', - + /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + */ + class GrAutocomplete extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-autocomplete'; } /** * Fired when a value is chosen. * @@ -42,193 +50,199 @@ * @event input-keydown */ - properties: { + static get properties() { + return { - /** - * Query for requesting autocomplete suggestions. The function should - * accept the input as a string parameter and return a promise. The - * promise yields an array of suggestion objects with "name", "label", - * "value" properties. The "name" property will be displayed in the - * suggestion entry. The "label" property will, when specified, appear - * next to the "name" as label text. The "value" property will be emitted - * if that suggestion is selected. - * - * @type {function(string): Promise<?>} - */ - query: { - type: Function, - value() { - return function() { - return Promise.resolve([]); - }; + /** + * Query for requesting autocomplete suggestions. The function should + * accept the input as a string parameter and return a promise. The + * promise yields an array of suggestion objects with "name", "label", + * "value" properties. The "name" property will be displayed in the + * suggestion entry. The "label" property will, when specified, appear + * next to the "name" as label text. The "value" property will be emitted + * if that suggestion is selected. + * + * @type {function(string): Promise<?>} + */ + query: { + type: Function, + value() { + return function() { + return Promise.resolve([]); + }; + }, }, - }, - /** - * The number of characters that must be typed before suggestions are - * made. If threshold is zero, default suggestions are enabled. - */ - threshold: { - type: Number, - value: 1, - }, + /** + * The number of characters that must be typed before suggestions are + * made. If threshold is zero, default suggestions are enabled. + */ + threshold: { + type: Number, + value: 1, + }, - allowNonSuggestedValues: Boolean, - borderless: Boolean, - disabled: Boolean, - showSearchIcon: { - type: Boolean, - value: false, - }, - // Vertical offset needed for a 1em font-size with no vertical padding. - // Inputs with additional padding will need to increase vertical offset. - verticalOffset: { - type: Number, - value: 20, - }, + allowNonSuggestedValues: Boolean, + borderless: Boolean, + disabled: Boolean, + showSearchIcon: { + type: Boolean, + value: false, + }, + /** + * Vertical offset needed for an element with 20px line-height, 4px + * padding and 1px border (30px height total). Plus 1px spacing between + * input and dropdown. Inputs with different line-height or padding will + * need to tweak vertical offset. + */ + verticalOffset: { + type: Number, + value: 31, + }, - text: { - type: String, - value: '', - notify: true, - }, + text: { + type: String, + value: '', + notify: true, + }, - placeholder: String, + placeholder: String, - clearOnCommit: { - type: Boolean, - value: false, - }, + clearOnCommit: { + type: Boolean, + value: false, + }, - /** - * When true, tab key autocompletes but does not fire the commit event. - * When false, tab key not caught, and focus is removed from the element. - * See Issue 4556, Issue 6645. - */ - tabComplete: { - type: Boolean, - value: false, - }, + /** + * When true, tab key autocompletes but does not fire the commit event. + * When false, tab key not caught, and focus is removed from the element. + * See Issue 4556, Issue 6645. + */ + tabComplete: { + type: Boolean, + value: false, + }, - value: { - type: String, - notify: true, - }, + value: { + type: String, + notify: true, + }, - /** - * Multi mode appends autocompleted entries to the value. - * If false, autocompleted entries replace value. - */ - multi: { - type: Boolean, - value: false, - }, + /** + * Multi mode appends autocompleted entries to the value. + * If false, autocompleted entries replace value. + */ + multi: { + type: Boolean, + value: false, + }, - /** - * When true and uncommitted text is left in the autocomplete input after - * blurring, the text will appear red. - */ - warnUncommitted: { - type: Boolean, - value: false, - }, + /** + * When true and uncommitted text is left in the autocomplete input after + * blurring, the text will appear red. + */ + warnUncommitted: { + type: Boolean, + value: false, + }, - /** - * When true, querying for suggestions is not debounced w/r/t keypresses - */ - noDebounce: { - type: Boolean, - value: false, - }, + /** + * When true, querying for suggestions is not debounced w/r/t keypresses + */ + noDebounce: { + type: Boolean, + value: false, + }, - /** @type {?} */ - _suggestions: { - type: Array, - value() { return []; }, - }, + /** @type {?} */ + _suggestions: { + type: Array, + value() { return []; }, + }, - _suggestionEls: { - type: Array, - value() { return []; }, - }, + _suggestionEls: { + type: Array, + value() { return []; }, + }, - _index: Number, - _disableSuggestions: { - type: Boolean, - value: false, - }, - _focused: { - type: Boolean, - value: false, - }, + _index: Number, + _disableSuggestions: { + type: Boolean, + value: false, + }, + _focused: { + type: Boolean, + value: false, + }, - /** The DOM element of the selected suggestion. */ - _selected: Object, - }, + /** The DOM element of the selected suggestion. */ + _selected: Object, + }; + } - behaviors: [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - ], - - observers: [ - '_maybeOpenDropdown(_suggestions, _focused)', - '_updateSuggestions(text, threshold, noDebounce)', - ], + static get observers() { + return [ + '_maybeOpenDropdown(_suggestions, _focused)', + '_updateSuggestions(text, threshold, noDebounce)', + ]; + } get _nativeInput() { // In Polymer 2 inputElement isn't nativeInput anymore return this.$.input.$.nativeInput || this.$.input.inputElement; - }, + } attached() { + super.attached(); this.listen(document.body, 'click', '_handleBodyClick'); - }, + } detached() { + super.detached(); this.unlisten(document.body, 'click', '_handleBodyClick'); this.cancelDebouncer('update-suggestions'); - }, + } get focusStart() { return this.$.input; - }, + } focus() { this._nativeInput.focus(); - }, + } selectAll() { const nativeInputElement = this._nativeInput; if (!this.$.input.value) { return; } nativeInputElement.setSelectionRange(0, this.$.input.value.length); - }, + } clear() { this.text = ''; - }, + } _handleItemSelect(e) { // Let _handleKeydown deal with keyboard interaction. if (e.detail.trigger !== 'click') { return; } this._selected = e.detail.selected; this._commit(); - }, + } get _inputElement() { // Polymer2: this.$ can be undefined when this is first evaluated. return this.$ && this.$.input; - }, + } /** * Set the text of the input without triggering the suggestion dropdown. + * * @param {string} text The new text for the input. */ setText(text) { this._disableSuggestions = true; this.text = text; this._disableSuggestions = false; - }, + } _onInputFocus() { this._focused = true; @@ -236,14 +250,14 @@ this.$.input.classList.remove('warnUncommitted'); // Needed so that --paper-input-container-input updated style is applied. this.updateStyles(); - }, + } _onInputBlur() { this.$.input.classList.toggle('warnUncommitted', this.warnUncommitted && this.text.length && !this._focused); // Needed so that --paper-input-container-input updated style is applied. this.updateStyles(); - }, + } _updateSuggestions(text, threshold, noDebounce) { // Polymer 2: check for undefined @@ -251,13 +265,23 @@ return; } + // Reset _suggestions for every update + // This will also prevent from carrying over suggestions: + // @see Issue 12039 + this._suggestions = []; + + // TODO(taoalpha): Also skip if text has not changed + if (this._disableSuggestions) { return; } if (text.length < threshold) { - this._suggestions = []; this.value = ''; return; } + if (!this._focused) { + return; + } + const update = () => { this.query(text).then(suggestions => { if (text !== this.text) { @@ -280,18 +304,18 @@ } else { this.debounce('update-suggestions', update, DEBOUNCE_WAIT_MS); } - }, + } _maybeOpenDropdown(suggestions, focused) { if (suggestions.length > 0 && focused) { return this.$.suggestions.open(); } return this.$.suggestions.close(); - }, + } _computeClass(borderless) { return borderless ? 'borderless' : ''; - }, + } /** * _handleKeydown used for key handling in the this.$.input AND all child @@ -338,7 +362,7 @@ this._suggestions = []; } this.fire('input-keydown', {keyCode: e.keyCode, input: this.$.input}); - }, + } _cancel() { if (this._suggestions.length) { @@ -346,19 +370,19 @@ } else { this.fire('cancel'); } - }, + } /** * @param {boolean=} opt_tabComplete */ _handleInputCommit(opt_tabComplete) { // Nothing to do if the dropdown is not open. - if (!this.allowNonSuggestedValues - && this.$.suggestions.isHidden) { return; } + if (!this.allowNonSuggestedValues && + this.$.suggestions.isHidden) { return; } this._selected = this.$.suggestions.getCursorTarget(); this._commit(opt_tabComplete); - }, + } _updateValue(suggestion, suggestions) { if (!suggestion) { return; } @@ -372,7 +396,7 @@ } else { this.value = completed; } - }, + } _handleBodyClick(e) { const eventPath = Polymer.dom(e).path; @@ -382,13 +406,13 @@ } } this._focused = false; - }, + } _handleSuggestionTap(e) { e.stopPropagation(); this.$.cursor.setCursor(e.target); this._commit(); - }, + } /** * Commits the suggestion, optionally firing the commit event. @@ -424,10 +448,12 @@ } this._textChangedSinceCommit = false; - }, + } _computeShowSearchIconClass(showSearchIcon) { return showSearchIcon ? 'showSearchIcon' : ''; - }, - }); + } + } + + customElements.define(GrAutocomplete.is, GrAutocomplete); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html index ea1fd50..9df3cfc 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-reviewer-list</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -38,6 +38,10 @@ suite('gr-autocomplete tests', () => { let element; let sandbox; + const focusOnInput = element => { + MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null, + 'enter'); + }; setup(() => { element = fixture('basic'); @@ -63,6 +67,7 @@ assert.isTrue(element.$.suggestions.isHidden); assert.equal(element.$.suggestions.$.cursor.index, -1); + focusOnInput(element); element.text = 'blah'; assert.isTrue(queryStub.called); @@ -186,6 +191,7 @@ return promise = Promise.resolve([{name: 'suggestion', value: 0}]); }); element.query = queryStub; + focusOnInput(element); element.text = 'blah'; promise.then(() => { @@ -207,6 +213,7 @@ return promise = Promise.resolve([{name: 'suggestion', value: 0}]); }); element.query = queryStub; + focusOnInput(element); element.text = 'blah'; element.clearOnCommit = true; @@ -228,15 +235,11 @@ return Promise.resolve([]); }); element.query = queryStub; - element.threshold = 2; - + focusOnInput(element); element.text = 'a'; - assert.isFalse(queryStub.called); - element.text = 'ab'; - assert.isTrue(queryStub.called); }); @@ -249,6 +252,7 @@ (name, cb) => { callback = cb; }); element.query = queryStub; element.noDebounce = false; + focusOnInput(element); element.text = 'a'; assert.isFalse(queryStub.called); assert.isTrue(debounceStub.called); @@ -269,11 +273,60 @@ assert.equal(element._suggestions.length, 0); }); + test('when focused', done => { + let promise; + const queryStub = sandbox.stub() + .returns(promise = Promise.resolve([{name: 'suggestion', value: 0}])); + element.query = queryStub; + element.suggestOnlyWhenFocus = true; + focusOnInput(element); + element.text = 'bla'; + assert.equal(element._focused, true); + flushAsynchronousOperations(); + promise.then(() => { + assert.equal(element._suggestions.length, 1); + assert.equal(queryStub.notCalled, false); + done(); + }); + }); + + test('when not focused', done => { + let promise; + const queryStub = sandbox.stub() + .returns(promise = Promise.resolve([{name: 'suggestion', value: 0}])); + element.query = queryStub; + element.suggestOnlyWhenFocus = true; + element.text = 'bla'; + assert.equal(element._focused, false); + flushAsynchronousOperations(); + promise.then(() => { + assert.equal(element._suggestions.length, 0); + done(); + }); + }); + + test('suggestions should not carry over', done => { + let promise; + const queryStub = sandbox.stub() + .returns(promise = Promise.resolve([{name: 'suggestion', value: 0}])); + element.query = queryStub; + focusOnInput(element); + element.text = 'bla'; + flushAsynchronousOperations(); + promise.then(() => { + assert.equal(element._suggestions.length, 1); + element._updateSuggestions('', 0, false); + assert.equal(element._suggestions.length, 0); + done(); + }); + }); + test('multi completes only the last part of the query', done => { let promise; const queryStub = sandbox.stub() .returns(promise = Promise.resolve([{name: 'suggestion', value: 0}])); element.query = queryStub; + focusOnInput(element); element.text = 'blah blah'; element.multi = true; @@ -335,7 +388,7 @@ }); test('vertical offset overridden by param if it exists', () => { - assert.equal(element.$.suggestions.verticalOffset, 20); + assert.equal(element.$.suggestions.verticalOffset, 31); element.verticalOffset = 30; assert.equal(element.$.suggestions.verticalOffset, 30); });
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js index bf56382..4238eed 100644 --- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js +++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -17,29 +17,35 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-avatar', + /** + * @appliesMixin Gerrit.BaseUrlMixin + */ + class GrAvatar extends Polymer.mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-avatar'; } - properties: { - account: { - type: Object, - observer: '_accountChanged', - }, - imageSize: { - type: Number, - value: 16, - }, - _hasAvatars: { - type: Boolean, - value: false, - }, - }, - - behaviors: [ - Gerrit.BaseUrlBehavior, - ], + static get properties() { + return { + account: { + type: Object, + observer: '_accountChanged', + }, + imageSize: { + type: Number, + value: 16, + }, + _hasAvatars: { + type: Boolean, + value: false, + }, + }; + } attached() { + super.attached(); Promise.all([ this._getConfig(), Gerrit.awaitPluginsLoaded(), @@ -48,15 +54,15 @@ this._updateAvatarURL(); }); - }, + } _getConfig() { return this.$.restAPI.getConfig(); - }, + } _accountChanged(account) { this._updateAvatarURL(); - }, + } _updateAvatarURL() { if (!this._hasAvatars || !this.account) { @@ -69,12 +75,12 @@ if (url) { this.style.backgroundImage = 'url("' + url + '")'; } - }, + } _getAccounts(account) { return account._account_id || account.email || account.username || account.name; - }, + } _buildAvatarURL(account) { if (!account) { return ''; } @@ -87,6 +93,8 @@ return this.getBaseUrl() + '/accounts/' + encodeURIComponent(this._getAccounts(account)) + '/avatar?s=' + this.imageSize; - }, - }); + } + } + + customElements.define(GrAvatar.is, GrAvatar); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html index 3456c13..98a75dd 100644 --- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html +++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-avatar</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -49,28 +49,28 @@ }); test('methods', () => { - assert.equal(element._buildAvatarURL( - { + assert.equal( + element._buildAvatarURL({ _account_id: 123, }), '/accounts/123/avatar?s=16'); - assert.equal(element._buildAvatarURL( - { + assert.equal( + element._buildAvatarURL({ email: 'test@example.com', }), '/accounts/test%40example.com/avatar?s=16'); - assert.equal(element._buildAvatarURL( - { + assert.equal( + element._buildAvatarURL({ name: 'John Doe', }), '/accounts/John%20Doe/avatar?s=16'); - assert.equal(element._buildAvatarURL( - { + assert.equal( + element._buildAvatarURL({ username: 'John_Doe', }), '/accounts/John_Doe/avatar?s=16'); - assert.equal(element._buildAvatarURL( - { + assert.equal( + element._buildAvatarURL({ _account_id: 123, avatars: [ { @@ -88,8 +88,8 @@ ], }), 'https://cdn.example.com/s16-p/photo.jpg'); - assert.equal(element._buildAvatarURL( - { + assert.equal( + element._buildAvatarURL({ _account_id: 123, avatars: [ {
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html index 87caf64..fd75c7c 100644 --- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html +++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -116,13 +116,6 @@ :host([link][primary]) { --text-color: var(--primary-button-background-color); } - :host([secondary]) { - --background-color: var(--secondary-button-text-color); - --text-color: var(--secondary-button-background-color); - } - :host([link][secondary]) { - --text-color: var(--secondary-button-text-color); - } /* Keep below color definition for primary so that this takes precedence when disabled. */
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js index afc6ba8..75efcdc 100644 --- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js +++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -17,65 +17,77 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-button', + /** + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @appliesMixin Gerrit.TooltipMixin + */ + class GrButton extends Polymer.mixinBehaviors( [ + Gerrit.KeyboardShortcutBehavior, + Gerrit.TooltipBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-button'; } - properties: { - tooltip: String, - downArrow: { - type: Boolean, - reflectToAttribute: true, - }, - link: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - loading: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - disabled: { - type: Boolean, - observer: '_disabledChanged', - reflectToAttribute: true, - }, - noUppercase: { - type: Boolean, - value: false, - }, - _enabledTabindex: { - type: String, - value: '0', - }, - }, + static get properties() { + return { + tooltip: String, + downArrow: { + type: Boolean, + reflectToAttribute: true, + }, + link: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + loading: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + disabled: { + type: Boolean, + observer: '_disabledChanged', + reflectToAttribute: true, + }, + noUppercase: { + type: Boolean, + value: false, + }, + _enabledTabindex: { + type: String, + value: '0', + }, + }; + } - listeners: { - click: '_handleAction', - keydown: '_handleKeydown', - }, + static get observers() { + return [ + '_computeDisabled(disabled, loading)', + ]; + } - observers: [ - '_computeDisabled(disabled, loading)', - ], + created() { + super.created(); + this.addEventListener('click', + e => this._handleAction(e)); + this.addEventListener('keydown', + e => this._handleKeydown(e)); + } - behaviors: [ - Gerrit.KeyboardShortcutBehavior, - Gerrit.TooltipBehavior, - ], - - hostAttributes: { - role: 'button', - tabindex: '0', - }, + ready() { + super.ready(); + this._ensureAttribute('role', 'button'); + this._ensureAttribute('tabindex', '0'); + } _handleAction(e) { if (this.disabled) { e.preventDefault(); e.stopImmediatePropagation(); } - }, + } _disabledChanged(disabled) { if (disabled) { @@ -83,11 +95,11 @@ } this.setAttribute('tabindex', disabled ? '-1' : this._enabledTabindex); this.updateStyles(); - }, + } _computeDisabled(disabled, loading) { return disabled || loading; - }, + } _handleKeydown(e) { if (this.modifierPressed(e)) { return; } @@ -98,6 +110,8 @@ e.stopPropagation(); this.click(); } - }, - }); + } + } + + customElements.define(GrButton.is, GrButton); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html index ef593c0..bcb560a 100644 --- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html +++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-button</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js index a83bc2b..05e759f 100644 --- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js +++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
@@ -17,31 +17,34 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-change-star', - + class GrChangeStar extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-change-star'; } /** * Fired when star state is toggled. * * @event toggle-star */ - properties: { + static get properties() { + return { /** @type {?} */ - change: { - type: Object, - notify: true, - }, - }, + change: { + type: Object, + notify: true, + }, + }; + } _computeStarClass(starred) { return starred ? 'active' : ''; - }, + } _computeStarIcon(starred) { // Hollow star is used to indicate inactive state. return `gr-icons:star${starred ? '' : '-border'}`; - }, + } toggleStar() { const newVal = !this.change.starred; @@ -51,6 +54,8 @@ composed: true, detail: {change: this.change, starred: newVal}, })); - }, - }); + } + } + + customElements.define(GrChangeStar.is, GrChangeStar); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html index 7ee22a7..60c504a 100644 --- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html +++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-star</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js index e6f52c6..35b8ef8 100644 --- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js +++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
@@ -36,35 +36,39 @@ const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' + 'current reviewers (or anyone with "View Private Changes" permission).'; - Polymer({ - is: 'gr-change-status', + class GrChangeStatus extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-change-status'; } - properties: { - flat: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - status: { - type: String, - observer: '_updateChipDetails', - }, - tooltipText: { - type: String, - value: '', - }, - }, + static get properties() { + return { + flat: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + status: { + type: String, + observer: '_updateChipDetails', + }, + tooltipText: { + type: String, + value: '', + }, + }; + } _computeStatusString(status) { if (status === ChangeStates.WIP && !this.flat) { return 'Work in Progress'; } return status; - }, + } _toClassName(str) { return str.toLowerCase().replace(/\s/g, '-'); - }, + } _updateChipDetails(status, previousStatus) { if (previousStatus) { @@ -86,6 +90,8 @@ this.tooltipText = ''; break; } - }, - }); + } + } + + customElements.define(GrChangeStatus.is, GrChangeStatus); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html index 421c6ab5..96a1840 100644 --- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html +++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-status</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html index bbd7ddf..d0a07b6 100644 --- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html +++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html
@@ -58,6 +58,9 @@ #container.unresolved { background-color: var(--unresolved-comment-background-color); } + #container.robotComment { + background-color: var(--robot-comment-background-color); + } #commentInfoContainer { border-top: 1px dotted var(--border-color); display: flex; @@ -82,19 +85,19 @@ <span class="descriptionText">Patchset [[patchNum]]</span> </div> </template> - <div id="container" class$="[[_computeHostClass(unresolved)]]"> + <div id="container" class$="[[_computeHostClass(unresolved, isRobotComment)]]"> <template id="commentList" is="dom-repeat" items="[[_orderedComments]]" as="comment"> <gr-comment comment="{{comment}}" - robot-button-disabled="[[_hideActions(_showActions, _lastComment)]]" + comments="{{comments}}" + robot-button-disabled="[[_shouldDisableAction(_showActions, _lastComment)]]" change-num="[[changeNum]]" patch-num="[[patchNum]]" draft="[[_isDraft(comment)]]" show-actions="[[_showActions]]" comment-side="[[comment.__commentSide]]" side="[[comment.side]]" - root-id="[[rootId]]" project-config="[[_projectConfig]]" on-create-fix-comment="_handleCommentFix" on-comment-discard="_handleCommentDiscard" @@ -107,25 +110,21 @@ <gr-button id="replyBtn" link - secondary class="action reply" on-click="_handleCommentReply">Reply</gr-button> <gr-button id="quoteBtn" link - secondary class="action quote" on-click="_handleCommentQuote">Quote</gr-button> <gr-button id="ackBtn" link - secondary class="action ack" on-click="_handleCommentAck">Ack</gr-button> <gr-button id="doneBtn" link - secondary class="action done" on-click="_handleCommentDone">Done</gr-button> </div>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js index b7fe8b0..a1165ca 100644 --- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js +++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
@@ -20,9 +20,22 @@ const UNRESOLVED_EXPAND_COUNT = 5; const NEWLINE_PATTERN = /\n/g; - Polymer({ - is: 'gr-comment-thread', - + /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @appliesMixin Gerrit.PathListMixin + */ + class GrCommentThread extends Polymer.mixinBehaviors( [ + /** + * Not used in this element rather other elements tests + */ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + Gerrit.PathListBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-comment-thread'; } /** * Fired when the thread should be discarded. * @@ -35,123 +48,128 @@ * @event thread-changed */ - /** - * gr-comment-thread exposes the following attributes that allow a - * diff widget like gr-diff to show the thread in the right location: - * - * line-num: - * 1-based line number or undefined if it refers to the entire file. - * - * comment-side: - * "left" or "right". These indicate which of the two diffed versions - * the comment relates to. In the case of unified diff, the left - * version is the one whose line number column is further to the left. - * - * range: - * The range of text that the comment refers to (start_line, - * start_character, end_line, end_character), serialized as JSON. If - * set, range's end_line will have the same value as line-num. Line - * numbers are 1-based, char numbers are 0-based. The start position - * (start_line, start_character) is inclusive, and the end position - * (end_line, end_character) is exclusive. - */ - properties: { - changeNum: String, - comments: { - type: Array, - value() { return []; }, - }, - /** - * @type {?{start_line: number, start_character: number, end_line: number, - * end_character: number}} - */ - range: { - type: Object, - reflectToAttribute: true, - }, - keyEventTarget: { - type: Object, - value() { return document.body; }, - }, - commentSide: { - type: String, - reflectToAttribute: true, - }, - patchNum: String, - path: String, - projectName: { - type: String, - observer: '_projectNameChanged', - }, - hasDraft: { - type: Boolean, - notify: true, - reflectToAttribute: true, - }, - isOnParent: { - type: Boolean, - value: false, - }, - parentIndex: { - type: Number, - value: null, - }, - rootId: { - type: String, - notify: true, - computed: '_computeRootId(comments.*)', - }, - /** - * If this is true, the comment thread also needs to have the change and - * line properties property set - */ - showFilePath: { - type: Boolean, - value: false, - }, - /** Necessary only if showFilePath is true or when used with gr-diff */ - lineNum: { - type: Number, - reflectToAttribute: true, - }, - unresolved: { - type: Boolean, - notify: true, - reflectToAttribute: true, - }, - _showActions: Boolean, - _lastComment: Object, - _orderedComments: Array, - _projectConfig: Object, - }, + /** + * gr-comment-thread exposes the following attributes that allow a + * diff widget like gr-diff to show the thread in the right location: + * + * line-num: + * 1-based line number or undefined if it refers to the entire file. + * + * comment-side: + * "left" or "right". These indicate which of the two diffed versions + * the comment relates to. In the case of unified diff, the left + * version is the one whose line number column is further to the left. + * + * range: + * The range of text that the comment refers to (start_line, + * start_character, end_line, end_character), serialized as JSON. If + * set, range's end_line will have the same value as line-num. Line + * numbers are 1-based, char numbers are 0-based. The start position + * (start_line, start_character) is inclusive, and the end position + * (end_line, end_character) is exclusive. + */ + static get properties() { + return { + changeNum: String, + comments: { + type: Array, + value() { return []; }, + }, + /** + * @type {?{start_line: number, start_character: number, end_line: number, + * end_character: number}} + */ + range: { + type: Object, + reflectToAttribute: true, + }, + keyEventTarget: { + type: Object, + value() { return document.body; }, + }, + commentSide: { + type: String, + reflectToAttribute: true, + }, + patchNum: String, + path: String, + projectName: { + type: String, + observer: '_projectNameChanged', + }, + hasDraft: { + type: Boolean, + notify: true, + reflectToAttribute: true, + }, + isOnParent: { + type: Boolean, + value: false, + }, + parentIndex: { + type: Number, + value: null, + }, + rootId: { + type: String, + notify: true, + computed: '_computeRootId(comments.*)', + }, + /** + * If this is true, the comment thread also needs to have the change and + * line properties property set + */ + showFilePath: { + type: Boolean, + value: false, + }, + /** Necessary only if showFilePath is true or when used with gr-diff */ + lineNum: { + type: Number, + reflectToAttribute: true, + }, + unresolved: { + type: Boolean, + notify: true, + reflectToAttribute: true, + }, + _showActions: Boolean, + _lastComment: Object, + _orderedComments: Array, + _projectConfig: Object, + isRobotComment: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + }; + } - behaviors: [ - /** - * Not used in this element rather other elements tests - */ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - Gerrit.PathListBehavior, - ], + static get observers() { + return [ + '_commentsChanged(comments.*)', + ]; + } - listeners: { - 'comment-update': '_handleCommentUpdate', - }, + get keyBindings() { + return { + 'e shift+e': '_handleEKey', + }; + } - observers: [ - '_commentsChanged(comments.*)', - ], - - keyBindings: { - 'e shift+e': '_handleEKey', - }, + created() { + super.created(); + this.addEventListener('comment-update', + e => this._handleCommentUpdate(e)); + } attached() { + super.attached(); this._getLoggedIn().then(loggedIn => { this._showActions = loggedIn; }); this._setInitialExpandedState(); - }, + } addOrEditDraft(opt_lineNum, opt_range) { const lastComment = this.comments[this.comments.length - 1] || {}; @@ -165,60 +183,65 @@ commentEl.collapsed = false; } else { const range = opt_range ? opt_range : - lastComment ? lastComment.range : undefined; + lastComment ? lastComment.range : undefined; const unresolved = lastComment ? lastComment.unresolved : undefined; this.addDraft(opt_lineNum, range, unresolved); } - }, + } addDraft(opt_lineNum, opt_range, opt_unresolved) { const draft = this._newDraft(opt_lineNum, opt_range); draft.__editing = true; draft.unresolved = opt_unresolved === false ? opt_unresolved : true; this.push('comments', draft); - }, + } fireRemoveSelf() { this.dispatchEvent(new CustomEvent('thread-discard', {detail: {rootId: this.rootId}, bubbles: false})); - }, + } _getDiffUrlForComment(projectName, changeNum, path, patchNum) { return Gerrit.Nav.getUrlForDiffById(changeNum, projectName, path, patchNum, null, this.lineNum); - }, + } _computeDisplayPath(path) { const lineString = this.lineNum ? `#${this.lineNum}` : ''; return this.computeDisplayPath(path) + lineString; - }, + } _getLoggedIn() { return this.$.restAPI.getLoggedIn(); - }, + } _commentsChanged() { this._orderedComments = this._sortedComments(this.comments); this.updateThreadProperties(); - }, + } updateThreadProperties() { if (this._orderedComments.length) { this._lastComment = this._getLastComment(); this.unresolved = this._lastComment.unresolved; this.hasDraft = this._lastComment.__draft; + this.isRobotComment = !!(this._lastComment.robot_id); } - }, + } + + _shouldDisableAction(_showActions, _lastComment) { + return !_showActions || !_lastComment || !!_lastComment.__draft; + } _hideActions(_showActions, _lastComment) { - return !_showActions || !_lastComment || !!_lastComment.__draft || + return this._shouldDisableAction(_showActions, _lastComment) || !!_lastComment.robot_id; - }, + } _getLastComment() { return this._orderedComments[this._orderedComments.length - 1] || {}; - }, + } _handleEKey(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } @@ -231,7 +254,7 @@ if (this.modifierPressed(e)) { return; } this._expandCollapseComments(false); } - }, + } _expandCollapseComments(actionIsCollapse) { const comments = @@ -239,7 +262,7 @@ for (const comment of comments) { comment.collapsed = actionIsCollapse; } - }, + } /** * Sets the initial state of the comment thread. @@ -259,7 +282,7 @@ comment.collapsed = !isRobotComment && resolvedThread; } } - }, + } _sortedComments(comments) { return comments.slice().sort((c1, c2) => { @@ -275,7 +298,7 @@ // If same date, fall back to sorting by id. return dateCompare ? dateCompare : c1.id.localeCompare(c2.id); }); - }, + } _createReplyComment(parent, content, opt_isEditing, opt_unresolved) { @@ -309,11 +332,11 @@ commentEl.save(); }, 1); } - }, + } _isDraft(comment) { return !!comment.__draft; - }, + } /** * @param {boolean=} opt_quote @@ -326,33 +349,33 @@ quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n'; } this._createReplyComment(comment, quoteStr, true, comment.unresolved); - }, + } _handleCommentReply(e) { this._processCommentReply(); - }, + } _handleCommentQuote(e) { this._processCommentReply(true); - }, + } _handleCommentAck(e) { const comment = this._lastComment; this._createReplyComment(comment, 'Ack', false, false); - }, + } _handleCommentDone(e) { const comment = this._lastComment; this._createReplyComment(comment, 'Done', false, false); - }, + } _handleCommentFix(e) { const comment = e.detail.comment; const msg = comment.message; const quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n'; - const response = quoteStr + 'Please Fix'; + const response = quoteStr + 'Please fix.'; this._createReplyComment(comment, response, false, true); - }, + } _commentElWithDraftID(id) { const els = Polymer.dom(this.root).querySelectorAll('gr-comment'); @@ -362,7 +385,7 @@ } } return null; - }, + } _newReply(inReplyTo, opt_lineNum, opt_message, opt_unresolved, opt_range) { @@ -376,7 +399,7 @@ d.unresolved = opt_unresolved; } return d; - }, + } /** * @param {number=} opt_lineNum @@ -402,12 +425,12 @@ d.parent = this.parentIndex; } return d; - }, + } _getSide(isOnParent) { if (isOnParent) { return 'PARENT'; } return 'REVISION'; - }, + } _computeRootId(comments) { // Keep the root ID even if the comment was removed, so that notification @@ -415,7 +438,7 @@ if (!comments.base.length) { return this.rootId; } const rootComment = comments.base[0]; return rootComment.id || rootComment.__draftID; - }, + } _handleCommentDiscard(e) { const diffCommentEl = Polymer.dom(e).rootTarget; @@ -445,13 +468,13 @@ changeComment.message); } } - }, + } _handleCommentSavedOrDiscarded(e) { this.dispatchEvent(new CustomEvent('thread-changed', {detail: {rootId: this.rootId, path: this.path}, bubbles: false})); - }, + } _handleCommentUpdate(e) { const comment = e.detail.comment; @@ -467,7 +490,7 @@ // observers, the this.set() call above will not cause a thread update in // some situations. this.updateThreadProperties(); - }, + } _indexOf(comment, arr) { for (let i = 0; i < arr.length; i++) { @@ -478,14 +501,18 @@ } } return -1; - }, + } _computeHostClass(unresolved) { + if (this.isRobotComment) { + return 'robotComment'; + } return unresolved ? 'unresolved' : ''; - }, + } /** * Load the project config when a project name has been provided. + * * @param {string} name The project name. */ _projectNameChanged(name) { @@ -493,6 +520,8 @@ this.$.restAPI.getProjectConfig(name).then(config => { this._projectConfig = config; }); - }, - }); + } + } + + customElements.define(GrCommentThread.is, GrCommentThread); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html index b6222b4..ceadae1 100644 --- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html +++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-comment-thread</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -152,6 +152,24 @@ assert.isTrue(addDraftStub.called); }); + test('_shouldDisableAction', () => { + let showActions = true; + const lastComment = {}; + assert.equal( + element._shouldDisableAction(showActions, lastComment), false); + showActions = false; + assert.equal( + element._shouldDisableAction(showActions, lastComment), true); + showActions = true; + lastComment.__draft = true; + assert.equal( + element._shouldDisableAction(showActions, lastComment), true); + const robotComment = {}; + robotComment.robot_id = true; + assert.equal( + element._shouldDisableAction(showActions, robotComment), false); + }); + test('_hideActions', () => { let showActions = true; const lastComment = {}; @@ -402,7 +420,7 @@ }); assert.equal(drafts.length, 1); assert.equal( - drafts[0].message, '> is this a crossover episode!?\n\nPlease Fix'); + drafts[0].message, '> is this a crossover episode!?\n\nPlease fix.'); assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); assert.isTrue(drafts[0].unresolved); done(); @@ -469,7 +487,7 @@ done(); }); draftEl.fire('comment-discard', {comment: draftEl.comment}, - {bubbles: false}); + {bubbles: false}); }); test('first editing comment does not add __otherEditing attribute', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html index 8ad261c..a96cf44 100644 --- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html +++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
@@ -60,7 +60,7 @@ display: none; } .header { - align-items: baseline; + align-items: center; cursor: pointer; display: flex; margin: calc(0px - var(--spacing-m)) calc(0px - var(--spacing-m)) 0 calc(0px - var(--spacing-m)); @@ -119,7 +119,6 @@ --gr-button: { height: 20px; padding: 0 var(--spacing-s); - color: var(--default-button-text-color); } } .editMessage { @@ -247,7 +246,7 @@ <div id="container" class="container"> <div class="header" id="header" on-click="_handleToggleCollapsed"> <div class="headerLeft"> - <span class="authorName">[[comment.author.name]]</span> + <span class="authorName">[[_computeAuthorName(comment)]]</span> <span class="draftLabel">DRAFT</span> <gr-tooltip-content class="draftTooltip" has-tooltip @@ -258,13 +257,19 @@ <div class="headerMiddle"> <span class="collapsedContent">[[comment.message]]</span> </div> + <div hidden$="[[_computeHideRunDetails(comment, collapsed)]]" class="runIdMessage message"> + <div class="runIdInformation"> + <a class="robotRunLink" href$="[[comment.url]]"> + <span class="robotRun link">Run Details</span> + </a> + </div> + </div> <gr-button id="deleteBtn" link - secondary class$="action delete [[_computeDeleteButtonClass(_isAdmin, draft)]]" on-click="_handleCommentDelete"> - (Delete) + <iron-icon id="icon" icon="gr-icons:delete"></iron-icon> </gr-button> <span class="date" on-click="_handleAnchorClick"> <gr-date-formatter @@ -284,10 +289,9 @@ </div> </div> <div class="body"> - <template is="dom-if" if="[[comment.robot_id]]"> + <template is="dom-if" if="[[isRobotComment]]"> <div class="robotId" hidden$="[[collapsed]]"> - <iron-icon class="robotIcon" icon="gr-icons:robot"></iron-icon> - [[comment.robot_id]] + [[comment.author.name]] </div> </template> <template is="dom-if" if="[[editing]]"> @@ -305,21 +309,7 @@ <gr-formatted-text class="message" content="[[comment.message]]" no-trailing-margin="[[!comment.__draft]]" - collapsed="[[collapsed]]" config="[[projectConfig.commentlinks]]"></gr-formatted-text> - <div hidden$="[[!comment.robot_run_id]]" class="message"> - <div class="runIdInformation" hidden$="[[collapsed]]"> - Run ID: - <template is="dom-if" if="[[comment.url]]"> - <a class="robotRunLink" href$="[[comment.url]]"> - <span class="robotRun link">[[comment.robot_run_id]]</span> - </a> - </template> - <template is="dom-if" if="[[!comment.url]]"> - <span class="robotRun text">[[comment.robot_run_id]]</span> - </template> - </div> - </div> <div class="actions humanActions" hidden$="[[!_showHumanActions]]"> <div class="action resolve hideOnPublished"> <label> @@ -333,22 +323,18 @@ <div class="rightActions"> <gr-button link - secondary class="action cancel hideOnPublished" on-click="_handleCancel">Cancel</gr-button> <gr-button link - secondary class="action discard hideOnPublished" on-click="_handleDiscard">Discard</gr-button> <gr-button link - secondary class="action edit hideOnPublished" on-click="_handleEdit">Edit</gr-button> <gr-button link - secondary disabled$="[[_computeSaveDisabled(_messageText, comment, resolved)]]" class="action save hideOnPublished" on-click="_handleSave">Save</gr-button> @@ -356,13 +342,23 @@ </div> <div class="robotActions" hidden$="[[!_showRobotActions]]"> <template is="dom-if" if="[[isRobotComment]]"> + <template is="dom-if" if="[[!_hasHumanReply]]"> + <gr-button + link + class="action fix" + on-click="_handleFix" + disabled="[[robotButtonDisabled]]"> + Please Fix + </gr-button> + </template> <gr-button link secondary - class="action fix" - on-click="_handleFix" + class="action show-fix" + hidden$="[[_hasNoFix(comment)]]" + on-click="_handleShowFix" disabled="[[robotButtonDisabled]]"> - Please Fix + Show Fix </gr-button> <gr-endpoint-decorator name="robot-comment-controls"> <gr-endpoint-param name="comment" value="[[comment]]">
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js index d41ef7f..0c45c85 100644 --- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js +++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
@@ -31,9 +31,17 @@ const FILE = 'FILE'; - Polymer({ - is: 'gr-comment', - + /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + */ + class GrComment extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-comment'; } /** * Fired when the create fix comment action is triggered. * @@ -41,6 +49,12 @@ */ /** + * Fired when the show fix preview action is triggered. + * + * @event open-fix-preview + */ + + /** * Fired when this comment is discarded. * * @event comment-discard @@ -64,108 +78,116 @@ * @event comment-anchor-tap */ - properties: { - changeNum: String, - /** @type {?} */ - comment: { - type: Object, - notify: true, - observer: '_commentChanged', - }, - isRobotComment: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - disabled: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - draft: { - type: Boolean, - value: false, - observer: '_draftChanged', - }, - editing: { - type: Boolean, - value: false, - observer: '_editingChanged', - }, - discarding: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - hasChildren: Boolean, - patchNum: String, - showActions: Boolean, - _showHumanActions: Boolean, - _showRobotActions: Boolean, - collapsed: { - type: Boolean, - value: true, - observer: '_toggleCollapseClass', - }, - /** @type {?} */ - projectConfig: Object, - robotButtonDisabled: Boolean, - _isAdmin: { - type: Boolean, - value: false, - }, + static get properties() { + return { + changeNum: String, + /** @type {!Gerrit.Comment} */ + comment: { + type: Object, + notify: true, + observer: '_commentChanged', + }, + comments: { + type: Array, + }, + isRobotComment: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + disabled: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + draft: { + type: Boolean, + value: false, + observer: '_draftChanged', + }, + editing: { + type: Boolean, + value: false, + observer: '_editingChanged', + }, + discarding: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + hasChildren: Boolean, + patchNum: String, + showActions: Boolean, + _showHumanActions: Boolean, + _showRobotActions: Boolean, + collapsed: { + type: Boolean, + value: true, + observer: '_toggleCollapseClass', + }, + /** @type {?} */ + projectConfig: Object, + robotButtonDisabled: Boolean, + _hasHumanReply: Boolean, + _isAdmin: { + type: Boolean, + value: false, + }, - _xhrPromise: Object, // Used for testing. - _messageText: { - type: String, - value: '', - observer: '_messageTextChanged', - }, - commentSide: String, + _xhrPromise: Object, // Used for testing. + _messageText: { + type: String, + value: '', + observer: '_messageTextChanged', + }, + commentSide: String, + side: String, - resolved: Boolean, + resolved: Boolean, - _numPendingDraftRequests: { - type: Object, - value: + _numPendingDraftRequests: { + type: Object, + value: {number: 0}, // Intentional to share the object across instances. - }, + }, - _enableOverlay: { - type: Boolean, - value: false, - }, + _enableOverlay: { + type: Boolean, + value: false, + }, - /** - * Property for storing references to overlay elements. When the overlays - * are moved to Gerrit.getRootElement() to be shown they are no-longer - * children, so they can't be queried along the tree, so they are stored - * here. - */ - _overlays: { - type: Object, - value: () => ({}), - }, - }, + /** + * Property for storing references to overlay elements. When the overlays + * are moved to Gerrit.getRootElement() to be shown they are no-longer + * children, so they can't be queried along the tree, so they are stored + * here. + */ + _overlays: { + type: Object, + value: () => ({}), + }, + }; + } - observers: [ - '_commentMessageChanged(comment.message)', - '_loadLocalDraft(changeNum, patchNum, comment)', - '_isRobotComment(comment)', - '_calculateActionstoShow(showActions, isRobotComment)', - ], + static get observers() { + return [ + '_commentMessageChanged(comment.message)', + '_loadLocalDraft(changeNum, patchNum, comment)', + '_isRobotComment(comment)', + '_calculateActionstoShow(showActions, isRobotComment)', + '_computeHasHumanReply(comment, comments.*)', + ]; + } - behaviors: [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - ], - - keyBindings: { - 'ctrl+enter meta+enter ctrl+s meta+s': '_handleSaveKey', - 'esc': '_handleEsc', - }, + get keyBindings() { + return { + 'ctrl+enter meta+enter ctrl+s meta+s': '_handleSaveKey', + 'esc': '_handleEsc', + }; + } attached() { + super.attached(); if (this.editing) { this.collapsed = false; } else if (this.comment) { @@ -174,40 +196,43 @@ this._getIsAdmin().then(isAdmin => { this._isAdmin = isAdmin; }); - }, + } detached() { + super.detached(); this.cancelDebouncer('fire-update'); if (this.textarea) { this.textarea.closeDropdown(); } - }, + } get textarea() { - return this.$$('#editTextarea'); - }, + return this.shadowRoot.querySelector('#editTextarea'); + } get confirmDeleteOverlay() { if (!this._overlays.confirmDelete) { this._enableOverlay = true; Polymer.dom.flush(); - this._overlays.confirmDelete = this.$$('#confirmDeleteOverlay'); + this._overlays.confirmDelete = this.shadowRoot + .querySelector('#confirmDeleteOverlay'); } return this._overlays.confirmDelete; - }, + } get confirmDiscardOverlay() { if (!this._overlays.confirmDiscard) { this._enableOverlay = true; Polymer.dom.flush(); - this._overlays.confirmDiscard = this.$$('#confirmDiscardOverlay'); + this._overlays.confirmDiscard = this.shadowRoot + .querySelector('#confirmDiscardOverlay'); } return this._overlays.confirmDiscard; - }, + } _computeShowHideIcon(collapsed) { return collapsed ? 'gr-icons:expand-more' : 'gr-icons:expand-less'; - }, + } _calculateActionstoShow(showActions, isRobotComment) { // Polymer 2: check for undefined @@ -217,19 +242,19 @@ this._showHumanActions = showActions && !isRobotComment; this._showRobotActions = showActions && isRobotComment; - }, + } _isRobotComment(comment) { this.isRobotComment = !!comment.robot_id; - }, + } isOnParent() { return this.side === 'PARENT'; - }, + } _getIsAdmin() { return this.$.restAPI.getIsAdmin(); - }, + } /** * @param {*=} opt_comment @@ -272,7 +297,7 @@ }); return this._xhrPromise; - }, + } _eraseDraftComment() { // Prevents a race condition in which removing the draft comment occurs @@ -286,7 +311,7 @@ line: this.comment.line, range: this.comment.range, }); - }, + } _commentChanged(comment) { this.editing = !!comment.__editing; @@ -294,7 +319,16 @@ if (this.editing) { // It's a new draft/reply, notify. this._fireUpdate(); } - }, + } + + _computeHasHumanReply() { + if (!this.comment || !this.comments) return; + // hide please fix button for robot comment that has human reply + this._hasHumanReply = this.comments.some(c => { + return c.in_reply_to && c.in_reply_to === this.comment.id && + !c.robot_id; + }); + } /** * @param {!Object=} opt_mixin @@ -306,21 +340,21 @@ comment: this.comment, patchNum: this.patchNum, }); - }, + } _fireSave() { this.fire('comment-save', this._getEventPayload()); - }, + } _fireUpdate() { this.debounce('fire-update', () => { this.fire('comment-update', this._getEventPayload()); }); - }, + } _draftChanged(draft) { this.$.container.classList.toggle('draft', draft); - }, + } _editingChanged(editing, previousValue) { // Polymer 2: observer fires when at least one property is defined. @@ -342,14 +376,14 @@ if (editing) { this.async(() => { Polymer.dom.flush(); - this.textarea.putCursorAtEnd(); + this.textarea && this.textarea.putCursorAtEnd(); }, 1); } - }, + } _computeDeleteButtonClass(isAdmin, draft) { return isAdmin && !draft ? 'showDeleteButtons' : ''; - }, + } _computeSaveDisabled(draft, comment, resolved) { // If resolved state has changed and a msg exists, save should be enabled. @@ -357,7 +391,7 @@ return false; } return !draft || draft.trim() === ''; - }, + } _handleSaveKey(e) { if (!this._computeSaveDisabled(this._messageText, this.comment, @@ -365,18 +399,18 @@ e.preventDefault(); this._handleSave(e); } - }, + } _handleEsc(e) { if (!this._messageText.length) { e.preventDefault(); this._handleCancel(e); } - }, + } _handleToggleCollapsed() { this.collapsed = !this.collapsed; - }, + } _toggleCollapseClass(collapsed) { if (collapsed) { @@ -384,11 +418,11 @@ } else { this.$.container.classList.remove('collapsed'); } - }, + } _commentMessageChanged(message) { this._messageText = message || ''; - }, + } _messageTextChanged(newValue, oldValue) { if (!this.comment || (this.comment && this.comment.id)) { @@ -413,7 +447,7 @@ this.$.storage.setDraftComment(commentLocation, message); } }, STORAGE_DEBOUNCE_INTERVAL); - }, + } _handleAnchorClick(e) { e.preventDefault(); @@ -428,14 +462,14 @@ side: this.side, }, })); - }, + } _handleEdit(e) { e.preventDefault(); this._messageText = this.comment.message; this.editing = true; this.$.reporting.recordDraftInteraction(); - }, + } _handleSave(e) { e.preventDefault(); @@ -445,11 +479,11 @@ return; } const timingLabel = this.comment.id ? - REPORT_UPDATE_DRAFT : REPORT_CREATE_DRAFT; + REPORT_UPDATE_DRAFT : REPORT_CREATE_DRAFT; const timer = this.$.reporting.getTimer(timingLabel); this.set('comment.__editing', false); return this.save().then(() => { timer.end(); }); - }, + } _handleCancel(e) { e.preventDefault(); @@ -462,12 +496,12 @@ } this._messageText = this.comment.message; this.editing = false; - }, + } _fireDiscard() { this.cancelDebouncer('fire-update'); this.fire('comment-discard', this._getEventPayload()); - }, + } _handleFix() { this.dispatchEvent(new CustomEvent('create-fix-comment', { @@ -475,7 +509,19 @@ composed: true, detail: this._getEventPayload(), })); - }, + } + + _handleShowFix() { + this.dispatchEvent(new CustomEvent('open-fix-preview', { + bubbles: true, + composed: true, + detail: this._getEventPayload(), + })); + } + + _hasNoFix(comment) { + return !comment || !comment.fix_suggestions; + } _handleDiscard(e) { e.preventDefault(); @@ -490,14 +536,14 @@ this.confirmDiscardOverlay.querySelector('#confirmDiscardDialog') .resetFocus(); }); - }, + } _handleConfirmDiscard(e) { e.preventDefault(); const timer = this.$.reporting.getTimer(REPORT_DISCARD_DRAFT); this._closeConfirmDiscardOverlay(); return this._discardDraft().then(() => { timer.end(); }); - }, + } _discardDraft() { if (!this.comment.__draft) { @@ -528,11 +574,11 @@ }); return this._xhrPromise; - }, + } _closeConfirmDiscardOverlay() { this._closeOverlay(this.confirmDiscardOverlay); - }, + } _getSavingMessage(numPending) { if (numPending === 0) { @@ -543,17 +589,17 @@ numPending, numPending === 1 ? DRAFT_SINGULAR : DRAFT_PLURAL, ].join(' '); - }, + } _showStartRequest() { const numPending = ++this._numPendingDraftRequests.number; this._updateRequestToast(numPending); - }, + } _showEndRequest() { const numPending = --this._numPendingDraftRequests.number; this._updateRequestToast(numPending); - }, + } _handleFailedDraftRequest() { this._numPendingDraftRequests.number--; @@ -561,7 +607,7 @@ // Cancel the debouncer so that error toasts from the error-manager will // not be overridden. this.cancelDebouncer('draft-toast'); - }, + } _updateRequestToast(numPending) { const message = this._getSavingMessage(numPending); @@ -572,7 +618,7 @@ document.body.dispatchEvent(new CustomEvent( 'show-alert', {detail: {message}, bubbles: true, composed: true})); }, TOAST_DEBOUNCE_INTERVAL); - }, + } _saveDraft(draft) { this._showStartRequest(); @@ -585,24 +631,24 @@ } return result; }); - }, + } _deleteDraft(draft) { this._showStartRequest(); return this.$.restAPI.deleteDiffDraft(this.changeNum, this.patchNum, draft).then(result => { - if (result.ok) { - this._showEndRequest(); - } else { - this._handleFailedDraftRequest(); - } - return result; - }); - }, + if (result.ok) { + this._showEndRequest(); + } else { + this._handleFailedDraftRequest(); + } + return result; + }); + } _getPatchNum() { return this.isOnParent() ? 'PARENT' : this.patchNum; - }, + } _loadLocalDraft(changeNum, patchNum, comment) { // Polymer 2: check for undefined @@ -631,7 +677,7 @@ if (draft) { this.set('comment.message', draft.message); } - }, + } _handleToggleResolved() { this.$.reporting.recordDraftInteraction(); @@ -645,25 +691,38 @@ // Save the resolved state immediately. this.save(payload.comment); } - }, + } _handleCommentDelete() { this._openOverlay(this.confirmDeleteOverlay); - }, + } _handleCancelDeleteComment() { this._closeOverlay(this.confirmDeleteOverlay); - }, + } _openOverlay(overlay) { Polymer.dom(Gerrit.getRootElement()).appendChild(overlay); return overlay.open(); - }, + } + + _computeAuthorName(comment) { + if (!comment) return ''; + if (comment.robot_id) { + return comment.robot_id; + } + return comment.author && comment.author.name; + } + + _computeHideRunDetails(comment, collapsed) { + if (!comment) return true; + return !(comment.robot_id && comment.url && !collapsed); + } _closeOverlay(overlay) { Polymer.dom(Gerrit.getRootElement()).removeChild(overlay); overlay.close(); - }, + } _handleConfirmDeleteComment() { const dialog = @@ -674,6 +733,8 @@ this._handleCancelDeleteComment(); this.comment = newComment; }); - }, - }); + } + } + + customElements.define(GrComment.is, GrComment); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html index c829343..ae8f3f6 100644 --- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html +++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-comment</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -44,7 +44,6 @@ </test-fixture> <script> - function isVisible(el) { assert.ok(el); return getComputedStyle(el).getPropertyValue('display') !== 'none'; @@ -457,17 +456,13 @@ element.editing = false; element.collapsed = false; flushAsynchronousOperations(); - assert.isNotOk(element.$$('.robotRun.link')); - assert.notEqual(getComputedStyle(element.$$('.robotRun.text')).display, - 'none'); + assert.isTrue(element.$$('.robotRun.link').textContent === 'Run Details'); // A robot comment with run ID and url should display a link. element.set(['comment', 'url'], '/path/to/run'); flushAsynchronousOperations(); assert.notEqual(getComputedStyle(element.$$('.robotRun.link')).display, 'none'); - assert.equal(getComputedStyle(element.$$('.robotRun.text')).display, - 'none'); }); test('collapsible drafts', () => { @@ -528,6 +523,37 @@ 'header middle content is not visible'); }); + test('robot comment layout', () => { + const comment = Object.assign({ + robot_id: 'happy_robot_id', + url: '/robot/comment', + author: { + name: 'Happy Robot', + }, + }, element.comment); + element.comment = comment; + element.collapsed = false; + flushAsynchronousOperations(); + + let runIdMessage; + runIdMessage = element.$$('.runIdMessage'); + assert.isFalse(runIdMessage.hidden); + + const runDetailsLink = element.$$('.robotRunLink'); + assert.isTrue(runDetailsLink.href.indexOf(element.comment.url) !== -1); + + const robotServiceName = element.$$('.authorName'); + assert.isTrue(robotServiceName.textContent === 'happy_robot_id'); + + const authorName = element.$$('.robotId'); + assert.isTrue(authorName.innerText === 'Happy Robot'); + + element.collapsed = true; + flushAsynchronousOperations(); + runIdMessage = element.$$('.runIdMessage'); + assert.isTrue(runIdMessage.hidden); + }); + test('draft creation/cancellation', done => { assert.isFalse(element.editing); MockInteractions.tap(element.$$('.edit')); @@ -841,9 +867,157 @@ done(); }); element.isRobotComment = true; + element.comments = [element.comment]; flushAsynchronousOperations(); MockInteractions.tap(element.$$('.fix')); }); + + test('do not show Please Fix button if human reply exists', () => { + element.comments = [ + { + robot_id: 'happy_robot_id', + robot_run_id: '5838406743490560', + fix_suggestions: [ + { + fix_id: '478ff847_3bf47aaf', + description: 'Make the smiley happier by giving it a nose.', + replacements: [ + { + path: 'Documentation/config-gerrit.txt', + range: { + start_line: 10, + start_character: 7, + end_line: 10, + end_character: 9, + }, + replacement: ':-)', + }, + ], + }, + ], + author: { + _account_id: 1030912, + name: 'Alice Kober-Sotzek', + email: 'aliceks@google.com', + avatars: [ + { + url: '/s32-p/photo.jpg', + height: 32, + }, + { + url: '/AaAdOFzPlFI/s56-p/photo.jpg', + height: 56, + }, + { + url: '/AaAdOFzPlFI/s100-p/photo.jpg', + height: 100, + }, + { + url: '/AaAdOFzPlFI/s120-p/photo.jpg', + height: 120, + }, + ], + }, + patch_set: 1, + id: 'eb0d03fd_5e95904f', + line: 10, + updated: '2017-04-04 15:36:17.000000000', + message: 'This is a robot comment with a fix.', + unresolved: false, + __commentSide: 'right', + collapsed: false, + }, + { + __draft: true, + __draftID: '0.wbrfbwj89sa', + __date: '2019-12-04T13:41:03.689Z', + path: 'Documentation/config-gerrit.txt', + patchNum: 1, + side: 'REVISION', + __commentSide: 'right', + line: 10, + in_reply_to: 'eb0d03fd_5e95904f', + message: '> This is a robot comment with a fix.\n\nPlease fix.', + unresolved: true, + }, + ]; + element.comment = element.comments[0]; + flushAsynchronousOperations(); + assert.isNull(element.$$('robotActions gr-button')); + }); + + test('show Please Fix if no human reply', () => { + element.comments = [ + { + robot_id: 'happy_robot_id', + robot_run_id: '5838406743490560', + fix_suggestions: [ + { + fix_id: '478ff847_3bf47aaf', + description: 'Make the smiley happier by giving it a nose.', + replacements: [ + { + path: 'Documentation/config-gerrit.txt', + range: { + start_line: 10, + start_character: 7, + end_line: 10, + end_character: 9, + }, + replacement: ':-)', + }, + ], + }, + ], + author: { + _account_id: 1030912, + name: 'Alice Kober-Sotzek', + email: 'aliceks@google.com', + avatars: [ + { + url: '/s32-p/photo.jpg', + height: 32, + }, + { + url: '/AaAdOFzPlFI/s56-p/photo.jpg', + height: 56, + }, + { + url: '/AaAdOFzPlFI/s100-p/photo.jpg', + height: 100, + }, + { + url: '/AaAdOFzPlFI/s120-p/photo.jpg', + height: 120, + }, + ], + }, + patch_set: 1, + id: 'eb0d03fd_5e95904f', + line: 10, + updated: '2017-04-04 15:36:17.000000000', + message: 'This is a robot comment with a fix.', + unresolved: false, + __commentSide: 'right', + collapsed: false, + }, + ]; + element.comment = element.comments[0]; + flushAsynchronousOperations(); + assert.isNotNull(element.$$('.robotActions gr-button')); + }); + + test('_handleShowFix fires open-fix-preview event', done => { + element.addEventListener('open-fix-preview', e => { + assert.deepEqual(e.detail, element._getEventPayload()); + done(); + }); + element.comment = {fix_suggestions: [{}]}; + element.isRobotComment = true; + flushAsynchronousOperations(); + + MockInteractions.tap(element.$$('.show-fix')); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js index c2075f0..6a4232f 100644 --- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js +++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
@@ -17,9 +17,15 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-confirm-delete-comment-dialog', - + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrConfirmDeleteCommentDialog extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-confirm-delete-comment-dialog'; } /** * Fired when the confirm button is pressed. * @@ -32,28 +38,29 @@ * @event cancel */ - properties: { - message: String, - }, - - behaviors: [ - Gerrit.FireBehavior, - ], + static get properties() { + return { + message: String, + }; + } resetFocus() { this.$.messageInput.textarea.focus(); - }, + } _handleConfirmTap(e) { e.preventDefault(); e.stopPropagation(); this.fire('confirm', {reason: this.message}, {bubbles: false}); - }, + } _handleCancelTap(e) { e.preventDefault(); e.stopPropagation(); this.fire('cancel', null, {bubbles: false}); - }, - }); + } + } + + customElements.define(GrConfirmDeleteCommentDialog.is, + GrConfirmDeleteCommentDialog); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js index 3e87202..6c062f6 100644 --- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js +++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
@@ -19,34 +19,38 @@ const COPY_TIMEOUT_MS = 1000; - Polymer({ - is: 'gr-copy-clipboard', + class GrCopyClipboard extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-copy-clipboard'; } - properties: { - text: String, - buttonTitle: String, - hasTooltip: { - type: Boolean, - value: false, - }, - hideInput: { - type: Boolean, - value: false, - }, - }, + static get properties() { + return { + text: String, + buttonTitle: String, + hasTooltip: { + type: Boolean, + value: false, + }, + hideInput: { + type: Boolean, + value: false, + }, + }; + } focusOnCopy() { this.$.button.focus(); - }, + } _computeInputClass(hideInput) { return hideInput ? 'hideInput' : ''; - }, + } _handleInputClick(e) { e.preventDefault(); Polymer.dom(e).rootTarget.select(); - }, + } _copyToClipboard() { if (this.hideInput) { @@ -62,6 +66,8 @@ this.async( () => this.$.icon.icon = 'gr-icons:content-copy', COPY_TIMEOUT_MS); - }, - }); + } + } + + customElements.define(GrCopyClipboard.is, GrCopyClipboard); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html index 9cec20e..14490f0 100644 --- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html +++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-copy-clipboard</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html index d061ac2..5022766 100644 --- a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html +++ b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-count-string-formatter</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js index be40ce6..04724df 100644 --- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js +++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
@@ -22,75 +22,83 @@ KEEP_VISIBLE: 'keep-visible', }; - Polymer({ - is: 'gr-cursor-manager', + class GrCursorManager extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-cursor-manager'; } - properties: { - stops: { - type: Array, - value() { - return []; + static get properties() { + return { + stops: { + type: Array, + value() { + return []; + }, + observer: '_updateIndex', }, - observer: '_updateIndex', - }, - /** - * @type (?Object) - */ - target: { - type: Object, - notify: true, - observer: '_scrollToTarget', - }, - /** - * The height of content intended to be included with the target. - * @type (?number) - */ - _targetHeight: Number, + /** + * @type {?Object} + */ + target: { + type: Object, + notify: true, + observer: '_scrollToTarget', + }, + /** + * The height of content intended to be included with the target. + * + * @type {?number} + */ + _targetHeight: Number, - /** - * The index of the current target (if any). -1 otherwise. - */ - index: { - type: Number, - value: -1, - notify: true, - }, + /** + * The index of the current target (if any). -1 otherwise. + */ + index: { + type: Number, + value: -1, + notify: true, + }, - /** - * The class to apply to the current target. Use null for no class. - */ - cursorTargetClass: { - type: String, - value: null, - }, + /** + * The class to apply to the current target. Use null for no class. + */ + cursorTargetClass: { + type: String, + value: null, + }, - /** - * The scroll behavior for the cursor. Values are 'never' and - * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond - * the viewport. - * TODO (beckysiegel) figure out why it can be undefined - * @type (string|undefined) - */ - scrollBehavior: { - type: String, - value: ScrollBehavior.NEVER, - }, + /** + * The scroll behavior for the cursor. Values are 'never' and + * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond + * the viewport. + * TODO (beckysiegel) figure out why it can be undefined + * + * @type {string|undefined} + */ + scrollBehavior: { + type: String, + value: ScrollBehavior.NEVER, + }, - /** - * When true, will call element.focus() during scrolling. - */ - focusOnMove: { - type: Boolean, - value: false, - }, - }, + /** + * When true, will call element.focus() during scrolling. + */ + focusOnMove: { + type: Boolean, + value: false, + }, + }; + } detached() { + super.detached(); this.unsetCursor(); - }, + } /** * Move the cursor forward. Clipped to the ends of the stop list. + * * @param {!Function=} opt_condition Optional stop condition. If a condition * is passed the cursor will continue to move in the specified direction * until the condition is met. @@ -104,14 +112,15 @@ next(opt_condition, opt_getTargetHeight, opt_clipToTop) { this._moveCursor(1, opt_condition, opt_getTargetHeight, opt_clipToTop); - }, + } previous(opt_condition) { this._moveCursor(-1, opt_condition); - }, + } /** * Set the cursor to an arbitrary element. + * * @param {!HTMLElement} element * @param {boolean=} opt_noScroll prevent any potential scrolling in response * setting the cursor. @@ -129,36 +138,37 @@ this._decorateTarget(); if (opt_noScroll) { this.scrollBehavior = behavior; } - }, + } unsetCursor() { this._unDecorateTarget(); this.index = -1; this.target = null; this._targetHeight = null; - }, + } isAtStart() { return this.index === 0; - }, + } isAtEnd() { return this.index === this.stops.length - 1; - }, + } moveToStart() { if (this.stops.length) { this.setCursor(this.stops[0]); } - }, + } setCursorAtIndex(index, opt_noScroll) { this.setCursor(this.stops[index], opt_noScroll); - }, + } /** * Move the cursor forward or backward by delta. Clipped to the beginning or * end of stop list. + * * @param {number} delta either -1 or 1. * @param {!Function=} opt_condition Optional stop condition. If a condition * is passed the cursor will continue to move in the specified direction @@ -199,22 +209,23 @@ if (this.focusOnMove) { this.target.focus(); } this._decorateTarget(); - }, + } _decorateTarget() { if (this.target && this.cursorTargetClass) { this.target.classList.add(this.cursorTargetClass); } - }, + } _unDecorateTarget() { if (this.target && this.cursorTargetClass) { this.target.classList.remove(this.cursorTargetClass); } - }, + } /** * Get the next stop index indicated by the delta direction. + * * @param {number} delta either -1 or 1. * @param {!Function=} opt_condition Optional stop condition. * @param {boolean=} opt_clipToTop When none of the next indices match, move @@ -247,7 +258,7 @@ } return newIndex; - }, + } _updateIndex() { if (!this.target) { @@ -261,22 +272,23 @@ } else { this.index = newIndex; } - }, + } /** * Calculate where the element is relative to the window. + * * @param {!Object} target Target to scroll to. * @return {number} Distance to top of the target. */ _getTop(target) { let top = target.offsetTop; for (let offsetParent = target.offsetParent; - offsetParent; - offsetParent = offsetParent.offsetParent) { + offsetParent; + offsetParent = offsetParent.offsetParent) { top += offsetParent.offsetTop; } return top; - }, + } /** * @return {boolean} @@ -286,12 +298,12 @@ return this.scrollBehavior === ScrollBehavior.KEEP_VISIBLE && top > dims.pageYOffset && top < dims.pageYOffset + dims.innerHeight; - }, + } _calculateScrollToValue(top, target) { const dims = this._getWindowDims(); return top - (dims.innerHeight / 3) + (target.offsetHeight / 2); - }, + } _scrollToTarget() { if (!this.target || this.scrollBehavior === ScrollBehavior.NEVER) { @@ -301,7 +313,7 @@ const dims = this._getWindowDims(); const top = this._getTop(this.target); const bottomIsVisible = this._targetHeight ? - this._targetIsVisible(top + this._targetHeight) : true; + this._targetIsVisible(top + this._targetHeight) : true; const scrollToValue = this._calculateScrollToValue(top, this.target); if (this._targetIsVisible(top)) { @@ -319,7 +331,7 @@ // element appears to be below the center of the window even when it // isn't. window.scrollTo(dims.scrollX, scrollToValue); - }, + } _getWindowDims() { return { @@ -328,6 +340,8 @@ innerHeight: window.innerHeight, pageYOffset: window.pageYOffset, }; - }, - }); + } + } + + customElements.define(GrCursorManager.is, GrCursorManager); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html index 0793ccd..103b41a 100644 --- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html +++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-cursor-manager</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -129,7 +129,6 @@ assert.equal(element.index, -1); }); - test('_moveCursor', () => { // Initialize the cursor with its stops. element.stops = list.querySelectorAll('li');
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js index 0360145..374844d 100644 --- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js +++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
@@ -31,50 +31,56 @@ MONTH_DAY_YEAR: 'MMM DD, YYYY', // Aug 29, 1997 }; - Polymer({ - is: 'gr-date-formatter', + /** + * @appliesMixin Gerrit.TooltipMixin + */ + class GrDateFormatter extends Polymer.mixinBehaviors( [ + Gerrit.TooltipBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-date-formatter'; } - properties: { - dateStr: { - type: String, - value: null, - notify: true, - }, - showDateAndTime: { - type: Boolean, - value: false, - }, + static get properties() { + return { + dateStr: { + type: String, + value: null, + notify: true, + }, + showDateAndTime: { + type: Boolean, + value: false, + }, - /** - * When true, the detailed date appears in a GR-TOOLTIP rather than in the - * native browser tooltip. - */ - hasTooltip: Boolean, + /** + * When true, the detailed date appears in a GR-TOOLTIP rather than in the + * native browser tooltip. + */ + hasTooltip: Boolean, - /** - * The title to be used as the native tooltip or by the tooltip behavior. - */ - title: { - type: String, - reflectToAttribute: true, - computed: '_computeFullDateStr(dateStr, _timeFormat)', - }, + /** + * The title to be used as the native tooltip or by the tooltip behavior. + */ + title: { + type: String, + reflectToAttribute: true, + computed: '_computeFullDateStr(dateStr, _timeFormat)', + }, - _timeFormat: String, // No default value to prevent flickering. - _relative: Boolean, // No default value to prevent flickering. - }, - - behaviors: [ - Gerrit.TooltipBehavior, - ], + _timeFormat: String, // No default value to prevent flickering. + _relative: Boolean, // No default value to prevent flickering. + }; + } attached() { + super.attached(); this._loadPreferences(); - }, + } _getUtcOffsetString() { return ' UTC' + moment().format('Z'); - }, + } _loadPreferences() { return this._getLoggedIn().then(loggedIn => { @@ -88,7 +94,7 @@ this._loadRelative(), ]); }); - }, + } _loadTimeFormat() { return this._getPreferences().then(preferences => { @@ -104,22 +110,22 @@ throw Error('Invalid time format: ' + timeFormat); } }); - }, + } _loadRelative() { return this._getPreferences().then(prefs => { // prefs.relative_date_in_change_table is not set when false. this._relative = !!(prefs && prefs.relative_date_in_change_table); }); - }, + } _getLoggedIn() { return this.$.restAPI.getLoggedIn(); - }, + } _getPreferences() { return this.$.restAPI.getPreferences(); - }, + } /** * Return true if date is within 24 hours and on the same day. @@ -127,7 +133,7 @@ _isWithinDay(now, date) { const diff = -date.diff(now); return diff < Duration.DAY && date.day() === now.getDay(); - }, + } /** * Returns true if date is from one to six months. @@ -136,7 +142,7 @@ const diff = -date.diff(now); return (date.day() !== now.getDay() || diff >= Duration.DAY) && diff < 180 * Duration.DAY; - }, + } _computeDateStr(dateStr, timeFormat, relative, showDateAndTime) { if (!dateStr) { return ''; } @@ -163,13 +169,13 @@ } } return date.format(format); - }, + } _timeToSecondsFormat(timeFormat) { return timeFormat === TimeFormats.TIME_12 ? - TimeFormats.TIME_12_WITH_SEC : - TimeFormats.TIME_24_WITH_SEC; - }, + TimeFormats.TIME_12_WITH_SEC : + TimeFormats.TIME_24_WITH_SEC; + } _computeFullDateStr(dateStr, timeFormat) { // Polymer 2: check for undefined @@ -186,6 +192,8 @@ let format = TimeFormats.MONTH_DAY_YEAR + ', '; format += this._timeToSecondsFormat(timeFormat); return date.format(format) + this._getUtcOffsetString(); - }, - }); + } + } + + customElements.define(GrDateFormatter.is, GrDateFormatter); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html index a0bf207..fffd38e 100644 --- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html +++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-date-formatter</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -89,7 +89,7 @@ suite('24 hours time format preference', () => { setup(() => { return stubRestAPI( - {time_format: 'HHMM_24', relative_date_in_change_table: false} + {time_format: 'HHMM_24', relative_date_in_change_table: false} ).then(() => { element = fixture('basic'); sandbox.stub(element, '_getUtcOffsetString').returns(''); @@ -139,7 +139,7 @@ setup(() => { // relative_date_in_change_table is not set when false. return stubRestAPI( - {time_format: 'HHMM_12'} + {time_format: 'HHMM_12'} ).then(() => { element = fixture('basic'); sandbox.stub(element, '_getUtcOffsetString').returns(''); @@ -159,7 +159,7 @@ suite('relative date preference', () => { setup(() => { return stubRestAPI( - {time_format: 'HHMM_12', relative_date_in_change_table: true} + {time_format: 'HHMM_12', relative_date_in_change_table: true} ).then(() => { element = fixture('basic'); sandbox.stub(element, '_getUtcOffsetString').returns(''); @@ -187,7 +187,7 @@ suite('logged in', () => { setup(() => { return stubRestAPI( - {time_format: 'HHMM_12', relative_date_in_change_table: true} + {time_format: 'HHMM_12', relative_date_in_change_table: true} ).then(() => { element = fixture('basic'); return element._loadPreferences();
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html index 2ef5539..77418df 100644 --- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html +++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html
@@ -32,38 +32,35 @@ display: flex; flex-direction: column; max-height: 90vh; + padding: var(--spacing-xl); } header { - border-bottom: 1px solid var(--border-color); flex-shrink: 0; - font-weight: var(--font-weight-bold); + padding-bottom: var(--spacing-xl); } main { display: flex; flex-shrink: 1; width: 100%; } - header, - main, - footer { - padding: var(--spacing-m) var(--spacing-xl); - } - gr-button { - margin-left: var(--spacing-l); - } footer { display: flex; flex-shrink: 0; justify-content: flex-end; + padding-top: var(--spacing-xl); + } + gr-button { + margin-left: var(--spacing-l); } .hidden { display: none; } </style> <div class="container" on-keydown="_handleKeydown"> - <header><slot name="header"></slot></header> + <header class="font-h3"><slot name="header"></slot></header> <main><slot name="main"></slot></main> <footer> + <slot name="footer"></slot> <gr-button id="cancel" class$="[[_computeCancelClass(cancelLabel)]]" link on-click="_handleCancelTap"> [[cancelLabel]] </gr-button>
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js index 68dc537..d36aa37 100644 --- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js +++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
@@ -17,9 +17,15 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-dialog', - + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrDialog extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-dialog'; } /** * Fired when the confirm button is pressed. * @@ -32,33 +38,32 @@ * @event cancel */ - properties: { - confirmLabel: { - type: String, - value: 'Confirm', - }, - // Supplying an empty cancel label will hide the button completely. - cancelLabel: { - type: String, - value: 'Cancel', - }, - disabled: { - type: Boolean, - value: false, - }, - confirmOnEnter: { - type: Boolean, - value: false, - }, - }, + static get properties() { + return { + confirmLabel: { + type: String, + value: 'Confirm', + }, + // Supplying an empty cancel label will hide the button completely. + cancelLabel: { + type: String, + value: 'Cancel', + }, + disabled: { + type: Boolean, + value: false, + }, + confirmOnEnter: { + type: Boolean, + value: false, + }, + }; + } - behaviors: [ - Gerrit.FireBehavior, - ], - - hostAttributes: { - role: 'dialog', - }, + ready() { + super.ready(); + this._ensureAttribute('role', 'dialog'); + } _handleConfirm(e) { if (this.disabled) { return; } @@ -66,24 +71,26 @@ e.preventDefault(); e.stopPropagation(); this.fire('confirm', null, {bubbles: false}); - }, + } _handleCancelTap(e) { e.preventDefault(); e.stopPropagation(); this.fire('cancel', null, {bubbles: false}); - }, + } _handleKeydown(e) { if (this.confirmOnEnter && e.keyCode === 13) { this._handleConfirm(e); } - }, + } resetFocus() { this.$.confirm.focus(); - }, + } _computeCancelClass(cancelLabel) { return cancelLabel.length ? '' : 'hidden'; - }, - }); + } + } + + customElements.define(GrDialog.is, GrDialog); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html index 1456e77..ced925e 100644 --- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html +++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-dialog</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.html b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.html index 9d85d44..367e30c 100644 --- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.html +++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.html
@@ -24,8 +24,12 @@ <dom-module id="gr-diff-preferences"> <template> - <style include="shared-styles"></style> - <style include="gr-form-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="gr-form-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <div id="diffPreferences" class="gr-form-styles"> <section> <span class="title">Context</span>
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js index 36fdf5b..6e34bbc 100644 --- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js +++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
@@ -17,62 +17,68 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-diff-preferences', + class GrDiffPreferences extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-diff-preferences'; } - properties: { - hasUnsavedChanges: { - type: Boolean, - notify: true, - value: false, - }, + static get properties() { + return { + hasUnsavedChanges: { + type: Boolean, + notify: true, + value: false, + }, - /** @type {?} */ - diffPrefs: Object, - }, + /** @type {?} */ + diffPrefs: Object, + }; + } loadData() { return this.$.restAPI.getDiffPreferences().then(prefs => { this.diffPrefs = prefs; }); - }, + } _handleDiffPrefsChanged() { this.hasUnsavedChanges = true; - }, + } _handleLineWrappingTap() { this.set('diffPrefs.line_wrapping', this.$.lineWrappingInput.checked); this._handleDiffPrefsChanged(); - }, + } _handleShowTabsTap() { this.set('diffPrefs.show_tabs', this.$.showTabsInput.checked); this._handleDiffPrefsChanged(); - }, + } _handleShowTrailingWhitespaceTap() { this.set('diffPrefs.show_whitespace_errors', this.$.showTrailingWhitespaceInput.checked); this._handleDiffPrefsChanged(); - }, + } _handleSyntaxHighlightTap() { this.set('diffPrefs.syntax_highlighting', this.$.syntaxHighlightInput.checked); this._handleDiffPrefsChanged(); - }, + } _handleAutomaticReviewTap() { this.set('diffPrefs.manual_review', !this.$.automaticReviewInput.checked); this._handleDiffPrefsChanged(); - }, + } save() { return this.$.restAPI.saveDiffPreferences(this.diffPrefs).then(res => { this.hasUnsavedChanges = false; }); - }, - }); + } + } + + customElements.define(GrDiffPreferences.is, GrDiffPreferences); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html index f7b10e0..fa1ade5 100644 --- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html +++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff-preferences</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -110,7 +110,7 @@ .returns(Promise.resolve()); const showTrailingWhitespaceCheckbox = valueOf('Show trailing whitespace', 'diffPreferences') - .firstElementChild; + .firstElementChild; showTrailingWhitespaceCheckbox.checked = false; element._handleShowTrailingWhitespaceTap();
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js index 121aa35..9e90ae3 100644 --- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js +++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
@@ -17,40 +17,46 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-download-commands', + /** + * @appliesMixin Gerrit.RESTClientMixin + */ + class GrDownloadCommands extends Polymer.mixinBehaviors( [ + Gerrit.RESTClientBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-download-commands'; } - properties: { - commands: Array, - _loggedIn: { - type: Boolean, - value: false, - observer: '_loggedInChanged', - }, - schemes: Array, - selectedScheme: { - type: String, - notify: true, - }, - }, - - behaviors: [ - Gerrit.RESTClientBehavior, - ], + static get properties() { + return { + commands: Array, + _loggedIn: { + type: Boolean, + value: false, + observer: '_loggedInChanged', + }, + schemes: Array, + selectedScheme: { + type: String, + notify: true, + }, + }; + } attached() { + super.attached(); this._getLoggedIn().then(loggedIn => { this._loggedIn = loggedIn; }); - }, + } focusOnCopy() { this.$$('gr-shell-command').focusOnCopy(); - }, + } _getLoggedIn() { return this.$.restAPI.getLoggedIn(); - }, + } _loggedInChanged(loggedIn) { if (!loggedIn) { return; } @@ -60,7 +66,7 @@ this.selectedScheme = prefs.download_scheme.toLowerCase(); } }); - }, + } _handleTabChange(e) { const scheme = this.schemes[e.detail.value]; @@ -71,15 +77,17 @@ {download_scheme: this.selectedScheme}); } } - }, + } _computeSelected(schemes, selectedScheme) { - return (schemes.findIndex(scheme => scheme === selectedScheme) || 0) - + ''; - }, + return (schemes.findIndex(scheme => scheme === selectedScheme) || 0) + + ''; + } _computeShowTabs(schemes) { return schemes.length > 1 ? '' : 'hidden'; - }, - }); + } + } + + customElements.define(GrDownloadCommands.is, GrDownloadCommands); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html index 85a5b1f..7d4b341 100644 --- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html +++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-download-commands</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html index 98d7bf6..7fffcff 100644 --- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html +++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
@@ -142,7 +142,7 @@ <paper-listbox class="dropdown-content" slot="dropdown-content" - attr-for-selected="value" + attr-for-selected="data-value" selected="{{value}}" on-tap="_handleDropdownTap"> <template is="dom-repeat" @@ -150,7 +150,7 @@ initial-count="[[initialCount]]"> <paper-item disabled="[[item.disabled]]" - value="[[item.value]]"> + data-value$="[[item.value]]"> <div class="topContent"> <div>[[item.text]]</div> <template is="dom-if" if="[[item.date]]">
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js index 3b5e25f..473a0a9 100644 --- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js +++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
@@ -45,9 +45,10 @@ */ Defs.item; - Polymer({ - is: 'gr-dropdown-list', - + class GrDropdownList extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-dropdown-list'; } /** * Fired when the selected value changes * @@ -56,27 +57,32 @@ * @property {string|number} value */ - properties: { - initialCount: Number, - /** @type {!Array<!Defs.item>} */ - items: Object, - text: String, - disabled: { - type: Boolean, - value: false, - }, - value: { - type: String, - notify: true, - }, - }, + static get properties() { + return { + initialCount: Number, + /** @type {!Array<!Defs.item>} */ + items: Object, + text: String, + disabled: { + type: Boolean, + value: false, + }, + value: { + type: String, + notify: true, + }, + }; + } - observers: [ - '_handleValueChange(value, items)', - ], + static get observers() { + return [ + '_handleValueChange(value, items)', + ]; + } /** * Handle a click on the iron-dropdown element. + * * @param {!Event} e */ _handleDropdownClick(e) { @@ -85,26 +91,27 @@ this.async(() => { this.$.dropdown.close(); }, 1); - }, + } /** * Handle a click on the button to open the dropdown. + * * @param {!Event} e */ _showDropdownTapHandler(e) { this._open(); - }, + } /** * Open the dropdown. */ _open() { this.$.dropdown.open(); - }, + } _computeMobileText(item) { return item.mobileText ? item.mobileText : item.text; - }, + } _handleValueChange(value, items) { // Polymer 2: check for undefined @@ -118,11 +125,13 @@ }); if (!selectedObj) { return; } this.text = selectedObj.triggerText? selectedObj.triggerText : - selectedObj.text; + selectedObj.text; this.dispatchEvent(new CustomEvent('value-change', { detail: {value}, bubbles: false, })); - }, - }); + } + } + + customElements.define(GrDropdownList.is, GrDropdownList); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html index 2b63d99..8d098df 100644 --- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html +++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-dropdown-list</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -109,7 +109,7 @@ assert.isNotOk(Polymer.dom(items[0]).querySelector('gr-date-formatter')); assert.isNotOk(Polymer.dom(items[0]).querySelector('.bottomContent')); - assert.equal(items[0].value, element.items[0].value); + assert.equal(items[0].dataset.value, element.items[0].value); assert.equal(mobileItems[0].value, element.items[0].value); assert.equal(Polymer.dom(items[0]).querySelector('.topContent div') .innerText, element.items[0].text); @@ -117,7 +117,6 @@ // Since no mobile specific text, it should fall back to text. assert.equal(mobileItems[0].text, element.items[0].text); - // Second Item // The second item should have top text, bottom text, and no date. assert.isFalse(!!items[1].disabled); @@ -127,7 +126,7 @@ assert.isNotOk(Polymer.dom(items[1]).querySelector('gr-date-formatter')); assert.isOk(Polymer.dom(items[1]).querySelector('.bottomContent')); - assert.equal(items[1].value, element.items[1].value); + assert.equal(items[1].dataset.value, element.items[1].value); assert.equal(mobileItems[1].value, element.items[1].value); assert.equal(Polymer.dom(items[1]).querySelector('.topContent div') .innerText, element.items[1].text); @@ -148,7 +147,7 @@ assert.isOk(Polymer.dom(items[2]).querySelector('gr-date-formatter')); assert.isOk(Polymer.dom(items[2]).querySelector('.bottomContent')); - assert.equal(items[2].value, element.items[2].value); + assert.equal(items[2].dataset.value, element.items[2].value); assert.equal(mobileItems[2].value, element.items[2].value); assert.equal(Polymer.dom(items[2]).querySelector('.topContent div') .innerText, element.items[2].text);
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js index 615acd7..619005a 100644 --- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js +++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -20,9 +20,17 @@ const REL_NOOPENER = 'noopener'; const REL_EXTERNAL = 'external'; - Polymer({ - is: 'gr-dropdown', - + /** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + */ + class GrDropdown extends Polymer.mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.KeyboardShortcutBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-dropdown'; } /** * Fired when a non-link dropdown item with the given ID is tapped. * @@ -35,63 +43,63 @@ * @event tap-item */ - properties: { - items: { - type: Array, - observer: '_resetCursorStops', - }, - downArrow: Boolean, - topContent: Object, - horizontalAlign: { - type: String, - value: 'left', - }, + static get properties() { + return { + items: { + type: Array, + observer: '_resetCursorStops', + }, + downArrow: Boolean, + topContent: Object, + horizontalAlign: { + type: String, + value: 'left', + }, - /** - * Style the dropdown trigger as a link (rather than a button). - */ - link: { - type: Boolean, - value: false, - }, + /** + * Style the dropdown trigger as a link (rather than a button). + */ + link: { + type: Boolean, + value: false, + }, - verticalOffset: { - type: Number, - value: 40, - }, + verticalOffset: { + type: Number, + value: 40, + }, - /** - * List the IDs of dropdown buttons to be disabled. (Note this only - * diisables bittons and not link entries.) - */ - disabledIds: { - type: Array, - value() { return []; }, - }, + /** + * List the IDs of dropdown buttons to be disabled. (Note this only + * diisables bittons and not link entries.) + */ + disabledIds: { + type: Array, + value() { return []; }, + }, - /** - * The elements of the list. - */ - _listElements: { - type: Array, - value() { return []; }, - }, - }, + /** + * The elements of the list. + */ + _listElements: { + type: Array, + value() { return []; }, + }, + }; + } - behaviors: [ - Gerrit.BaseUrlBehavior, - Gerrit.KeyboardShortcutBehavior, - ], - - keyBindings: { - 'down': '_handleDown', - 'enter space': '_handleEnter', - 'tab': '_handleTab', - 'up': '_handleUp', - }, + get keyBindings() { + return { + 'down': '_handleDown', + 'enter space': '_handleEnter', + 'tab': '_handleTab', + 'up': '_handleUp', + }; + } /** * Handle the up key. + * * @param {!Event} e */ _handleUp(e) { @@ -102,10 +110,11 @@ } else { this._open(); } - }, + } /** * Handle the down key. + * * @param {!Event} e */ _handleDown(e) { @@ -116,10 +125,11 @@ } else { this._open(); } - }, + } /** * Handle the tab key. + * * @param {!Event} e */ _handleTab(e) { @@ -128,10 +138,11 @@ e.preventDefault(); e.stopPropagation(); } - }, + } /** * Handle the enter key. + * * @param {!Event} e */ _handleEnter(e) { @@ -146,18 +157,20 @@ } else { this._open(); } - }, + } /** * Handle a click on the iron-dropdown element. + * * @param {!Event} e */ _handleDropdownClick(e) { this._close(); - }, + } /** * Hanlde a click on the button to open the dropdown. + * * @param {!Event} e */ _dropdownTriggerTapHandler(e) { @@ -168,7 +181,7 @@ } else { this._open(); } - }, + } /** * Open the dropdown and initialize the cursor. @@ -178,7 +191,7 @@ this._resetCursorStops(); this.$.cursor.setCursorAtIndex(0); this.$.cursor.target.focus(); - }, + } _close() { // async is needed so that that the click event is fired before the @@ -186,44 +199,48 @@ this.async(() => { this.$.dropdown.close(); }, 1); - }, + } /** * Get the class for a top-content item based on the given boolean. + * * @param {boolean} bold Whether the item is bold. * @return {string} The class for the top-content item. */ _getClassIfBold(bold) { return bold ? 'bold-text' : ''; - }, + } /** * Build a URL for the given host and path. The base URL will be only added, * if it is not already included in the path. + * * @param {!string} host * @param {!string} path * @return {!string} The scheme-relative URL. */ _computeURLHelper(host, path) { const base = path.startsWith(this.getBaseUrl()) ? - '' : this.getBaseUrl(); + '' : this.getBaseUrl(); return '//' + host + base + path; - }, + } /** * Build a scheme-relative URL for the current host. Will include the base * URL if one is present. Note: the URL will be scheme-relative but absolute * with regard to the host. + * * @param {!string} path The path for the URL. * @return {!string} The scheme-relative URL. */ _computeRelativeURL(path) { const host = window.location.host; return this._computeURLHelper(host, path); - }, + } /** * Compute the URL for a link object. + * * @param {!Object} link The object describing the link. * @return {!string} The URL. */ @@ -235,12 +252,13 @@ return link.url; } return this._computeRelativeURL(link.url); - }, + } /** * Compute the value for the rel attribute of an anchor for the given link * object. If the link has a target value, then the rel must be "noopener" * for security reasons. + * * @param {!Object} link The object describing the link. * @return {?string} The rel value for the link. */ @@ -249,10 +267,11 @@ if (link.target) { return REL_NOOPENER; } if (link.external) { return REL_EXTERNAL; } return null; - }, + } /** * Handle a click on an item of the dropdown. + * * @param {!Event} e */ _handleItemTap(e) { @@ -264,10 +283,11 @@ } this.dispatchEvent(new CustomEvent('tap-item-' + id)); } - }, + } /** * If a dropdown item is shown as a button, get the class for the button. + * * @param {string} id * @param {!Object} disabledIdsRecord The change record for the disabled IDs * list. @@ -275,7 +295,7 @@ */ _computeDisabledClass(id, disabledIdsRecord) { return disabledIdsRecord.base.includes(id) ? 'disabled' : ''; - }, + } /** * Recompute the stops for the dropdown item cursor. @@ -286,14 +306,16 @@ this._listElements = Array.from( Polymer.dom(this.root).querySelectorAll('li')); } - }, + } _computeHasTooltip(tooltip) { return !!tooltip; - }, + } _computeIsDownload(link) { return !!link.download; - }, - }); + } + } + + customElements.define(GrDropdown.is, GrDropdown); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html index 295f746..4b5ccb4 100644 --- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html +++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-dropdown</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js index 1e339dc..0c2d4f5 100644 --- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js +++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
@@ -20,9 +20,15 @@ const RESTORED_MESSAGE = 'Content restored from a previous edit.'; const STORAGE_DEBOUNCE_INTERVAL_MS = 400; - Polymer({ - is: 'gr-editable-content', - + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrEditableContent extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-editable-content'; } /** * Fired when the save button is pressed. * @@ -41,42 +47,40 @@ * @event show-alert */ - properties: { - content: { - notify: true, - type: String, - }, - disabled: { - reflectToAttribute: true, - type: Boolean, - value: false, - }, - editing: { - observer: '_editingChanged', - type: Boolean, - value: false, - }, - removeZeroWidthSpace: Boolean, - // If no storage key is provided, content is not stored. - storageKey: String, - _saveDisabled: { - computed: '_computeSaveDisabled(disabled, content, _newContent)', - type: Boolean, - value: true, - }, - _newContent: { - type: String, - observer: '_newContentChanged', - }, - }, - - behaviors: [ - Gerrit.FireBehavior, - ], + static get properties() { + return { + content: { + notify: true, + type: String, + }, + disabled: { + reflectToAttribute: true, + type: Boolean, + value: false, + }, + editing: { + observer: '_editingChanged', + type: Boolean, + value: false, + }, + removeZeroWidthSpace: Boolean, + // If no storage key is provided, content is not stored. + storageKey: String, + _saveDisabled: { + computed: '_computeSaveDisabled(disabled, content, _newContent)', + type: Boolean, + value: true, + }, + _newContent: { + type: String, + observer: '_newContentChanged', + }, + }; + } focusTextarea() { this.$$('iron-autogrow-textarea').textarea.focus(); - }, + } _newContentChanged(newContent, oldContent) { if (!this.storageKey) { return; } @@ -88,7 +92,7 @@ this.$.storage.eraseEditableContentItem(this.storageKey); } }, STORAGE_DEBOUNCE_INTERVAL_MS); - }, + } _editingChanged(editing) { if (!editing) { return; } @@ -112,9 +116,9 @@ // TODO(wyatta) switch linkify sequence, see issue 5526. this._newContent = this.removeZeroWidthSpace ? - content.replace(/^R=\u200B/gm, 'R=') : - content; - }, + content.replace(/^R=\u200B/gm, 'R=') : + content; + } _computeSaveDisabled(disabled, content, newContent) { // Polymer 2: check for undefined @@ -127,17 +131,19 @@ } return disabled || (content === newContent); - }, + } _handleSave(e) { e.preventDefault(); this.fire('editable-content-save', {content: this._newContent}); - }, + } _handleCancel(e) { e.preventDefault(); this.editing = false; this.fire('editable-content-cancel'); - }, - }); + } + } + + customElements.define(GrEditableContent.is, GrEditableContent); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html index 24db69f..69c9a00 100644 --- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html +++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-editable-content</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js index 4485551..13000b4 100644 --- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js +++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
@@ -20,76 +20,84 @@ const AWAIT_MAX_ITERS = 10; const AWAIT_STEP = 5; - Polymer({ - is: 'gr-editable-label', - + /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + */ + class GrEditableLabel extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-editable-label'; } /** * Fired when the value is changed. * * @event changed */ - properties: { - labelText: String, - editing: { - type: Boolean, - value: false, - }, - value: { - type: String, - notify: true, - value: '', - observer: '_updateTitle', - }, - placeholder: { - type: String, - value: '', - }, - readOnly: { - type: Boolean, - value: false, - }, - uppercase: { - type: Boolean, - reflectToAttribute: true, - value: false, - }, - maxLength: Number, - _inputText: String, - // This is used to push the iron-input element up on the page, so - // the input is placed in approximately the same position as the - // trigger. - _verticalOffset: { - type: Number, - readOnly: true, - value: -30, - }, - }, + static get properties() { + return { + labelText: String, + editing: { + type: Boolean, + value: false, + }, + value: { + type: String, + notify: true, + value: '', + observer: '_updateTitle', + }, + placeholder: { + type: String, + value: '', + }, + readOnly: { + type: Boolean, + value: false, + }, + uppercase: { + type: Boolean, + reflectToAttribute: true, + value: false, + }, + maxLength: Number, + _inputText: String, + // This is used to push the iron-input element up on the page, so + // the input is placed in approximately the same position as the + // trigger. + _verticalOffset: { + type: Number, + readOnly: true, + value: -30, + }, + }; + } - behaviors: [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - ], + ready() { + super.ready(); + this._ensureAttribute('tabindex', '0'); + } - keyBindings: { - enter: '_handleEnter', - esc: '_handleEsc', - }, - - hostAttributes: { - tabindex: '0', - }, + get keyBindings() { + return { + enter: '_handleEnter', + esc: '_handleEsc', + }; + } _usePlaceholder(value, placeholder) { return (!value || !value.length) && placeholder; - }, + } _computeLabel(value, placeholder) { if (this._usePlaceholder(value, placeholder)) { return placeholder; } return value; - }, + } _showDropdown() { if (this.readOnly || this.editing) { return; } @@ -98,13 +106,13 @@ if (!this.$.input.value) { return; } this._nativeInput.setSelectionRange(0, this.$.input.value.length); }); - }, + } open() { return this._open().then(() => { this._nativeInput.focus(); }); - }, + } _open(...args) { this.$.dropdown.open(); @@ -115,7 +123,7 @@ Polymer.IronOverlayBehaviorImpl.open.apply(this.$.dropdown, args); this._awaitOpen(resolve); }); - }, + } /** * NOTE: (wyatta) Slightly hacky way to listen to the overlay actually @@ -133,11 +141,11 @@ }, AWAIT_STEP); }; step.call(this); - }, + } _id() { return this.getAttribute('id') || 'global'; - }, + } _save() { if (!this.editing) { return; } @@ -145,20 +153,20 @@ this.value = this._inputText; this.editing = false; this.fire('changed', this.value); - }, + } _cancel() { if (!this.editing) { return; } this.$.dropdown.close(); this.editing = false; this._inputText = this.value; - }, + } get _nativeInput() { // In Polymer 2, the namespace of nativeInput // changed from input to nativeInput return this.$.input.$.nativeInput || this.$.input.$.input; - }, + } _handleEnter(e) { e = this.getKeyboardEvent(e); @@ -167,7 +175,7 @@ e.preventDefault(); this._save(); } - }, + } _handleEsc(e) { e = this.getKeyboardEvent(e); @@ -176,7 +184,7 @@ e.preventDefault(); this._cancel(); } - }, + } _computeLabelClass(readOnly, value, placeholder) { const classes = []; @@ -185,10 +193,12 @@ classes.push('placeholder'); } return classes.join(' '); - }, + } _updateTitle(value) { this.setAttribute('title', this._computeLabel(value, this.placeholder)); - }, - }); + } + } + + customElements.define(GrEditableLabel.is, GrEditableLabel); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html index 7ff0a14..1690c3a 100644 --- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html +++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-editable-label</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -166,7 +166,6 @@ MockInteractions.tap(element.$.saveBtn, 13); }); - test('edit and then escape key', done => { const editedStub = sandbox.stub(); element.addEventListener('changed', editedStub);
diff --git a/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface.js b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface.js index d024bb2..f763457 100644 --- a/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface.js +++ b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface.js
@@ -39,6 +39,7 @@ constructor() { /** * Shared events map from name to the listeners. + * * @type {!Object<string, Array<eventCallback>>} */ this._listenersMap = new Map();
diff --git a/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html index 137ed25..4509dfe 100644 --- a/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html +++ b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html
@@ -62,7 +62,7 @@ p = plugin; Gerrit.emit(eventName, {value: 'test', plugin}); }, '0.1', - 'http://test.com/plugins/testplugin/static/test.js'); + 'http://test.com/plugins/testplugin/static/test.js'); }); test('listen on events from core', done => { @@ -83,19 +83,21 @@ done(); }); }, '0.1', - 'http://test.com/plugins/testA/static/testA.js'); + 'http://test.com/plugins/testA/static/testA.js'); Gerrit.install(plugin => { Gerrit.emit(eventName, {plugin}); }, '0.1', - 'http://test.com/plugins/testB/static/testB.js'); + 'http://test.com/plugins/testB/static/testB.js'); }); }); suite('test on interfaces', () => { let testObj; + class TestClass extends EventEmitter { } + setup(() => { testObj = new TestClass(); }); @@ -143,4 +145,4 @@ }); }); }); -</script> \ No newline at end of file +</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js index 2c32709..1e8f767 100644 --- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js +++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
@@ -17,47 +17,52 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-fixed-panel', + class GrFixedPanel extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-fixed-panel'; } - properties: { - floatingDisabled: Boolean, - readyForMeasure: { - type: Boolean, - observer: '_readyForMeasureObserver', - }, - keepOnScroll: { - type: Boolean, - value: false, - }, - _isMeasured: { - type: Boolean, - value: false, - }, + static get properties() { + return { + floatingDisabled: Boolean, + readyForMeasure: { + type: Boolean, + observer: '_readyForMeasureObserver', + }, + keepOnScroll: { + type: Boolean, + value: false, + }, + _isMeasured: { + type: Boolean, + value: false, + }, - /** - * Initial offset from the top of the document, in pixels. - */ - _topInitial: Number, + /** + * Initial offset from the top of the document, in pixels. + */ + _topInitial: Number, - /** - * Current offset from the top of the window, in pixels. - */ - _topLast: Number, + /** + * Current offset from the top of the window, in pixels. + */ + _topLast: Number, - _headerHeight: Number, - _headerFloating: { - type: Boolean, - value: false, - }, - _observer: { - type: Object, - value: null, - }, - _webComponentsReady: Boolean, - }, + _headerHeight: Number, + _headerFloating: { + type: Boolean, + value: false, + }, + _observer: { + type: Object, + value: null, + }, + _webComponentsReady: Boolean, + }; + } attached() { + super.attached(); if (this.floatingDisabled) { return; } @@ -69,21 +74,22 @@ this.listen(window, 'scroll', '_updateOnScroll'); this._observer = new MutationObserver(this.update.bind(this)); this._observer.observe(this.$.header, {childList: true, subtree: true}); - }, + } detached() { + super.detached(); this.unlisten(window, 'scroll', '_updateOnScroll'); this.unlisten(window, 'resize', 'update'); if (this._observer) { this._observer.disconnect(); } - }, + } _readyForMeasureObserver(readyForMeasure) { if (readyForMeasure) { this.update(); } - }, + } _computeHeaderClass(headerFloating, topLast) { const fixedAtTop = this.keepOnScroll && topLast === 0; @@ -91,7 +97,7 @@ headerFloating ? 'floating' : '', fixedAtTop ? 'fixedAtTop' : '', ].join(' '); - }, + } unfloat() { if (this.floatingDisabled) { @@ -100,19 +106,19 @@ this.$.header.style.top = ''; this._headerFloating = false; this.updateStyles({'--header-height': ''}); - }, + } update() { this.debounce('update', () => { this._updateDebounced(); }, 100); - }, + } _updateOnScroll() { this.debounce('update', () => { this._updateDebounced(); }); - }, + } _updateDebounced() { if (this.floatingDisabled) { @@ -121,11 +127,11 @@ this._isMeasured = false; this._maybeFloatHeader(); this._reposition(); - }, + } _getElementTop() { return this.getBoundingClientRect().top; - }, + } _reposition() { if (!this._headerFloating) { @@ -155,7 +161,7 @@ } this._topLast = newTop; } - }, + } _measure() { if (this._isMeasured) { @@ -171,12 +177,12 @@ this._topInitial = this.getBoundingClientRect().top + document.body.scrollTop; this._isMeasured = true; - }, + } _isFloatingNeeded() { return this.keepOnScroll || document.body.scrollWidth > document.body.clientWidth; - }, + } _maybeFloatHeader() { if (!this._isFloatingNeeded()) { @@ -186,11 +192,13 @@ if (this._isMeasured) { this._floatHeader(); } - }, + } _floatHeader() { this.updateStyles({'--header-height': this._headerHeight + 'px'}); this._headerFloating = true; - }, - }); + } + } + + customElements.define(GrFixedPanel.is, GrFixedPanel); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html index 75e9901..419b3e7 100644 --- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html +++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-fixed-panel</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js index 4e68d42..39b114d 100644 --- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js +++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
@@ -20,30 +20,37 @@ // eslint-disable-next-line no-unused-vars const QUOTE_MARKER_PATTERN = /\n\s?>\s/g; - Polymer({ - is: 'gr-formatted-text', + class GrFormattedText extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-formatted-text'; } - properties: { - content: { - type: String, - observer: '_contentChanged', - }, - config: Object, - noTrailingMargin: { - type: Boolean, - value: false, - }, - }, + static get properties() { + return { + content: { + type: String, + observer: '_contentChanged', + }, + config: Object, + noTrailingMargin: { + type: Boolean, + value: false, + }, + }; + } - observers: [ - '_contentOrConfigChanged(content, config)', - ], + static get observers() { + return [ + '_contentOrConfigChanged(content, config)', + ]; + } ready() { + super.ready(); if (this.noTrailingMargin) { this.classList.add('noTrailingMargin'); } - }, + } /** * Get the plain text as it appears in the generated DOM. @@ -56,7 +63,7 @@ */ getTextContent() { return this._blocksToText(this._computeBlocks(this.content)); - }, + } _contentChanged(content) { // In the case where the config may not be set (perhaps due to the @@ -64,7 +71,7 @@ // prevent waiting on the config to display the text. if (this.config) { return; } this._contentOrConfigChanged(content); - }, + } /** * Given a source string, update the DOM inside #container. @@ -81,7 +88,7 @@ for (const node of this._computeNodes(this._computeBlocks(content))) { container.appendChild(node); } - }, + } /** * Given a source string, parse into an array of block objects. Each block @@ -127,7 +134,7 @@ } } return result; - }, + } /** * Take a block of comment text that contains a list and potentially @@ -201,7 +208,7 @@ if (block !== null) { out.push(block); } - }, + } _makeQuote(p) { const quotedLines = p @@ -212,21 +219,21 @@ type: 'quote', blocks: this._computeBlocks(quotedLines), }; - }, + } _isQuote(p) { return p.startsWith('> ') || p.startsWith(' > '); - }, + } _isPreFormat(p) { return p.includes('\n ') || p.includes('\n\t') || p.startsWith(' ') || p.startsWith('\t'); - }, + } _isList(p) { return p.includes('\n- ') || p.includes('\n* ') || p.startsWith('- ') || p.startsWith('* '); - }, + } /** * @param {string} content @@ -241,10 +248,11 @@ text.classList.add('pre'); } return text; - }, + } /** * Map an array of block objects to an array of DOM nodes. + * * @param {!Array<!Object>} blocks * @return {!Array<!HTMLElement>} */ @@ -278,7 +286,7 @@ return ul; } }); - }, + } _blocksToText(blocks) { return blocks.map(block => { @@ -292,6 +300,8 @@ return block.items.join('\n'); } }).join('\n\n'); - }, - }); + } + } + + customElements.define(GrFormattedText.is, GrFormattedText); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html index 801190a..92fbc74 100644 --- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html +++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-editable-label</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js index 1b31e89..37972f9 100644 --- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js +++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
@@ -25,82 +25,90 @@ */ const DIAGONAL_OVERFLOW = 15; - Polymer({ - is: 'gr-hovercard', + class GrHovercard extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-hovercard'; } - properties: { + static get properties() { + return { /** * @type {?} */ - _target: Object, + _target: Object, - /** - * Determines whether or not the hovercard is visible. - * - * @type {boolean} - */ - _isShowing: { - type: Boolean, - value: false, - }, - /** - * The `id` of the element that the hovercard is anchored to. - * - * @type {string} - */ - for: { - type: String, - observer: '_forChanged', - }, + /** + * Determines whether or not the hovercard is visible. + * + * @type {boolean} + */ + _isShowing: { + type: Boolean, + value: false, + }, + /** + * The `id` of the element that the hovercard is anchored to. + * + * @type {string} + */ + for: { + type: String, + observer: '_forChanged', + }, - /** - * The spacing between the top of the hovercard and the element it is - * anchored to. - * - * @type {number} - */ - offset: { - type: Number, - value: 14, - }, + /** + * The spacing between the top of the hovercard and the element it is + * anchored to. + * + * @type {number} + */ + offset: { + type: Number, + value: 14, + }, - /** - * Positions the hovercard to the top, right, bottom, left, bottom-left, - * bottom-right, top-left, or top-right of its content. - * - * @type {string} - */ - position: { - type: String, - value: 'bottom', - }, + /** + * Positions the hovercard to the top, right, bottom, left, bottom-left, + * bottom-right, top-left, or top-right of its content. + * + * @type {string} + */ + position: { + type: String, + value: 'bottom', + }, - container: Object, - /** - * ID for the container element. - * - * @type {string} - */ - containerId: { - type: String, - value: 'gr-hovercard-container', - }, - }, - - listeners: { - mouseleave: 'hide', - }, + container: Object, + /** + * ID for the container element. + * + * @type {string} + */ + containerId: { + type: String, + value: 'gr-hovercard-container', + }, + }; + } attached() { + super.attached(); if (!this._target) { this._target = this.target; } this.listen(this._target, 'mouseenter', 'show'); this.listen(this._target, 'focus', 'show'); this.listen(this._target, 'mouseleave', 'hide'); this.listen(this._target, 'blur', 'hide'); this.listen(this._target, 'click', 'hide'); - }, + } + + created() { + super.created(); + this.addEventListener('mouseleave', + e => this.hide(e)); + } ready() { + super.ready(); // First, check to see if the container has already been created. this.container = Gerrit.getRootElement() .querySelector('#' + this.containerId); @@ -111,7 +119,7 @@ this.container = document.createElement('div'); this.container.setAttribute('id', this.containerId); Gerrit.getRootElement().appendChild(this.container); - }, + } removeListeners() { this.unlisten(this._target, 'mouseenter', 'show'); @@ -119,7 +127,7 @@ this.unlisten(this._target, 'mouseleave', 'hide'); this.unlisten(this._target, 'blur', 'hide'); this.unlisten(this._target, 'click', 'hide'); - }, + } /** * Returns the target element that the hovercard is anchored to (the `id` of @@ -136,11 +144,11 @@ target = Polymer.dom(ownerRoot).querySelector('#' + this.for); } else { target = parentNode.nodeType == Node.DOCUMENT_FRAGMENT_NODE ? - ownerRoot.host : - parentNode; + ownerRoot.host : + parentNode; } return target; - }, + } /** * Hides/closes the hovercard. This occurs when the user triggers the @@ -184,7 +192,7 @@ if (this.container.contains(this)) { this.container.removeChild(this); } - }, + } /** * Shows/opens the hovercard. This occurs when the user triggers the @@ -207,7 +215,7 @@ // Trigger the transition this.classList.add(HOVER_CLASS); - }, + } /** * Updates the hovercard's position based on the `position` attribute @@ -244,7 +252,7 @@ hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2; hovercardTop = targetTop - thisRect.height - this.offset; cssText += `padding-bottom:${this.offset - }px; margin-bottom:-${this.offset}px;`; + }px; margin-bottom:-${this.offset}px;`; break; case 'bottom': hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2; @@ -306,7 +314,7 @@ // Set the hovercard's position cssText += `left:${hovercardLeft}px; top:${hovercardTop}px;`; this.style.cssText = cssText; - }, + } /** * Responds to a change in the `for` value and gets the updated `target` @@ -316,6 +324,8 @@ */ _forChanged() { this._target = this.target; - }, - }); + } + } + + customElements.define(GrHovercard.is, GrHovercard); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html index 8e79f65..35d736c 100644 --- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html +++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-hovercard</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html index 7bd6f48..743923b 100644 --- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html +++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
@@ -76,6 +76,7 @@ <g id="restore"><path d="M12,8 L12,13 L16.28,15.54 L17,14.33 L13.5,12.25 L13.5,8 L12,8 Z M13,3 C8.03,3 4,7.03 4,12 L1,12 L4.89,15.89 L4.96,16.03 L9,12 L6,12 C6,8.13 9.13,5 13,5 C16.87,5 20,8.13 20,12 C20,15.87 16.87,19 13,19 C11.07,19 9.32,18.21 8.06,16.94 L6.64,18.36 C8.27,19.99 10.51,21 13,21 C17.97,21 22,16.97 22,12 C22,7.03 17.97,3 13,3 Z"></path></g> <!-- This is a custom PolyGerrit SVG --> <g id="revert"><path d="M12.3,8.5 C9.64999995,8.5 7.24999995,9.49 5.39999995,11.1 L1.79999995,7.5 L1.79999995,16.5 L10.8,16.5 L7.17999995,12.88 C8.56999995,11.72 10.34,11 12.3,11 C15.84,11 18.85,13.31 19.9,16.5 L22.27,15.72 C20.88,11.53 16.95,8.5 12.3,8.5"></path></g> + <g id="revert_submission"><path d="M12.3,8.5 C9.64999995,8.5 7.24999995,9.49 5.39999995,11.1 L1.79999995,7.5 L1.79999995,16.5 L10.8,16.5 L7.17999995,12.88 C8.56999995,11.72 10.34,11 12.3,11 C15.84,11 18.85,13.31 19.9,16.5 L22.27,15.72 C20.88,11.53 16.95,8.5 12.3,8.5"></path></g> <!-- This is a custom PolyGerrit SVG --> <g id="stopEdit"><path d="M4 4 20 4 20 20 4 20z"></path></g> <!-- This is a custom PolyGerrit SVG -->
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context.js index 76cb51f..708e5b6 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context.js
@@ -19,6 +19,7 @@ /** * Used to create a context for GrAnnotationActionsInterface. + * * @param {HTMLElement} contentEl The DIV.contentText element of the line * content to apply the annotation to using annotateRange. * @param {HTMLElement} lineNumberEl The TD element of the line number to @@ -41,6 +42,7 @@ /** * Method to add annotations to a content line. + * * @param {number} offset The char offset where the update starts. * @param {number} length The number of chars that the update covers. * @param {GrStyleObject} styleObject The style object for the range. @@ -56,6 +58,7 @@ /** * Method to add a CSS class to the line number TD element. + * * @param {GrStyleObject} styleObject The style object for the range. * @param {string} side The side of the update. ('left' or 'right') */
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html index 3223636..66e7a74 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-annotation-actions-context</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.js index 98268c5..4ba9820 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.js
@@ -37,6 +37,7 @@ * GrAnnotationActionsContext.annotateRange and * GrAnnotationActionsContext.annotateLineNumber to apply a CSS class to the * line content or the line number. + * * @param {function(GrAnnotationActionsContext)} addLayerFunc The function * that will be called when the AnnotationLayer is ready to annotate. */ @@ -48,6 +49,7 @@ /** * The specified function will be called with a notify function for the plugin * to call when it has all required data for annotation. Optional. + * * @param {function(function(String, Number, Number, String))} notifyFunc See * doc of the notify function below to see what it does. */ @@ -129,6 +131,7 @@ * The notify function will call the listeners of all required annotation * layers. Intended to be called by the plugin when all required data for * annotation is available. + * * @param {String} path The file path whose listeners should be notified. * @param {Number} start The line where the update starts. * @param {Number} end The line where the update ends. @@ -149,6 +152,7 @@ /** * Should be called to register annotation layers by the framework. Not * intended to be called by plugins. + * * @param {String} path The file path (eg: /COMMIT_MSG'). * @param {String} changeNum The Gerrit change number. * @param {String} patchNum The Gerrit patch number. @@ -156,13 +160,14 @@ GrAnnotationActionsInterface.prototype.getLayer = function( path, changeNum, patchNum) { const annotationLayer = new AnnotationLayer(path, changeNum, patchNum, - this._addLayerFunc); + this._addLayerFunc); this._annotationLayers.push(annotationLayer); return annotationLayer; }; /** * Used to create an instance of the Annotation Layer interface. + * * @param {String} path The file path (eg: /COMMIT_MSG'). * @param {String} changeNum The Gerrit change number. * @param {String} patchNum The Gerrit patch number. @@ -180,6 +185,7 @@ /** * Register a listener for layer updates. + * * @param {function(Number, Number, String)} fn The update handler function. * Should accept as arguments the line numbers for the start and end of * the update and the side as a string. @@ -190,6 +196,7 @@ /** * Layer method to add annotations to a line. + * * @param {HTMLElement} contentEl The DIV.contentText element of the line * content to apply the annotation to using annotateRange. * @param {HTMLElement} lineNumberEl The TD element of the line number to @@ -205,6 +212,7 @@ /** * Notify Layer listeners of changes to annotations. + * * @param {Number} start The line where the update starts. * @param {Number} end The line where the update ends. * @param {String} side The side of the update. ('left' or 'right')
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html index 987b551..98ff954 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-annotation-actions-js-api-js-api</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js index 4123f70..f5e720a 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js
@@ -35,6 +35,7 @@ /** * Retrieves the name of the plugin base on the url. + * * @param {string|URL} url */ function getPluginNameFromUrl(url) { @@ -50,7 +51,11 @@ return url.pathname; } const base = Gerrit.BaseUrlBehavior.getBaseUrl(); - const pathname = url.pathname.replace(base, ''); + let pathname = url.pathname.replace(base, ''); + // Load from ASSETS_PATH + if (window.ASSETS_PATH && url.href.includes(window.ASSETS_PATH)) { + pathname = url.href.replace(window.ASSETS_PATH, ''); + } // Site theme is server from predefined path. if (pathname === '/static/gerrit-theme.html') { return 'gerrit-theme'; @@ -90,7 +95,6 @@ }); } - // TEST only methods / properties function testOnly_resetInternalState() {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html index c407aa8..b43796f 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-api-interface</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -29,7 +29,6 @@ <script>void(0);</script> <script> - const PRELOADED_PROTOCOL = 'preloaded:'; suite('gr-api-utils tests', () => { @@ -73,6 +72,15 @@ 'gerrit-theme' ); }); + + test('with ASSETS_PATH', () => { + window.ASSETS_PATH = 'http://cdn.com/2'; + assert.equal( + getPluginNameFromUrl(`${window.ASSETS_PATH}/plugins/a.html`), + 'a' + ); + window.ASSETS_PATH = undefined; + }); }); }); </script> \ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js index 407a6a8..5658245 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
@@ -20,6 +20,7 @@ /** * Ensure GrChangeActionsInterface instance has access to gr-change-actions * element and retrieve if the interface was created before element. + * * @param {!GrChangeActionsInterface} api */ function ensureEl(api) { @@ -32,6 +33,7 @@ /** * Set gr-change-actions element to a GrChangeActionsInterface instance. + * * @param {!GrChangeActionsInterface} api * @param {!Element} el gr-change-actions */
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html index 7332877..cbe2d37 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-actions-js-api</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js index e86ba4d..e6a3f40 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
@@ -20,6 +20,7 @@ /** * Ensure GrChangeReplyInterface instance has access to gr-reply-dialog * element and retrieve if the interface was created before element. + * * @param {!GrChangeReplyInterfaceOld} api */ function ensureEl(api) { @@ -55,8 +56,8 @@ function GrChangeReplyInterface(plugin, el) { GrChangeReplyInterfaceOld.call(this, el); this.plugin = plugin; - this._hookName = (plugin.getPluginName() || 'test') + '-autogenerated-' - + String(Math.random()).split('.')[1]; + this._hookName = (plugin.getPluginName() || 'test') + '-autogenerated-' + + String(Math.random()).split('.')[1]; } GrChangeReplyInterface.prototype._hookName = ''; GrChangeReplyInterface.prototype._hookClass = null;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html index 842a2fe..7955aa4 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-reply-js-api</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js index 7a23fdb..73a2480 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js
@@ -15,18 +15,18 @@ * limitations under the License. */ - /** - * This defines the Gerrit instance. All methods directly attached to Gerrit - * should be defined or linked here. - */ +/** + * This defines the Gerrit instance. All methods directly attached to Gerrit + * should be defined or linked here. + */ (function(window) { 'use strict'; // Import utils methods const { - send, - getRestAPI, + send, + getRestAPI, } = window._apiUtils; /** @@ -54,7 +54,7 @@ testOnly_resetInternalState, } = window._apiUtils; Gerrit._testOnly_installPreloadedPlugins = (...args) => Gerrit._pluginLoader - .installPreloadedPlugins(...args); + .installPreloadedPlugins(...args); Gerrit._testOnly_flushPreinstalls = flushPreinstalls; Gerrit._testOnly_resetPlugins = () => { testOnly_resetInternalState();
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html index e81b8aa..7b03308 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-api-interface</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js index 70a7a01..b1675cc 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
@@ -25,6 +25,7 @@ COMMIT_MSG_EDIT: 'commitmsgedit', COMMENT: 'comment', REVERT: 'revert', + REVERT_SUBMISSION: 'revert_submission', POST_REVERT: 'postrevert', ANNOTATE_DIFF: 'annotatediff', ADMIN_MENU_LINKS: 'admin-menu-links', @@ -36,24 +37,34 @@ REPLY_DIALOG: 'replydialog', }; - Polymer({ - is: 'gr-js-api-interface', + /** + * @appliesMixin Gerrit.PatchSetMixin + */ + class GrJsApiInterface extends Polymer.mixinBehaviors( [ + Gerrit.PatchSetBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-js-api-interface'; } - properties: { - _elements: { - type: Object, - value: {}, // Shared across all instances. - }, - _eventCallbacks: { - type: Object, - value: {}, // Shared across all instances. - }, - }, + constructor() { + super(); + this.Element = Element; + this.EventType = EventType; + } - behaviors: [Gerrit.PatchSetBehavior], - - Element, - EventType, + static get properties() { + return { + _elements: { + type: Object, + value: {}, // Shared across all instances. + }, + _eventCallbacks: { + type: Object, + value: {}, // Shared across all instances. + }, + }; + } handleEvent(type, detail) { Gerrit.awaitPluginsLoaded().then(() => { @@ -79,22 +90,22 @@ break; } }); - }, + } addElement(key, el) { this._elements[key] = el; - }, + } getElement(key) { return this._elements[key]; - }, + } addEventCallback(eventName, callback) { if (!this._eventCallbacks[eventName]) { this._eventCallbacks[eventName] = []; } this._eventCallbacks[eventName].push(callback); - }, + } canSubmitChange(change, revision) { const submitCallbacks = this._getEventCallbacks(EventType.SUBMIT_CHANGE); @@ -108,14 +119,14 @@ }); return !cancelSubmit; - }, + } _removeEventCallbacks() { for (const k in EventType) { if (!EventType.hasOwnProperty(k)) { continue; } this._eventCallbacks[EventType[k]] = []; } - }, + } _handleHistory(detail) { for (const cb of this._getEventCallbacks(EventType.HISTORY)) { @@ -125,7 +136,7 @@ console.error(err); } } - }, + } _handleShowChange(detail) { // Note (issue 8221) Shallow clone the change object and add a mergeable @@ -160,7 +171,7 @@ console.error(err); } } - }, + } handleCommitMessage(change, msg) { for (const cb of this._getEventCallbacks(EventType.COMMIT_MSG_EDIT)) { @@ -170,7 +181,7 @@ console.error(err); } } - }, + } _handleComment(detail) { for (const cb of this._getEventCallbacks(EventType.COMMENT)) { @@ -180,7 +191,7 @@ console.error(err); } } - }, + } _handleLabelChange(detail) { for (const cb of this._getEventCallbacks(EventType.LABEL_CHANGE)) { @@ -190,7 +201,7 @@ console.error(err); } } - }, + } _handleHighlightjsLoaded(detail) { for (const cb of this._getEventCallbacks(EventType.HIGHLIGHTJS_LOADED)) { @@ -200,7 +211,7 @@ console.error(err); } } - }, + } modifyRevertMsg(change, revertMsg, origMsg) { for (const cb of this._getEventCallbacks(EventType.REVERT)) { @@ -211,12 +222,23 @@ } } return revertMsg; - }, + } + + modifyRevertSubmissionMsg(change, revertSubmissionMsg, origMsg) { + for (const cb of this._getEventCallbacks(EventType.REVERT_SUBMISSION)) { + try { + revertSubmissionMsg = cb(change, revertSubmissionMsg, origMsg); + } catch (err) { + console.error(err); + } + } + return revertSubmissionMsg; + } getDiffLayers(path, changeNum, patchNum) { const layers = []; for (const annotationApi of - this._getEventCallbacks(EventType.ANNOTATE_DIFF)) { + this._getEventCallbacks(EventType.ANNOTATE_DIFF)) { try { const layer = annotationApi.getLayer(path, changeNum, patchNum); layers.push(layer); @@ -225,44 +247,31 @@ } } return layers; - }, + } /** * Retrieves coverage data possibly provided by a plugin. * * Will wait for plugins to be loaded. If multiple plugins offer a coverage - * provider, the first one is used. If no plugin offers a coverage provider, - * will resolve to []. + * provider, the first one is returned. If no plugin offers a coverage provider, + * will resolve to null. * - * @param {string|number} changeNum - * @param {string} path - * @param {string|number} basePatchNum - * @param {string|number} patchNum - * @return {!Promise<!Array<!Gerrit.CoverageRange>>} + * @return {!Promise<?GrAnnotationActionsInterface>} */ - getCoverageRanges(changeNum, path, basePatchNum, patchNum) { - return Gerrit.awaitPluginsLoaded().then(() => { - for (const annotationApi of - this._getEventCallbacks(EventType.ANNOTATE_DIFF)) { - const provider = annotationApi.getCoverageProvider(); - // Only one coverage provider makes sense. If there are more, then we - // simply ignore them. - if (provider) { - return provider(changeNum, path, basePatchNum, patchNum); - } - } - return []; - }); - }, + getCoverageAnnotationApi() { + return Gerrit.awaitPluginsLoaded() + .then(() => this._getEventCallbacks(EventType.ANNOTATE_DIFF) + .find(api => api.getCoverageProvider())); + } getAdminMenuLinks() { const links = []; for (const adminApi of - this._getEventCallbacks(EventType.ADMIN_MENU_LINKS)) { + this._getEventCallbacks(EventType.ADMIN_MENU_LINKS)) { links.push(...adminApi.getMenuLinks()); } return links; - }, + } getLabelValuesPostRevert(change) { let labels = {}; @@ -274,10 +283,12 @@ } } return labels; - }, + } _getEventCallbacks(type) { return this._eventCallbacks[type] || []; - }, - }); + } + } + + customElements.define(GrJsApiInterface.is, GrJsApiInterface); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html index ae12940..bdce91f 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-api-interface</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -82,6 +82,28 @@ 'http://test.com/plugins/testplugin/static/test.js'); }); + test('url for preloaded plugin without ASSETS_PATH', () => { + let plugin; + Gerrit.install(p => { plugin = p; }, '0.1', + 'preloaded:testpluginB'); + assert.equal(plugin.url(), + `${window.location.origin}/plugins/testpluginB/`); + assert.equal(plugin.url('/static/test.js'), + `${window.location.origin}/plugins/testpluginB/static/test.js`); + }); + + test('url for preloaded plugin without ASSETS_PATH', () => { + const oldAssetsPath = window.ASSETS_PATH; + window.ASSETS_PATH = 'http://test.com'; + let plugin; + Gerrit.install(p => { plugin = p; }, '0.1', + 'preloaded:testpluginC'); + assert.equal(plugin.url(), `${window.ASSETS_PATH}/plugins/testpluginC/`); + assert.equal(plugin.url('/static/test.js'), + `${window.ASSETS_PATH}/plugins/testpluginC/static/test.js`); + window.ASSETS_PATH = oldAssetsPath; + }); + test('_send on failure rejects with response text', () => { sendStub.returns(Promise.resolve( {status: 400, text() { return Promise.resolve('text'); }}));
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html index 6da117f..f7b7cf8 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-plugin-action-context</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js index 8832a3f..5f5a74e 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js
@@ -33,7 +33,7 @@ GrPluginEndpoints.prototype._getOrCreateModuleInfo = function(plugin, endpoint, type, moduleName, domHook) { const existingModule = this._endpoints[endpoint].find(info => - info.plugin === plugin && + info.plugin === plugin && info.moduleName === moduleName && info.domHook === domHook ); @@ -86,6 +86,7 @@ /** * Get detailed information about modules registered with an extension * endpoint. + * * @param {string} name Endpoint name. * @param {?{ * type: (string|undefined), @@ -112,6 +113,7 @@ /** * Get detailed module names for instantiating at the endpoint. + * * @param {string} name Endpoint name. * @param {?{ * type: (string|undefined), @@ -129,6 +131,7 @@ /** * Get .html plugin URLs with element and module definitions. + * * @param {string} name Endpoint name. * @param {?{ * type: (string|undefined),
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html index 8ed7f14..b39ed6d 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-plugin-endpoints</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js index fef026a..8a09641 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
@@ -20,10 +20,10 @@ // Import utils methods const { - PLUGIN_LOADING_TIMEOUT_MS, - PRELOADED_PROTOCOL, - getPluginNameFromUrl, - getBaseUrl, + PLUGIN_LOADING_TIMEOUT_MS, + PRELOADED_PROTOCOL, + getPluginNameFromUrl, + getBaseUrl, } = window._apiUtils; /** @@ -95,6 +95,7 @@ /** * Use the plugin name or use the full url if not recognized. + * * @see gr-api-utils#getPluginNameFromUrl * @param {string|URL} url */ @@ -113,7 +114,7 @@ this._pluginListLoaded = true; plugins.forEach(path => { - const url = this._urlFor(path); + const url = this._urlFor(path, window.ASSETS_PATH); // Skip if preloaded, for bundling. if (this.isPluginPreloaded(url)) return; @@ -128,11 +129,11 @@ }); if (this._isPathEndsWith(url, '.html')) { - this._importHtmlPlugin(url, opts && opts[path]); + this._importHtmlPlugin(path, opts && opts[path]); } else if (this._isPathEndsWith(url, '.js')) { - this._loadJsPlugin(url); + this._loadJsPlugin(path); } else { - this._failToLoad(`Unrecognized plugin url ${url}`, url); + this._failToLoad(`Unrecognized plugin path ${path}`, path); } }); @@ -181,14 +182,15 @@ return; } - const pluginObject = this.getPlugin(src); + const url = this._urlFor(src); + const pluginObject = this.getPlugin(url); let plugin = pluginObject && pluginObject.plugin; if (!plugin) { - plugin = new Plugin(src); + plugin = new Plugin(url); } try { callback(plugin); - this._pluginInstalled(src, plugin); + this._pluginInstalled(url, plugin); } catch (e) { this._failToLoad(`${e.name}: ${e.message}`, src); } @@ -282,6 +284,7 @@ /** * Checks if given plugin path/url is enabled or not. + * * @param {string} pathOrUrl */ isPluginEnabled(pathOrUrl) { @@ -293,6 +296,7 @@ /** * Returns the plugin object with a given url. + * * @param {string} pathOrUrl */ getPlugin(pathOrUrl) { @@ -302,6 +306,7 @@ /** * Checks if given plugin path/url is loaded or not. + * * @param {string} pathOrUrl */ isPluginLoaded(pathOrUrl) { @@ -313,38 +318,79 @@ } _importHtmlPlugin(pluginUrl, opts = {}) { - // onload (second param) needs to be a function. When null or undefined - // were passed, plugins were not loaded correctly. + const urlWithAP = this._urlFor(pluginUrl, window.ASSETS_PATH); + const urlWithoutAP = this._urlFor(pluginUrl); + let onerror = null; + if (urlWithAP !== urlWithoutAP) { + onerror = () => this._loadHtmlPlugin(urlWithoutAP, opts.sync); + } + this._loadHtmlPlugin(urlWithAP, opts.sync, onerror); + } + + _loadHtmlPlugin(url, sync, onerror) { + if (!onerror) { + onerror = () => { + this._failToLoad(`${url} import error`, url); + }; + } + (Polymer.importHref || Polymer.Base.importHref)( - this._urlFor(pluginUrl), () => {}, - () => this._failToLoad(`${pluginUrl} import error`, pluginUrl), - !opts.sync); + url, () => {}, + onerror, + !sync); } _loadJsPlugin(pluginUrl) { - this._createScriptTag(this._urlFor(pluginUrl)); + const urlWithAP = this._urlFor(pluginUrl, window.ASSETS_PATH); + const urlWithoutAP = this._urlFor(pluginUrl); + let onerror = null; + if (urlWithAP !== urlWithoutAP) { + onerror = () => this._createScriptTag(urlWithoutAP); + } + + this._createScriptTag(urlWithAP, onerror); } - _createScriptTag(url) { + _createScriptTag(url, onerror) { + if (!onerror) { + onerror = () => this._failToLoad(`${url} load error`, url); + } + const el = document.createElement('script'); el.defer = true; - el.src = url; - el.onerror = () => this._failToLoad(`${url} load error`, url); + el.setAttribute('src', url); + el.onerror = onerror; return document.body.appendChild(el); } - _urlFor(pathOrUrl) { + _urlFor(pathOrUrl, assetsPath) { if (!pathOrUrl) { return pathOrUrl; } + + // theme is per host, should always load from assetsPath + const isThemeFile = pathOrUrl.endsWith('static/gerrit-theme.html'); + const shouldTryLoadFromAssetsPathFirst = !isThemeFile && assetsPath; if (pathOrUrl.startsWith(PRELOADED_PROTOCOL) || pathOrUrl.startsWith('http')) { // Plugins are loaded from another domain or preloaded. + if (pathOrUrl.includes(location.host) && + shouldTryLoadFromAssetsPathFirst) { + // if is loading from host server, try replace with cdn when assetsPath provided + return pathOrUrl + .replace(location.origin, assetsPath); + } return pathOrUrl; } + if (!pathOrUrl.startsWith('/')) { pathOrUrl = '/' + pathOrUrl; } + + if (shouldTryLoadFromAssetsPathFirst) { + return assetsPath + pathOrUrl; + } + return window.location.origin + getBaseUrl() + pathOrUrl; }
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html index ee54319..151c340 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-plugin-host</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -325,11 +325,11 @@ let loadJsPluginStub; setup(() => { importHtmlPluginStub = sandbox.stub(); - sandbox.stub(Gerrit._pluginLoader, '_importHtmlPlugin', url => { + sandbox.stub(Gerrit._pluginLoader, '_loadHtmlPlugin', url => { importHtmlPluginStub(url); }); loadJsPluginStub = sandbox.stub(); - sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => { + sandbox.stub(Gerrit._pluginLoader, '_createScriptTag', url => { loadJsPluginStub(url); }); }); @@ -346,8 +346,8 @@ assert.isTrue(failToLoadStub.calledOnce); assert.isTrue(failToLoadStub.calledWithExactly( - `Unrecognized plugin url ${url}/foo/bar`, - `${url}/foo/bar` + 'Unrecognized plugin path foo/bar', + 'foo/bar' )); }); @@ -367,7 +367,6 @@ ); }); - test('relative path should honor getBaseUrl', () => { const testUrl = '/test'; sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl', () => { @@ -408,6 +407,72 @@ }); }); + suite('With ASSETS_PATH', () => { + let importHtmlPluginStub; + let loadJsPluginStub; + setup(() => { + window.ASSETS_PATH = 'https://cdn.com'; + importHtmlPluginStub = sandbox.stub(); + sandbox.stub(Gerrit._pluginLoader, '_loadHtmlPlugin', url => { + importHtmlPluginStub(url); + }); + loadJsPluginStub = sandbox.stub(); + sandbox.stub(Gerrit._pluginLoader, '_createScriptTag', url => { + loadJsPluginStub(url); + }); + }); + + teardown(() => { + window.ASSETS_PATH = ''; + }); + + test('Should try load plugins from assets path instead', () => { + Gerrit._loadPlugins([ + 'foo/bar.js', + 'foo/bar.html', + ]); + + assert.isTrue(importHtmlPluginStub.calledOnce); + assert.isTrue( + importHtmlPluginStub.calledWithExactly(`https://cdn.com/foo/bar.html`) + ); + assert.isTrue(loadJsPluginStub.calledOnce); + assert.isTrue( + loadJsPluginStub.calledWithExactly(`https://cdn.com/foo/bar.js`)); + }); + + test('Should honor original path if exists', () => { + Gerrit._loadPlugins([ + 'http://e.com/foo/bar.html', + 'http://e.com/foo/bar.js', + ]); + + assert.isTrue(importHtmlPluginStub.calledOnce); + assert.isTrue( + importHtmlPluginStub.calledWithExactly(`http://e.com/foo/bar.html`) + ); + assert.isTrue(loadJsPluginStub.calledOnce); + assert.isTrue( + loadJsPluginStub.calledWithExactly(`http://e.com/foo/bar.js`)); + }); + + test('Should try replace current host with assetsPath', () => { + const host = window.location.origin; + Gerrit._loadPlugins([ + `${host}/foo/bar.html`, + `${host}/foo/bar.js`, + ]); + + assert.isTrue(importHtmlPluginStub.calledOnce); + assert.isTrue( + importHtmlPluginStub.calledWithExactly(`https://cdn.com/foo/bar.html`) + ); + assert.isTrue(loadJsPluginStub.calledOnce); + assert.isTrue( + loadJsPluginStub.calledWithExactly(`https://cdn.com/foo/bar.js`)); + }); + }); + test('adds js plugins will call the body', () => { Gerrit._loadPlugins([ 'http://e.com/foo/bar.js', @@ -490,12 +555,10 @@ test('installing preloaded plugin', () => { let plugin; - window.ASSETS_PATH = 'http://blips.com/chitz'; Gerrit.install(p => { plugin = p; }, '0.1', 'preloaded:foo'); assert.strictEqual(plugin.getPluginName(), 'foo'); assert.strictEqual(plugin.url('/some/thing.html'), - 'http://blips.com/chitz/plugins/foo/some/thing.html'); - delete window.ASSETS_PATH; + `${window.location.origin}/plugins/foo/some/thing.html`); }); }); });
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js index af8bd9e..ecfafb5 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js
@@ -61,6 +61,7 @@ /** * Fetch and return native browser REST API Response. + * * @param {string} method HTTP Method (GET, POST, etc) * @param {string} url URL without base path or plugin prefix * @param {Object=} payload Respected for POST and PUT only. @@ -76,6 +77,7 @@ /** * Fetch and parse REST API response, if request succeeds. + * * @param {string} method HTTP Method (GET, POST, etc) * @param {string} url URL without base path or plugin prefix * @param {Object=} payload Respected for POST and PUT only.
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html index bcbd961..4aae032 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-plugin-rest-api</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js index d76c983..3d051f8 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -17,8 +17,6 @@ (function(window) { 'use strict'; - const PRELOADED_PROTOCOL = 'preloaded:'; - const PANEL_ENDPOINTS_MAPPING = { CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK: 'change-view-integration', CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK: 'change-metadata-item', @@ -26,8 +24,9 @@ // Import utils methods const { - getPluginNameFromUrl, - send, + PRELOADED_PROTOCOL, + getPluginNameFromUrl, + send, } = window._apiUtils; /** @@ -66,13 +65,6 @@ this._url = new URL(opt_url); this._name = getPluginNameFromUrl(this._url); - if (this._url.protocol === PRELOADED_PROTOCOL) { - // Original plugin URL is used in plugin assets URLs calculation. - const assetsBaseUrl = window.ASSETS_PATH || - (window.location.origin + Gerrit.BaseUrlBehavior.getBaseUrl()); - this._url = new URL(assetsBaseUrl + '/plugins/' + this._name + - '/static/' + this._name + '.js'); - } } Plugin._sharedAPIElement = document.createElement('gr-js-api-interface'); @@ -90,7 +82,7 @@ /** * Registers an endpoint for the plugin. - */ + */ Plugin.prototype.registerCustomComponent = function( endpointName, opt_moduleName, opt_options) { return this._registerCustomComponent(endpointName, opt_moduleName, @@ -102,7 +94,7 @@ * * Dynamic plugins are registered by specific prefix, such as * 'change-list-header'. - */ + */ Plugin.prototype.registerDynamicCustomComponent = function( endpointName, opt_moduleName, opt_options) { const fullEndpointName = `${endpointName}-${this.getPluginName()}`; @@ -113,7 +105,7 @@ Plugin.prototype._registerCustomComponent = function( endpointName, opt_moduleName, opt_options, dynamicEndpoint) { const type = opt_options && opt_options.replace ? - EndpointType.REPLACE : EndpointType.DECORATE; + EndpointType.REPLACE : EndpointType.DECORATE; const hook = this._domHooks.getDomHook(endpointName, opt_moduleName); const moduleName = opt_moduleName || hook.getModuleName(); Gerrit._endpoints.registerModule( @@ -139,9 +131,15 @@ Plugin.prototype.url = function(opt_path) { const relPath = '/plugins/' + this._name + (opt_path || '/'); + const sameOriginPath = window.location.origin + + `${Gerrit.BaseUrlBehavior.getBaseUrl()}${relPath}`; if (window.location.origin === this._url.origin) { // Plugin loaded from the same origin as gr-app, getBaseUrl in effect. - return this._url.origin + Gerrit.BaseUrlBehavior.getBaseUrl() + relPath; + return sameOriginPath; + } else if (this._url.protocol === PRELOADED_PROTOCOL) { + // Plugin is preloaded, load plugin with ASSETS_PATH or location.origin + return window.ASSETS_PATH ? `${window.ASSETS_PATH}${relPath}` : + sameOriginPath; } else { // Plugin loaded from assets bundle, expect assets placed along with it. return this._url.href.split('/plugins/' + this._name)[0] + relPath; @@ -184,14 +182,14 @@ Plugin.prototype.changeActions = function() { return new GrChangeActionsInterface(this, - Plugin._sharedAPIElement.getElement( - Plugin._sharedAPIElement.Element.CHANGE_ACTIONS)); + Plugin._sharedAPIElement.getElement( + Plugin._sharedAPIElement.Element.CHANGE_ACTIONS)); }; Plugin.prototype.changeReply = function() { return new GrChangeReplyInterface(this, - Plugin._sharedAPIElement.getElement( - Plugin._sharedAPIElement.Element.REPLY_DIALOG)); + Plugin._sharedAPIElement.getElement( + Plugin._sharedAPIElement.Element.REPLY_DIALOG)); }; Plugin.prototype.changeView = function() { @@ -224,6 +222,7 @@ /** * To make REST requests for plugin-provided endpoints, use + * * @example * const pluginRestApi = plugin.restApi(plugin.url()); *
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.html b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.html index 63a528e..b17b251 100644 --- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.html +++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.html
@@ -28,7 +28,9 @@ <dom-module id="gr-label-info"> <template strip-whitespace> - <style include="gr-voting-styles"></style> + <style include="gr-voting-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style include="shared-styles"> .placeholder { color: var(--deemphasized-text-color);
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js index 3eb44e6..0e529b1 100644 --- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js +++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
@@ -17,17 +17,21 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-label-info', + class GrLabelInfo extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-label-info'; } - properties: { - labelInfo: Object, - label: String, - /** @type {?} */ - change: Object, - account: Object, - mutable: Boolean, - }, + static get properties() { + return { + labelInfo: Object, + label: String, + /** @type {?} */ + change: Object, + account: Object, + mutable: Boolean, + }; + } /** * @param {!Object} labelInfo @@ -85,7 +89,7 @@ } } return result; - }, + } /** * A user is able to delete a vote iff the mutable property is true and the @@ -106,7 +110,7 @@ return ''; } return 'hidden'; - }, + } /** * Closure annotation for Polymer.prototype.splice is off. @@ -125,22 +129,22 @@ const accountID = parseInt(target.getAttribute('data-account-id'), 10); this._xhrPromise = this.$.restAPI.deleteVote(this.change._number, accountID, this.label) - .then(response => { - target.disabled = false; - if (!response.ok) { return; } - Gerrit.Nav.navigateToChange(this.change); - }).catch(err => { - target.disabled = false; - return; - }); - }, + .then(response => { + target.disabled = false; + if (!response.ok) { return; } + Gerrit.Nav.navigateToChange(this.change); + }).catch(err => { + target.disabled = false; + return; + }); + } _computeValueTooltip(labelInfo, score) { if (!labelInfo || !labelInfo.values || !labelInfo.values[score]) { return ''; } return labelInfo.values[score]; - }, + } /** * @param {!Object} labelInfo @@ -156,6 +160,8 @@ } } return ''; - }, - }); + } + } + + customElements.define(GrLabelInfo.is, GrLabelInfo); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html index 35dc772..adf88d9 100644 --- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html +++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
@@ -17,7 +17,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-label-info</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label.js b/polygerrit-ui/app/elements/shared/gr-label/gr-label.js index 0de0881..440e8e9 100644 --- a/polygerrit-ui/app/elements/shared/gr-label/gr-label.js +++ b/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
@@ -17,11 +17,16 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-label', + /** + * @appliesMixin Gerrit.TooltipMixin + */ + class GrLabel extends Polymer.mixinBehaviors( [ + Gerrit.TooltipBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-label'; } + } - behaviors: [ - Gerrit.TooltipBehavior, - ], - }); + customElements.define(GrLabel.is, GrLabel); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html index da0b93f..47be6f7 100644 --- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html +++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html
@@ -38,14 +38,7 @@ #body { display: flex; } - gr-autocomplete { - height: 1.5em; - --gr-autocomplete: { - border: none; - } - } #trigger { - border-left: 1px solid var(--deemphasized-text-color); color: var(--deemphasized-text-color); cursor: pointer; padding-left: var(--spacing-s);
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js index fd0f228..c02c598 100644 --- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js +++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
@@ -17,60 +17,65 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-labeled-autocomplete', - + class GrLabeledAutocomplete extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-labeled-autocomplete'; } /** * Fired when a value is chosen. * * @event commit */ - properties: { + static get properties() { + return { - /** - * Used just like the query property of gr-autocomplete. - * - * @type {function(string): Promise<?>} - */ - query: { - type: Function, - value() { - return function() { - return Promise.resolve([]); - }; + /** + * Used just like the query property of gr-autocomplete. + * + * @type {function(string): Promise<?>} + */ + query: { + type: Function, + value() { + return function() { + return Promise.resolve([]); + }; + }, }, - }, - text: { - type: String, - value: '', - notify: true, - }, - label: String, - placeholder: String, - disabled: Boolean, + text: { + type: String, + value: '', + notify: true, + }, + label: String, + placeholder: String, + disabled: Boolean, - _autocompleteThreshold: { - type: Number, - value: 0, - readOnly: true, - }, - }, + _autocompleteThreshold: { + type: Number, + value: 0, + readOnly: true, + }, + }; + } _handleTriggerClick(e) { // Stop propagation here so we don't confuse gr-autocomplete, which // listens for taps on body to try to determine when it's blurred. e.stopPropagation(); this.$.autocomplete.focus(); - }, + } setText(text) { this.$.autocomplete.setText(text); - }, + } clear() { this.setText(''); - }, - }); + } + } + + customElements.define(GrLabeledAutocomplete.is, GrLabeledAutocomplete); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html index b257746..7e8f73f 100644 --- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html +++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html
@@ -17,7 +17,7 @@ --> <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-labeled-autocomplete</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js index 853a299..20b239e 100644 --- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js +++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
@@ -20,26 +20,31 @@ const HLJS_PATH = 'bower_components/highlightjs/highlight.min.js'; const DARK_THEME_PATH = 'styles/themes/dark-theme.html'; - Polymer({ - is: 'gr-lib-loader', + class GrLibLoader extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-lib-loader'; } - properties: { - _hljsState: { - type: Object, + static get properties() { + return { + _hljsState: { + type: Object, - // NOTE: intended singleton. - value: { - configured: false, - loading: false, - callbacks: [], + // NOTE: intended singleton. + value: { + configured: false, + loading: false, + callbacks: [], + }, }, - }, - }, + }; + } /** * Get the HLJS library. Returns a promise that resolves with a reference to * the library after it's been loaded. The promise resolves immediately if * it's already been loaded. + * * @return {!Promise<Object>} */ getHLJS() { @@ -59,11 +64,12 @@ this._hljsState.callbacks.push(resolve); }); - }, + } /** * Loads the dark theme document. Returns a promise that resolves with a * custom-style DOM element. + * * @return {!Promise<Element>} * @suppress {checkTypes} */ @@ -71,12 +77,15 @@ return new Promise((resolve, reject) => { (this.importHref || Polymer.importHref)( this._getLibRoot() + DARK_THEME_PATH, () => { - const module = document.createElement('style', 'custom-style'); + const module = document.createElement('style'); module.setAttribute('include', 'dark-theme'); - resolve(module); + const cs = document.createElement('custom-style'); + cs.appendChild(module); + + resolve(cs); }); }); - }, + } /** * Execute callbacks awaiting the HLJS lib load. @@ -91,11 +100,12 @@ cb(lib); } this._hljsState.callbacks = []; - }, + } /** * Get the HLJS library, assuming it has been loaded. Configure the library * if it hasn't already been configured. + * * @return {!Object} */ _getHighlightLib() { @@ -106,11 +116,12 @@ lib.configure({classPrefix: 'gr-diff gr-syntax gr-syntax-'}); } return lib; - }, + } /** * Get the resource path used to load the application. If the application * was loaded through a CDN, then this will be the path to CDN resources. + * * @return {string} */ _getLibRoot() { @@ -118,10 +129,11 @@ return window.STATIC_RESOURCE_PATH + '/'; } return '/'; - }, + } /** * Load and execute a JS file from the lib root. + * * @param {string} src The path to the JS file without the lib root. * @return {Promise} a promise that resolves when the script's onload * executes. @@ -140,12 +152,14 @@ script.onerror = reject; Polymer.dom(document.head).appendChild(script); }); - }, + } _getHLJSUrl() { const root = this._getLibRoot(); if (!root) { return null; } return root + HLJS_PATH; - }, - }); + } + } + + customElements.define(GrLibLoader.is, GrLibLoader); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html index 10d1608..832a558 100644 --- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html +++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-lib-loader</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js index 048e4f5..a57f331 100644 --- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js +++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
@@ -17,57 +17,63 @@ (function() { 'use strict'; + /** + * @appliesMixin Gerrit.TooltipMixin + */ /* * The gr-limited-text element is for displaying text with a maximum length * (in number of characters) to display. If the length of the text exceeds the * configured limit, then an ellipsis indicates that the text was truncated * and a tooltip containing the full text is enabled. */ + class GrLimitedText extends Polymer.mixinBehaviors( [ + Gerrit.TooltipBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-limited-text'; } - Polymer({ - is: 'gr-limited-text', - - properties: { + static get properties() { + return { /** The un-truncated text to display. */ - text: String, + text: String, - /** The maximum length for the text to display before truncating. */ - limit: { - type: Number, - value: null, - }, + /** The maximum length for the text to display before truncating. */ + limit: { + type: Number, + value: null, + }, - /** Boolean property used by Gerrit.TooltipBehavior. */ - hasTooltip: { - type: Boolean, - value: false, - }, + /** Boolean property used by Gerrit.TooltipBehavior. */ + hasTooltip: { + type: Boolean, + value: false, + }, - /** - * Disable the tooltip. - * When set to true, will not show tooltip even text is over limit - */ - disableTooltip: { - type: Boolean, - value: false, - }, + /** + * Disable the tooltip. + * When set to true, will not show tooltip even text is over limit + */ + disableTooltip: { + type: Boolean, + value: false, + }, - /** - * The maximum number of characters to display in the tooltop. - */ - tooltipLimit: { - type: Number, - value: 1024, - }, - }, + /** + * The maximum number of characters to display in the tooltop. + */ + tooltipLimit: { + type: Number, + value: 1024, + }, + }; + } - observers: [ - '_updateTitle(text, limit, tooltipLimit)', - ], - - behaviors: [ - Gerrit.TooltipBehavior, - ], + static get observers() { + return [ + '_updateTitle(text, limit, tooltipLimit)', + ]; + } /** * The text or limit have changed. Recompute whether a tooltip needs to be @@ -85,13 +91,15 @@ } else { this.removeAttribute('title'); } - }, + } _computeDisplayText(text, limit) { if (!!limit && !!text && text.length > limit) { return text.substr(0, limit - 1) + '…'; } return text; - }, - }); + } + } + + customElements.define(GrLimitedText.is, GrLimitedText); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html index 7946bb6..3ba30d1 100644 --- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html +++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-limited-text</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js index 33a9c25..bef9698 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js +++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
@@ -17,41 +17,48 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-linked-chip', + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrLinkedChip extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-linked-chip'; } - properties: { - href: String, - disabled: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - removable: { - type: Boolean, - value: false, - }, - text: String, - transparentBackground: { - type: Boolean, - value: false, - }, + static get properties() { + return { + href: String, + disabled: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + removable: { + type: Boolean, + value: false, + }, + text: String, + transparentBackground: { + type: Boolean, + value: false, + }, - /** If provided, sets the maximum length of the content. */ - limit: Number, - }, - - behaviors: [ - Gerrit.FireBehavior, - ], + /** If provided, sets the maximum length of the content. */ + limit: Number, + }; + } _getBackgroundClass(transparent) { return transparent ? 'transparentBackground' : ''; - }, + } _handleRemoveTap(e) { e.preventDefault(); this.fire('remove'); - }, - }); + } + } + + customElements.define(GrLinkedChip.is, GrLinkedChip); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html index 22a2eaf..733e897 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html +++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-linked-chip</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js index e7247b9..5d2a1ca 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js +++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
@@ -17,31 +17,37 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-linked-text', + class GrLinkedText extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-linked-text'; } - properties: { - removeZeroWidthSpace: Boolean, - content: { - type: String, - observer: '_contentChanged', - }, - pre: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - disabled: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - config: Object, - }, + static get properties() { + return { + removeZeroWidthSpace: Boolean, + content: { + type: String, + observer: '_contentChanged', + }, + pre: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + disabled: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + config: Object, + }; + } - observers: [ - '_contentOrConfigChanged(content, config)', - ], + static get observers() { + return [ + '_contentOrConfigChanged(content, config)', + ]; + } _contentChanged(content) { // In the case where the config may not be set (perhaps due to the @@ -49,11 +55,12 @@ // prevent waiting on the config to display the text. if (this.config != null) { return; } this.$.output.textContent = content; - }, + } /** * Because either the source text or the linkification config has changed, * the content should be re-parsed. + * * @param {string|null|undefined} content The raw, un-linkified source * string to parse. * @param {Object|null|undefined} config The server config specifying @@ -74,7 +81,7 @@ anchor.setAttribute('target', '_blank'); anchor.setAttribute('rel', 'noopener'); }); - }, + } /** * This method is called when the GrLikTextParser emits a partial result @@ -84,6 +91,7 @@ * element should be created and attached to the resulting DOM. * - To attach an arbitrary fragment: when called with only the `fragment` * argument, the fragment should be attached to the resulting DOM as is. + * * @param {string|null} text * @param {string|null} href * @param {DocumentFragment|undefined} fragment @@ -100,6 +108,8 @@ } else if (fragment) { output.appendChild(fragment); } - }, - }); + } + } + + customElements.define(GrLinkedText.is, GrLinkedText); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html index f01a75c..e9477bf 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html +++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-linked-text</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js index 61e28cc..1aa707f 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js +++ b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
@@ -19,6 +19,7 @@ /** * Pattern describing URLs with supported protocols. + * * @type {RegExp} */ const URL_PROTOCOL_PATTERN = /^(https?:\/\/|mailto:)/; @@ -27,6 +28,7 @@ * Construct a parser for linkifying text. Will linkify plain URLs that appear * in the text as well as custom links if any are specified in the linkConfig * parameter. + * * @param {Object|null|undefined} linkConfig Comment links as specified by the * commentlinks field on a project config. * @param {Function} callback The callback to be fired when an intermediate @@ -45,6 +47,7 @@ /** * Emit a callback to create a link element. + * * @param {string} text The text of the link. * @param {string} href The URL to use as the href of the link. */ @@ -56,6 +59,7 @@ /** * Given the source text and a list of CommentLinkItem objects that were * generated by the commentlinks config, emit parsing callbacks. + * * @param {string} text The chuml of source text over which the outputArray * items range. * @param {!Array<Gerrit.CommentLinkItem>} outputArray The list of items to add @@ -93,6 +97,7 @@ /** * Sort the given array of CommentLinkItems such that the positions are in * reverse order. + * * @param {!Array<Gerrit.CommentLinkItem>} outputArray */ GrLinkTextParser.prototype.sortArrayReverse = function(outputArray) { @@ -108,6 +113,7 @@ * - With the `html` paremeter provided, and the `text` and `href` parameters * passed as `null`. In this case, the string of HTML will be parsed and the * first resulting node will be used as the resulting content. + * * @param {string|null} text The text to use if creating a link. * @param {string|null} href The href to use as the URL if creating a link. * @param {string|null} html The html to parse and use as the result. @@ -150,6 +156,7 @@ /** * Create a CommentLinkItem for a link and append it to the given output * array. + * * @param {string|null} text The text for the link. * @param {string|null} href The href to use as the URL of the link. * @param {number} position The position inside the source text where the link @@ -172,6 +179,7 @@ /** * Create a CommentLinkItem specified by an HTMl string and append it to the * given output array. + * * @param {string|null} html The html to parse and use as the result. * @param {number} position The position inside the source text where the item * starts. @@ -192,6 +200,7 @@ /** * Does the given range overlap with anything already in the item list. + * * @param {number} position * @param {number} length * @param {!Array<Gerrit.CommentLinkItem>} outputArray @@ -214,6 +223,7 @@ /** * Parse the given source text and emit callbacks for the items that are * parsed. + * * @param {string} text */ GrLinkTextParser.prototype.parse = function(text) { @@ -231,6 +241,7 @@ * ba-linkify has found a plain URL and wants it linkified. * - With only a `text` parameter provided: this represents the non-link * content that lies between the links the library has found. + * * @param {string} text * @param {string|null|undefined} href */ @@ -257,6 +268,7 @@ /** * Walk over the given source text to find matches for comemntlink patterns * and emit parse result callbacks. + * * @param {string} text The raw source text. * @param {Object|null|undefined} patterns A comment links specification * object.
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js index 8d81030..c4e4ddb 100644 --- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js +++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
@@ -19,31 +19,39 @@ const REQUEST_DEBOUNCE_INTERVAL_MS = 200; - Polymer({ - is: 'gr-list-view', + /** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.URLEncodingMixin + */ + class GrListView extends Polymer.mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.FireBehavior, + Gerrit.URLEncodingBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-list-view'; } - properties: { - createNew: Boolean, - items: Array, - itemsPerPage: Number, - filter: { - type: String, - observer: '_filterChanged', - }, - offset: Number, - loading: Boolean, - path: String, - }, - - behaviors: [ - Gerrit.BaseUrlBehavior, - Gerrit.FireBehavior, - Gerrit.URLEncodingBehavior, - ], + static get properties() { + return { + createNew: Boolean, + items: Array, + itemsPerPage: Number, + filter: { + type: String, + observer: '_filterChanged', + }, + offset: Number, + loading: Boolean, + path: String, + }; + } detached() { + super.detached(); this.cancelDebouncer('reload'); - }, + } _filterChanged(newFilter, oldFilter) { if (!newFilter && !oldFilter) { @@ -51,7 +59,7 @@ } this._debounceReload(newFilter); - }, + } _debounceReload(filter) { this.debounce('reload', () => { @@ -61,11 +69,11 @@ } page.show(this.path); }, REQUEST_DEBOUNCE_INTERVAL_MS); - }, + } _createNewItem() { this.fire('create-clicked'); - }, + } _computeNavLink(offset, direction, itemsPerPage, filter, path) { // Offset could be a string when passed from the router. @@ -79,15 +87,15 @@ href += ',' + newOffset; } return href; - }, + } _computeCreateClass(createNew) { return createNew ? 'show' : ''; - }, + } _hidePrevArrow(loading, offset) { return loading || offset === 0; - }, + } _hideNextArrow(loading, items) { if (loading || !items || !items.length) { @@ -95,6 +103,8 @@ } const lastPage = items.length < this.itemsPerPage + 1; return lastPage; - }, - }); + } + } + + customElements.define(GrListView.is, GrListView); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html index c67d8b2..f99d5d9 100644 --- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html +++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-list-view</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/page/page.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -126,11 +126,13 @@ }); test('createNew link appears correctly', () => { - assert.isFalse(element.$$('#createNewContainer').classList + assert.isFalse(element.shadowRoot + .querySelector('#createNewContainer').classList .contains('show')); element.createNew = true; flushAsynchronousOperations(); - assert.isTrue(element.$$('#createNewContainer').classList + assert.isTrue(element.shadowRoot + .querySelector('#createNewContainer').classList .contains('show')); }); @@ -139,7 +141,7 @@ element.addEventListener('create-clicked', clickHandler); element.createNew = true; flushAsynchronousOperations(); - MockInteractions.tap(element.$$('#createNew')); + MockInteractions.tap(element.shadowRoot.querySelector('#createNew')); assert.isTrue(clickHandler.called); });
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html index 2b4b982..46b8fb6 100644 --- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html +++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
@@ -25,7 +25,10 @@ <style include="shared-styles"> :host { background: var(--dialog-background-color); - box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px; + border-radius: var(--border-radius); + box-shadow: 0 11px 15px -7px rgba(0,0,0,.2), + 0 24px 38px 3px rgba(0,0,0,.14), + 0 9px 46px 8px rgba(0,0,0,.12); } @media screen and (max-width: 50em) { @@ -35,6 +38,8 @@ position: fixed; right: 0; top: 0; + border-radius: 0; + box-shadow: none; } } </style>
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js index 8623458..fc9dc99 100644 --- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js +++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
@@ -21,9 +21,16 @@ const AWAIT_STEP = 5; const BREAKPOINT_FULLSCREEN_OVERLAY = '50em'; - Polymer({ - is: 'gr-overlay', - + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrOverlay extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Polymer.IronOverlayBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-overlay'; } /** * Fired when a fullscreen overlay is closed * @@ -36,22 +43,22 @@ * @event fullscreen-overlay-opened */ - properties: { - _fullScreenOpen: { - type: Boolean, - value: false, - }, - }, + static get properties() { + return { + _fullScreenOpen: { + type: Boolean, + value: false, + }, + }; + } - behaviors: [ - Gerrit.FireBehavior, - Polymer.IronOverlayBehavior, - ], - - listeners: { - 'iron-overlay-closed': '_close', - 'iron-overlay-cancelled': '_close', - }, + created() { + super.created(); + this.addEventListener('iron-overlay-closed', + () => this._close()); + this.addEventListener('iron-overlay-cancelled', + () => this._close()); + } open(...args) { return new Promise(resolve => { @@ -62,18 +69,18 @@ } this._awaitOpen(resolve); }); - }, + } _isMobile() { return window.matchMedia(`(max-width: ${BREAKPOINT_FULLSCREEN_OVERLAY})`); - }, + } _close() { if (this._fullScreenOpen) { this.fire('fullscreen-overlay-closed'); this._fullScreenOpen = false; } - }, + } /** * Override the focus stops that iron-overlay-behavior tries to find. @@ -81,7 +88,7 @@ setFocusStops(stops) { this.__firstFocusableNode = stops.start; this.__lastFocusableNode = stops.end; - }, + } /** * NOTE: (wyatta) Slightly hacky way to listen to the overlay actually @@ -99,10 +106,12 @@ }, AWAIT_STEP); }; step.call(this); - }, + } _id() { return this.getAttribute('id') || 'global'; - }, - }); + } + } + + customElements.define(GrOverlay.is, GrOverlay); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html index 08b7497..659823f 100644 --- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html +++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-overlay</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/page/page.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js index 9ccff600..23389ee 100644 --- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js +++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
@@ -17,27 +17,33 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-page-nav', + class GrPageNav extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-page-nav'; } - properties: { - _headerHeight: Number, - }, + static get properties() { + return { + _headerHeight: Number, + }; + } attached() { + super.attached(); this.listen(window, 'scroll', '_handleBodyScroll'); - }, + } detached() { + super.detached(); this.unlisten(window, 'scroll', '_handleBodyScroll'); - }, + } _handleBodyScroll() { if (this._headerHeight === undefined) { let top = this._getOffsetTop(this); for (let offsetParent = this.offsetParent; - offsetParent; - offsetParent = this._getOffsetParent(offsetParent)) { + offsetParent; + offsetParent = this._getOffsetParent(offsetParent)) { top += this._getOffsetTop(offsetParent); } this._headerHeight = top; @@ -45,20 +51,22 @@ this.$.nav.classList.toggle('pinned', this._getScrollY() >= this._headerHeight); - }, + } /* Functions used for test purposes */ _getOffsetParent(element) { if (!element || !element.offsetParent) { return ''; } return element.offsetParent; - }, + } _getOffsetTop(element) { return element.offsetTop; - }, + } _getScrollY() { return window.scrollY; - }, - }); + } + } + + customElements.define(GrPageNav.is, GrPageNav); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html index b384b47..663d3bd 100644 --- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html +++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-page-nav</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/page/page.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js index e2298c3..c7afd4d 100644 --- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js +++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
@@ -20,47 +20,54 @@ const SUGGESTIONS_LIMIT = 15; const REF_PREFIX = 'refs/heads/'; - Polymer({ - is: 'gr-repo-branch-picker', + /** + * @appliesMixin Gerrit.URLEncodingMixin + */ + class GrRepoBranchPicker extends Polymer.mixinBehaviors( [ + Gerrit.URLEncodingBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-repo-branch-picker'; } - properties: { - repo: { - type: String, - notify: true, - observer: '_repoChanged', - }, - branch: { - type: String, - notify: true, - }, - _branchDisabled: Boolean, - _query: { - type: Function, - value() { - return this._getRepoBranchesSuggestions.bind(this); + static get properties() { + return { + repo: { + type: String, + notify: true, + observer: '_repoChanged', }, - }, - _repoQuery: { - type: Function, - value() { - return this._getRepoSuggestions.bind(this); + branch: { + type: String, + notify: true, }, - }, - }, - - behaviors: [ - Gerrit.URLEncodingBehavior, - ], + _branchDisabled: Boolean, + _query: { + type: Function, + value() { + return this._getRepoBranchesSuggestions.bind(this); + }, + }, + _repoQuery: { + type: Function, + value() { + return this._getRepoSuggestions.bind(this); + }, + }, + }; + } attached() { + super.attached(); if (this.repo) { this.$.repoInput.setText(this.repo); } - }, + } ready() { + super.ready(); this._branchDisabled = !this.repo; - }, + } _getRepoBranchesSuggestions(input) { if (!this.repo) { return Promise.resolve([]); } @@ -69,19 +76,19 @@ } return this.$.restAPI.getRepoBranches(input, this.repo, SUGGESTIONS_LIMIT) .then(this._branchResponseToSuggestions.bind(this)); - }, + } _getRepoSuggestions(input) { return this.$.restAPI.getRepos(input, SUGGESTIONS_LIMIT) .then(this._repoResponseToSuggestions.bind(this)); - }, + } _repoResponseToSuggestions(res) { return res.map(repo => ({ name: repo.name, value: this.singleDecodeURL(repo.id), })); - }, + } _branchResponseToSuggestions(res) { return Object.keys(res).map(key => { @@ -91,19 +98,21 @@ } return {name: branch, value: branch}; }); - }, + } _repoCommitted(e) { this.repo = e.detail.value; - }, + } _branchCommitted(e) { this.branch = e.detail.value; - }, + } _repoChanged() { this.$.branchInput.clear(); this._branchDisabled = !this.repo; - }, - }); + } + } + + customElements.define(GrRepoBranchPicker.is, GrRepoBranchPicker); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html index 1ed9151..e7252d6 100644 --- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html +++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html
@@ -17,7 +17,7 @@ --> <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo-branch-picker</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js index 43e3922..0084932 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
@@ -76,7 +76,8 @@ }, this._defaultOptions, opt_options); if (this._type === Gerrit.Auth.TYPE.ACCESS_TOKEN) { return this._getAccessToken().then( - accessToken => this._fetchWithAccessToken(url, options, accessToken) + accessToken => + this._fetchWithAccessToken(url, options, accessToken) ); } else { return this._fetchWithXsrfToken(url, options); @@ -146,7 +147,7 @@ params.push(`access_token=${accessToken}`); const baseUrl = Gerrit.BaseUrlBehavior.getBaseUrl(); const pathname = baseUrl ? - url.substring(url.indexOf(baseUrl) + baseUrl.length) : url; + url.substring(url.indexOf(baseUrl) + baseUrl.length) : url; if (!pathname.startsWith('/a/')) { url = url.replace(pathname, '/a' + pathname); }
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html index cfdc6ee..6c5c150 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-auth</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -111,8 +111,8 @@ test('getToken calls are cached', () => { return Promise.all([ auth.fetch('/url-one'), auth.fetch('/url-two')]).then(() => { - assert.equal(getToken.callCount, 1); - }); + assert.equal(getToken.callCount, 1); + }); }); test('getToken refreshes token', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js index f72c7cc..c20cdd7 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
@@ -30,6 +30,7 @@ /** * Get or upgrade fetch options to include an ETag in a request. + * * @param {string} url The URL being fetched. * @param {!Object=} opt_options Optional options object in which to include * the ETag request header. If omitted, the result will be a fresh option @@ -77,6 +78,7 @@ /** * Get the cached payload for a given URL. + * * @param {string} url * @return {string|undefined} Returns the unparsed JSON payload from the * cache.
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html index 76c8c2c..623ea18 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-etag-decorator</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js index 078c332..fdb108f8 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -39,16 +39,21 @@ const ANONYMIZED_REVISION_BASE_URL = ANONYMIZED_CHANGE_BASE_URL + '/revisions/*'; - Polymer({ - is: 'gr-rest-api-interface', - - behaviors: [ - Gerrit.FireBehavior, - Gerrit.PathListBehavior, - Gerrit.PatchSetBehavior, - Gerrit.RESTClientBehavior, - ], - + /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.PathListMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.RESTClientMixin + */ + class GrRestApiInterface extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.PathListBehavior, + Gerrit.PatchSetBehavior, + Gerrit.RESTClientBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-rest-api-interface'; } /** * Fired when an server error occurs. * @@ -73,43 +78,49 @@ * @event rpc-log */ - properties: { - _cache: { - type: Object, - value: new SiteBasedCache(), // Shared across instances. - }, - _credentialCheck: { - type: Object, - value: {checking: false}, // Shared across instances. - }, - _sharedFetchPromises: { - type: Object, - value: new FetchPromisesCache(), // Shared across instances. - }, - _pendingRequests: { - type: Object, - value: {}, // Intentional to share the object across instances. - }, - _etags: { - type: Object, - value: new GrEtagDecorator(), // Share across instances. - }, - /** - * Used to maintain a mapping of changeNums to project names. - */ - _projectLookup: { - type: Object, - value: {}, // Intentional to share the object across instances. - }, - _auth: { - type: Object, - value: Gerrit.Auth, // Share across instances. - }, - }, + constructor() { + super(); + this.JSON_PREFIX = JSON_PREFIX; + } - JSON_PREFIX, + static get properties() { + return { + _cache: { + type: Object, + value: new SiteBasedCache(), // Shared across instances. + }, + _credentialCheck: { + type: Object, + value: {checking: false}, // Shared across instances. + }, + _sharedFetchPromises: { + type: Object, + value: new FetchPromisesCache(), // Shared across instances. + }, + _pendingRequests: { + type: Object, + value: {}, // Intentional to share the object across instances. + }, + _etags: { + type: Object, + value: new GrEtagDecorator(), // Share across instances. + }, + /** + * Used to maintain a mapping of changeNums to project names. + */ + _projectLookup: { + type: Object, + value: {}, // Intentional to share the object across instances. + }, + _auth: { + type: Object, + value: Gerrit.Auth, // Share across instances. + }, + }; + } created() { + super.created(); /* Polymer 1 and Polymer 2 have slightly different lifecycle. * Differences are not very well documented (see * https://github.com/Polymer/old-docs-site/issues/2322). @@ -130,28 +141,29 @@ // this._initRestApiHelper(); - }, + } ready() { + super.ready(); // See comments in created() this._initRestApiHelper(); - }, + } _initRestApiHelper() { if (this._restApiHelper) { return; } - if (this._cache && this._auth && this._sharedFetchPromises - && this._credentialCheck) { + if (this._cache && this._auth && this._sharedFetchPromises && + this._credentialCheck) { this._restApiHelper = new GrRestApiHelper(this._cache, this._auth, this._sharedFetchPromises, this._credentialCheck, this); } - }, + } _fetchSharedCacheURL(req) { // Cache is shared across instances return this._restApiHelper.fetchCacheURL(req); - }, + } /** * @param {!Object} response @@ -159,7 +171,7 @@ */ getResponseObject(response) { return this._restApiHelper.getResponseObject(response); - }, + } getConfig(noCache) { if (!noCache) { @@ -173,7 +185,7 @@ url: '/config/server/info', reportUrlAsIs: true, }); - }, + } getRepo(repo, opt_errFn) { // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend @@ -183,7 +195,7 @@ errFn: opt_errFn, anonymizedUrl: '/projects/*', }); - }, + } getProjectConfig(repo, opt_errFn) { // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend @@ -193,7 +205,7 @@ errFn: opt_errFn, anonymizedUrl: '/projects/*/config', }); - }, + } getRepoAccess(repo) { // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend @@ -202,7 +214,7 @@ url: '/access/?project=' + encodeURIComponent(repo), anonymizedUrl: '/access/?project=*', }); - }, + } getRepoDashboards(repo, opt_errFn) { // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend @@ -212,7 +224,7 @@ errFn: opt_errFn, anonymizedUrl: '/projects/*/dashboards?inherited', }); - }, + } saveRepoConfig(repo, config, opt_errFn) { // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend @@ -226,7 +238,7 @@ errFn: opt_errFn, anonymizedUrl: '/projects/*/config', }); - }, + } runRepoGC(repo, opt_errFn) { if (!repo) { return ''; } @@ -240,7 +252,7 @@ errFn: opt_errFn, anonymizedUrl: '/projects/*/gc', }); - }, + } /** * @param {?Object} config @@ -258,7 +270,7 @@ errFn: opt_errFn, anonymizedUrl: '/projects/*', }); - }, + } /** * @param {?Object} config @@ -274,7 +286,7 @@ errFn: opt_errFn, anonymizedUrl: '/groups/*', }); - }, + } getGroupConfig(group, opt_errFn) { return this._restApiHelper.fetchJSON({ @@ -282,7 +294,7 @@ errFn: opt_errFn, anonymizedUrl: '/groups/*/detail', }); - }, + } /** * @param {string} repo @@ -302,7 +314,7 @@ errFn: opt_errFn, anonymizedUrl: '/projects/*/branches/*', }); - }, + } /** * @param {string} repo @@ -322,7 +334,7 @@ errFn: opt_errFn, anonymizedUrl: '/projects/*/tags/*', }); - }, + } /** * @param {string} name @@ -343,7 +355,7 @@ errFn: opt_errFn, anonymizedUrl: '/projects/*/branches/*', }); - }, + } /** * @param {string} name @@ -364,7 +376,7 @@ errFn: opt_errFn, anonymizedUrl: '/projects/*/tags/*', }); - }, + } /** * @param {!string} groupName @@ -378,7 +390,7 @@ }; return this._fetchSharedCacheURL(req) .then(configs => configs.hasOwnProperty(groupName)); - }, + } getGroupMembers(groupName, opt_errFn) { const encodeName = encodeURIComponent(groupName); @@ -387,14 +399,14 @@ errFn: opt_errFn, anonymizedUrl: '/groups/*/members', }); - }, + } getIncludedGroup(groupName) { return this._restApiHelper.fetchJSON({ url: `/groups/${encodeURIComponent(groupName)}/groups/`, anonymizedUrl: '/groups/*/groups', }); - }, + } saveGroupName(groupId, name) { const encodeId = encodeURIComponent(groupId); @@ -404,7 +416,7 @@ body: {name}, anonymizedUrl: '/groups/*/name', }); - }, + } saveGroupOwner(groupId, ownerId) { const encodeId = encodeURIComponent(groupId); @@ -414,7 +426,7 @@ body: {owner: ownerId}, anonymizedUrl: '/groups/*/owner', }); - }, + } saveGroupDescription(groupId, description) { const encodeId = encodeURIComponent(groupId); @@ -424,7 +436,7 @@ body: {description}, anonymizedUrl: '/groups/*/description', }); - }, + } saveGroupOptions(groupId, options) { const encodeId = encodeURIComponent(groupId); @@ -434,7 +446,7 @@ body: options, anonymizedUrl: '/groups/*/options', }); - }, + } getGroupAuditLog(group, opt_errFn) { return this._fetchSharedCacheURL({ @@ -442,7 +454,7 @@ errFn: opt_errFn, anonymizedUrl: '/groups/*/log.audit', }); - }, + } saveGroupMembers(groupName, groupMembers) { const encodeName = encodeURIComponent(groupName); @@ -453,7 +465,7 @@ parseResponse: true, anonymizedUrl: '/groups/*/members/*', }); - }, + } saveIncludedGroup(groupName, includedGroup, opt_errFn) { const encodeName = encodeURIComponent(groupName); @@ -469,7 +481,7 @@ return this.getResponseObject(response); } }); - }, + } deleteGroupMembers(groupName, groupMembers) { const encodeName = encodeURIComponent(groupName); @@ -479,7 +491,7 @@ url: `/groups/${encodeName}/members/${encodeMember}`, anonymizedUrl: '/groups/*/members/*', }); - }, + } deleteIncludedGroup(groupName, includedGroup) { const encodeName = encodeURIComponent(groupName); @@ -489,14 +501,14 @@ url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`, anonymizedUrl: '/groups/*/groups/*', }); - }, + } getVersion() { return this._fetchSharedCacheURL({ url: '/config/server/version', reportUrlAsIs: true, }); - }, + } getDiffPreferences() { return this.getLoggedIn().then(loggedIn => { @@ -527,7 +539,7 @@ theme: 'DEFAULT', }); }); - }, + } getEditPreferences() { return this.getLoggedIn().then(loggedIn => { @@ -558,7 +570,7 @@ theme: 'DEFAULT', }); }); - }, + } /** * @param {?Object} prefs @@ -578,7 +590,7 @@ errFn: opt_errFn, reportUrlAsIs: true, }); - }, + } /** * @param {?Object} prefs @@ -594,7 +606,7 @@ errFn: opt_errFn, reportUrlAsIs: true, }); - }, + } /** * @param {?Object} prefs @@ -610,7 +622,7 @@ errFn: opt_errFn, reportUrlAsIs: true, }); - }, + } getAccount() { return this._fetchSharedCacheURL({ @@ -622,7 +634,7 @@ } }, }); - }, + } getAvatarChangeUrl() { return this._fetchSharedCacheURL({ @@ -634,14 +646,14 @@ } }, }); - }, + } getExternalIds() { return this._restApiHelper.fetchJSON({ url: '/accounts/self/external.ids', reportUrlAsIs: true, }); - }, + } deleteAccountIdentity(id) { return this._restApiHelper.send({ @@ -651,7 +663,7 @@ parseResponse: true, reportUrlAsIs: true, }); - }, + } /** * @param {string} userId the ID of the user usch as an email address. @@ -662,14 +674,14 @@ url: `/accounts/${encodeURIComponent(userId)}/detail`, anonymizedUrl: '/accounts/*/detail', }); - }, + } getAccountEmails() { return this._fetchSharedCacheURL({ url: '/accounts/self/emails', reportUrlAsIs: true, }); - }, + } /** * @param {string} email @@ -682,7 +694,7 @@ errFn: opt_errFn, anonymizedUrl: '/account/self/emails/*', }); - }, + } /** * @param {string} email @@ -695,7 +707,7 @@ errFn: opt_errFn, anonymizedUrl: '/accounts/self/email/*', }); - }, + } /** * @param {string} email @@ -724,7 +736,7 @@ this._cache.set('/accounts/self/emails', emails); } }); - }, + } /** * @param {?Object} obj @@ -738,7 +750,7 @@ this._cache.set('/accounts/self/detail', Object.assign({}, cachedAccount, obj)); } - }, + } /** * @param {string} name @@ -755,7 +767,7 @@ }; return this._restApiHelper.send(req) .then(newName => this._updateCachedAccount({name: newName})); - }, + } /** * @param {string} username @@ -772,7 +784,7 @@ }; return this._restApiHelper.send(req) .then(newName => this._updateCachedAccount({username: newName})); - }, + } /** * @param {string} status @@ -789,28 +801,28 @@ }; return this._restApiHelper.send(req) .then(newStatus => this._updateCachedAccount({status: newStatus})); - }, + } getAccountStatus(userId) { return this._restApiHelper.fetchJSON({ url: `/accounts/${encodeURIComponent(userId)}/status`, anonymizedUrl: '/accounts/*/status', }); - }, + } getAccountGroups() { return this._restApiHelper.fetchJSON({ url: '/accounts/self/groups', reportUrlAsIs: true, }); - }, + } getAccountAgreements() { return this._restApiHelper.fetchJSON({ url: '/accounts/self/agreements', reportUrlAsIs: true, }); - }, + } saveAccountAgreement(name) { return this._restApiHelper.send({ @@ -819,7 +831,7 @@ body: name, reportUrlAsIs: true, }); - }, + } /** * @param {string=} opt_params @@ -835,7 +847,7 @@ url: '/accounts/self/capabilities' + queryString, anonymizedUrl: '/accounts/self/capabilities?q=*', }); - }, + } getLoggedIn() { return this.getAccount().then(account => { @@ -843,7 +855,7 @@ }).catch(() => { return false; }); - }, + } getIsAdmin() { return this.getLoggedIn().then(isLoggedIn => { @@ -855,18 +867,18 @@ }).then(capabilities => { return capabilities && capabilities.administrateServer; }); - }, + } checkCredentials() { return this._restApiHelper.checkCredentials(); - }, + } getDefaultPreferences() { return this._fetchSharedCacheURL({ url: '/config/server/preferences', reportUrlAsIs: true, }); - }, + } getPreferences() { return this.getLoggedIn().then(loggedIn => { @@ -887,19 +899,19 @@ return Promise.resolve({ changes_per_page: 25, default_diff_view: this._isNarrowScreen() ? - DiffViewMode.UNIFIED : DiffViewMode.SIDE_BY_SIDE, + DiffViewMode.UNIFIED : DiffViewMode.SIDE_BY_SIDE, diff_view: 'SIDE_BY_SIDE', size_bar_in_change_table: true, }); }); - }, + } getWatchedProjects() { return this._fetchSharedCacheURL({ url: '/accounts/self/watched.projects', reportUrlAsIs: true, }); - }, + } /** * @param {string} projects @@ -914,7 +926,7 @@ parseResponse: true, reportUrlAsIs: true, }); - }, + } /** * @param {string} projects @@ -928,11 +940,11 @@ errFn: opt_errFn, reportUrlAsIs: true, }); - }, + } _isNarrowScreen() { return window.innerWidth < MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX; - }, + } /** * @param {number=} opt_changesPerPage @@ -988,17 +1000,18 @@ } return response; }); - }, + } /** * Inserts a change into _projectLookup iff it has a valid structure. + * * @param {?{ _number: (number|string) }} change */ _maybeInsertInLookup(change) { if (change && change.project && change._number) { this.setInProjectLookup(change._number, change.project); } - }, + } /** * TODO (beckysiegel) this needs to be rewritten with the optional param @@ -1012,7 +1025,7 @@ getChangeActionURL(changeNum, opt_patchNum, endpoint) { return this._changeBaseURL(changeNum, opt_patchNum) .then(url => url + endpoint); - }, + } /** * @param {number|string} changeNum @@ -1043,7 +1056,7 @@ changeNum, optionsHex, opt_errFn, opt_cancelCondition) .then(GrReviewerUpdatesParser.parse); }); - }, + } /** * @param {number|string} changeNum @@ -1059,7 +1072,7 @@ ); return this._getChangeDetail(changeNum, optionsHex, opt_errFn, opt_cancelCondition); - }, + } /** * @param {number|string} changeNum @@ -1097,8 +1110,8 @@ } const payloadPromise = response ? - this._restApiHelper.readResponsePayload(response) : - Promise.resolve(null); + this._restApiHelper.readResponsePayload(response) : + Promise.resolve(null); return payloadPromise.then(payload => { if (!payload) { return null; } @@ -1109,7 +1122,7 @@ }); }); }); - }, + } /** * @param {number|string} changeNum @@ -1122,7 +1135,7 @@ patchNum, reportEndpointAsIs: true, }); - }, + } /** * @param {number|string} changeNum @@ -1143,7 +1156,7 @@ params, reportEndpointAsIs: true, }); - }, + } /** * @param {number|string} changeNum @@ -1161,7 +1174,7 @@ endpoint, anonymizedEndpoint, }); - }, + } /** * @param {number|string} changeNum @@ -1176,7 +1189,7 @@ patchNum, anonymizedEndpoint: '/files?q=*', }); - }, + } /** * @param {number|string} changeNum @@ -1186,14 +1199,15 @@ getChangeOrEditFiles(changeNum, patchRange) { if (this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME)) { return this.getChangeEditFiles(changeNum, patchRange).then(res => - res.files); + res.files); } return this.getChangeFiles(changeNum, patchRange); - }, + } /** * The closure compiler doesn't realize this.specialFilePathCompare is * valid. + * * @suppress {checkTypes} */ getChangeFilePathsAsSpeciallySortedArray(changeNum, patchRange) { @@ -1201,7 +1215,7 @@ if (!files) return; return Object.keys(files).sort(this.specialFilePathCompare); }); - }, + } getChangeRevisionActions(changeNum, patchNum) { const req = { @@ -1219,7 +1233,7 @@ } return revisionActions; }); - }, + } /** * @param {number|string} changeNum @@ -1229,7 +1243,7 @@ getChangeSuggestedReviewers(changeNum, inputVal, opt_errFn) { return this._getChangeSuggestedGroup('REVIEWER', changeNum, inputVal, opt_errFn); - }, + } /** * @param {number|string} changeNum @@ -1239,7 +1253,7 @@ getChangeSuggestedCCs(changeNum, inputVal, opt_errFn) { return this._getChangeSuggestedGroup('CC', changeNum, inputVal, opt_errFn); - }, + } _getChangeSuggestedGroup(reviewerState, changeNum, inputVal, opt_errFn) { // More suggestions may obscure content underneath in the reply dialog, @@ -1253,7 +1267,7 @@ params, reportEndpointAsIs: true, }); - }, + } /** * @param {number|string} changeNum @@ -1264,7 +1278,7 @@ endpoint: '/in', reportEndpointAsIs: true, }); - }, + } _computeFilter(filter) { if (filter && filter.startsWith('^')) { @@ -1275,7 +1289,7 @@ filter = ''; } return filter; - }, + } /** * @param {string} filter @@ -1287,7 +1301,7 @@ return `/groups/?n=${groupsPerPage + 1}&S=${offset}` + this._computeFilter(filter); - }, + } /** * @param {string} filter @@ -1323,15 +1337,15 @@ return `/projects/?n=${reposPerPage + 1}&S=${offset}` + `&query=${encodedFilter}`; - }, + } invalidateGroupsCache() { this._restApiHelper.invalidateFetchPromisesPrefix('/groups/?'); - }, + } invalidateReposCache() { this._restApiHelper.invalidateFetchPromisesPrefix('/projects/?'); - }, + } /** * @param {string} filter @@ -1346,7 +1360,7 @@ url, anonymizedUrl: '/groups/?*', }); - }, + } /** * @param {string} filter @@ -1363,7 +1377,7 @@ url, anonymizedUrl: '/projects/?*', }); - }, + } setRepoHead(repo, ref) { // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend @@ -1374,7 +1388,7 @@ body: {ref}, anonymizedUrl: '/projects/*/HEAD', }); - }, + } /** * @param {string} filter @@ -1397,7 +1411,7 @@ errFn: opt_errFn, anonymizedUrl: '/projects/*/branches?*', }); - }, + } /** * @param {string} filter @@ -1421,7 +1435,7 @@ errFn: opt_errFn, anonymizedUrl: '/projects/*/tags', }); - }, + } /** * @param {string} filter @@ -1440,7 +1454,7 @@ errFn: opt_errFn, anonymizedUrl: '/plugins/?all', }); - }, + } getRepoAccessRights(repoName, opt_errFn) { // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend @@ -1450,7 +1464,7 @@ errFn: opt_errFn, anonymizedUrl: '/projects/*/access', }); - }, + } setRepoAccessRights(repoName, repoInfo) { // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend @@ -1461,7 +1475,7 @@ body: repoInfo, anonymizedUrl: '/projects/*/access', }); - }, + } setRepoAccessRightsForReview(projectName, projectInfo) { return this._restApiHelper.send({ @@ -1471,7 +1485,7 @@ parseResponse: true, anonymizedUrl: '/projects/*/access:review', }); - }, + } /** * @param {string} inputVal @@ -1487,7 +1501,7 @@ params, reportUrlAsIs: true, }); - }, + } /** * @param {string} inputVal @@ -1507,7 +1521,7 @@ params, reportUrlAsIs: true, }); - }, + } /** * @param {string} inputVal @@ -1526,15 +1540,15 @@ params, anonymizedUrl: '/accounts/?n=*', }); - }, + } addChangeReviewer(changeNum, reviewerID) { return this._sendChangeReviewerRequest('POST', changeNum, reviewerID); - }, + } removeChangeReviewer(changeNum, reviewerID) { return this._sendChangeReviewerRequest('DELETE', changeNum, reviewerID); - }, + } _sendChangeReviewerRequest(method, changeNum, reviewerID) { return this.getChangeActionURL(changeNum, null, '/reviewers') @@ -1553,7 +1567,7 @@ return this._restApiHelper.send({method, url, body}); }); - }, + } getRelatedChanges(changeNum, patchNum) { return this._getChangeURLAndFetch({ @@ -1562,7 +1576,7 @@ patchNum, reportEndpointAsIs: true, }); - }, + } getChangesSubmittedTogether(changeNum) { return this._getChangeURLAndFetch({ @@ -1570,7 +1584,7 @@ endpoint: '/submitted_together?o=NON_VISIBLE_CHANGES', reportEndpointAsIs: true, }); - }, + } getChangeConflicts(changeNum) { const options = this.listChangesOptionsToHex( @@ -1579,14 +1593,14 @@ ); const params = { O: options, - q: 'status:open is:mergeable conflicts:' + changeNum, + q: 'status:open conflicts:' + changeNum, }; return this._restApiHelper.fetchJSON({ url: '/changes/', params, anonymizedUrl: '/changes/conflicts:*', }); - }, + } getChangeCherryPicks(project, changeID, changeNum) { const options = this.listChangesOptionsToHex( @@ -1608,7 +1622,7 @@ params, anonymizedUrl: '/changes/change:*', }); - }, + } getChangesWithSameTopic(topic, changeNum) { const options = this.listChangesOptionsToHex( @@ -1631,7 +1645,7 @@ params, anonymizedUrl: '/changes/topic:*', }); - }, + } getReviewedFiles(changeNum, patchNum) { return this._getChangeURLAndFetch({ @@ -1640,7 +1654,7 @@ patchNum, reportEndpointAsIs: true, }); - }, + } /** * @param {number|string} changeNum @@ -1658,7 +1672,7 @@ errFn: opt_errFn, anonymizedEndpoint: '/files/*/reviewed', }); - }, + } /** * @param {number|string} changeNum @@ -1679,7 +1693,7 @@ errFn: opt_errFn, }); }); - }, + } getChangeEdit(changeNum, opt_download_commands) { const params = opt_download_commands ? {'download-commands': true} : null; @@ -1692,7 +1706,7 @@ reportEndpointAsIs: true, }); }); - }, + } /** * @param {string} project @@ -1722,7 +1736,7 @@ parseResponse: true, reportUrlAsIs: true, }); - }, + } /** * @param {number|string} changeNum @@ -1737,8 +1751,8 @@ return res; }; const promise = this.patchNumEquals(patchNum, this.EDIT_NAME) ? - this._getFileInChangeEdit(changeNum, path) : - this._getFileInRevision(changeNum, path, patchNum, suppress404s); + this._getFileInChangeEdit(changeNum, path) : + this._getFileInRevision(changeNum, path, patchNum, suppress404s); return promise.then(res => { if (!res.ok) { return res; } @@ -1750,10 +1764,11 @@ return {content, type, ok: true}; }); }); - }, + } /** * Gets a file in a specific change and revision. + * * @param {number|string} changeNum * @param {string} path * @param {number|string} patchNum @@ -1769,10 +1784,11 @@ headers: {Accept: 'application/json'}, anonymizedEndpoint: '/files/*/content', }); - }, + } /** * Gets a file in a change edit. + * * @param {number|string} changeNum * @param {string} path */ @@ -1784,7 +1800,7 @@ headers: {Accept: 'application/json'}, anonymizedEndpoint: '/edit/*', }); - }, + } rebaseChangeEdit(changeNum) { return this._getChangeURLAndSend({ @@ -1793,7 +1809,7 @@ endpoint: '/edit:rebase', reportEndpointAsIs: true, }); - }, + } deleteChangeEdit(changeNum) { return this._getChangeURLAndSend({ @@ -1802,7 +1818,7 @@ endpoint: '/edit', reportEndpointAsIs: true, }); - }, + } restoreFileInChangeEdit(changeNum, restore_path) { return this._getChangeURLAndSend({ @@ -1812,7 +1828,7 @@ body: {restore_path}, reportEndpointAsIs: true, }); - }, + } renameFileInChangeEdit(changeNum, old_path, new_path) { return this._getChangeURLAndSend({ @@ -1822,7 +1838,7 @@ body: {old_path, new_path}, reportEndpointAsIs: true, }); - }, + } deleteFileInChangeEdit(changeNum, path) { return this._getChangeURLAndSend({ @@ -1831,7 +1847,7 @@ endpoint: '/edit/' + encodeURIComponent(path), anonymizedEndpoint: '/edit/*', }); - }, + } saveChangeEdit(changeNum, path, contents) { return this._getChangeURLAndSend({ @@ -1842,7 +1858,26 @@ contentType: 'text/plain', anonymizedEndpoint: '/edit/*', }); - }, + } + + getRobotCommentFixPreview(changeNum, patchNum, fixId) { + return this._getChangeURLAndFetch({ + changeNum, + patchNum, + endpoint: `/fixes/${encodeURIComponent(fixId)}/preview`, + reportEndpointAsId: true, + }); + } + + applyFixSuggestion(changeNum, patchNum, fixId) { + return this._getChangeURLAndSend({ + method: 'POST', + changeNum, + patchNum, + endpoint: `/fixes/${encodeURIComponent(fixId)}/apply`, + reportEndpointAsId: true, + }); + } // Deprecated, prefer to use putChangeCommitMessage instead. saveChangeCommitMessageEdit(changeNum, message) { @@ -1853,7 +1888,7 @@ body: {message}, reportEndpointAsIs: true, }); - }, + } publishChangeEdit(changeNum) { return this._getChangeURLAndSend({ @@ -1862,7 +1897,7 @@ endpoint: '/edit:publish', reportEndpointAsIs: true, }); - }, + } putChangeCommitMessage(changeNum, message) { return this._getChangeURLAndSend({ @@ -1872,7 +1907,7 @@ body: {message}, reportEndpointAsIs: true, }); - }, + } saveChangeStarred(changeNum, starred) { // Some servers may require the project name to be provided @@ -1887,7 +1922,7 @@ anonymizedUrl: '/accounts/self/starred.changes/*', }); }); - }, + } saveChangeReviewed(changeNum, reviewed) { return this._getChangeURLAndSend({ @@ -1895,10 +1930,11 @@ method: 'PUT', endpoint: reviewed ? '/reviewed' : '/unreviewed', }); - }, + } /** * Public version of the _restApiHelper.send method preserved for plugins. + * * @param {string} method * @param {string} url * @param {?string|number|Object=} opt_body passed as null sometimes @@ -1919,7 +1955,7 @@ contentType: opt_contentType, headers: opt_headers, }); - }, + } /** * @param {number|string} changeNum @@ -1961,7 +1997,7 @@ } return this._getChangeURLAndFetch(req); - }, + } /** * @param {number|string} changeNum @@ -1973,7 +2009,7 @@ getDiffComments(changeNum, opt_basePatchNum, opt_patchNum, opt_path) { return this._getDiffComments(changeNum, '/comments', opt_basePatchNum, opt_patchNum, opt_path); - }, + } /** * @param {number|string} changeNum @@ -1985,7 +2021,7 @@ getDiffRobotComments(changeNum, opt_basePatchNum, opt_patchNum, opt_path) { return this._getDiffComments(changeNum, '/robotcomments', opt_basePatchNum, opt_patchNum, opt_path); - }, + } /** * If the user is logged in, fetch the user's draft diff comments. If there @@ -2004,7 +2040,7 @@ return this._getDiffComments(changeNum, '/drafts', opt_basePatchNum, opt_patchNum, opt_path); }); - }, + } _setRange(comments, comment) { if (comment.in_reply_to && !comment.range) { @@ -2016,7 +2052,7 @@ } } return comment; - }, + } _setRanges(comments) { comments = comments || []; @@ -2027,7 +2063,7 @@ this._setRange(comments, comment); } return comments; - }, + } /** * @param {number|string} changeNum @@ -2100,7 +2136,7 @@ comments, }); }); - }, + } /** * @param {number|string} changeNum @@ -2110,15 +2146,15 @@ _getDiffCommentsFetchURL(changeNum, endpoint, opt_patchNum) { return this._changeBaseURL(changeNum, opt_patchNum) .then(url => url + endpoint); - }, + } saveDiffDraft(changeNum, patchNum, draft) { return this._sendDiffDraftRequest('PUT', changeNum, patchNum, draft); - }, + } deleteDiffDraft(changeNum, patchNum, draft) { return this._sendDiffDraftRequest('DELETE', changeNum, patchNum, draft); - }, + } /** * @returns {boolean} Whether there are pending diff draft sends. @@ -2126,7 +2162,7 @@ hasPendingDiffDrafts() { const promises = this._pendingRequests[Requests.SEND_DIFF_DRAFT]; return promises && promises.length; - }, + } /** * @returns {!Promise<undefined>} A promise that resolves when all pending @@ -2137,7 +2173,7 @@ .then(() => { this._pendingRequests[Requests.SEND_DIFF_DRAFT] = []; }); - }, + } _sendDiffDraftRequest(method, changeNum, patchNum, draft) { const isCreate = !draft.id && method === 'PUT'; @@ -2173,7 +2209,7 @@ } return promise; - }, + } getCommitInfo(project, commit) { return this._restApiHelper.fetchJSON({ @@ -2181,7 +2217,7 @@ '/commits/' + encodeURIComponent(commit), anonymizedUrl: '/projects/*/comments/*', }); - }, + } _fetchB64File(url) { return this._restApiHelper.fetch({url: this.getBaseUrl() + url}) @@ -2195,7 +2231,7 @@ return {body: text, type}; }); }); - }, + } /** * @param {string} changeId @@ -2205,12 +2241,12 @@ */ getB64FileContents(changeId, patchNum, path, opt_parentIndex) { const parent = typeof opt_parentIndex === 'number' ? - '?parent=' + opt_parentIndex : ''; + '?parent=' + opt_parentIndex : ''; return this._changeBaseURL(changeId, patchNum).then(url => { url = `${url}/files/${encodeURIComponent(path)}/content${parent}`; return this._fetchB64File(url); }); - }, + } getImagesForDiff(changeNum, diff, patchRange) { let promiseA; @@ -2252,7 +2288,7 @@ return {baseImage, revisionImage}; }); - }, + } /** * @param {number|string} changeNum @@ -2264,8 +2300,8 @@ // TODO(kaspern): For full slicer migration, app should warn with a call // stack every time _changeBaseURL is called without a project. const projectPromise = opt_project ? - Promise.resolve(opt_project) : - this.getFromProjectLookup(changeNum); + Promise.resolve(opt_project) : + this.getFromProjectLookup(changeNum); return projectPromise.then(project => { let url = `/changes/${encodeURIComponent(project)}~${changeNum}`; if (opt_patchNum) { @@ -2273,7 +2309,7 @@ } return url; }); - }, + } /** * @suppress {checkTypes} @@ -2289,7 +2325,7 @@ parseResponse: true, reportUrlAsIs: true, }); - }, + } /** * @suppress {checkTypes} @@ -2305,7 +2341,7 @@ parseResponse: true, reportUrlAsIs: true, }); - }, + } deleteAccountHttpPassword() { return this._restApiHelper.send({ @@ -2313,7 +2349,7 @@ url: '/accounts/self/password.http', reportUrlAsIs: true, }); - }, + } /** * @suppress {checkTypes} @@ -2328,14 +2364,14 @@ parseResponse: true, reportUrlAsIs: true, }); - }, + } getAccountSSHKeys() { return this._fetchSharedCacheURL({ url: '/accounts/self/sshkeys', reportUrlAsIs: true, }); - }, + } addAccountSSHKey(key) { const req = { @@ -2356,7 +2392,7 @@ if (!obj.valid) { return Promise.reject(new Error('error')); } return obj; }); - }, + } deleteAccountSSHKey(id) { return this._restApiHelper.send({ @@ -2364,14 +2400,14 @@ url: '/accounts/self/sshkeys/' + id, anonymizedUrl: '/accounts/self/sshkeys/*', }); - }, + } getAccountGPGKeys() { return this._restApiHelper.fetchJSON({ url: '/accounts/self/gpgkeys', reportUrlAsIs: true, }); - }, + } addAccountGPGKey(key) { const req = { @@ -2391,7 +2427,7 @@ if (!obj) { return Promise.reject(new Error('error')); } return obj; }); - }, + } deleteAccountGPGKey(id) { return this._restApiHelper.send({ @@ -2399,7 +2435,7 @@ url: '/accounts/self/gpgkeys/' + id, anonymizedUrl: '/accounts/self/gpgkeys/*', }); - }, + } deleteVote(changeNum, account, label) { return this._getChangeURLAndSend({ @@ -2408,7 +2444,7 @@ endpoint: `/reviewers/${account}/votes/${encodeURIComponent(label)}`, anonymizedEndpoint: '/reviewers/*/votes/*', }); - }, + } setDescription(changeNum, patchNum, desc) { return this._getChangeURLAndSend({ @@ -2418,7 +2454,7 @@ body: {description: desc}, reportUrlAsIs: true, }); - }, + } confirmEmail(token) { const req = { @@ -2433,7 +2469,7 @@ } return null; }); - }, + } getCapabilities(opt_errFn) { return this._restApiHelper.fetchJSON({ @@ -2441,7 +2477,7 @@ errFn: opt_errFn, reportUrlAsIs: true, }); - }, + } getTopMenus(opt_errFn) { return this._fetchSharedCacheURL({ @@ -2449,7 +2485,7 @@ errFn: opt_errFn, reportUrlAsIs: true, }); - }, + } setAssignee(changeNum, assignee) { return this._getChangeURLAndSend({ @@ -2459,7 +2495,7 @@ body: {assignee}, reportUrlAsIs: true, }); - }, + } deleteAssignee(changeNum) { return this._getChangeURLAndSend({ @@ -2468,14 +2504,14 @@ endpoint: '/assignee', reportUrlAsIs: true, }); - }, + } probePath(path) { return fetch(new Request(path, {method: 'HEAD'})) .then(response => { return response.ok; }); - }, + } /** * @param {number|string} changeNum @@ -2498,7 +2534,7 @@ return 'Change marked as Work In Progress.'; } }); - }, + } /** * @param {number|string} changeNum @@ -2514,7 +2550,7 @@ errFn: opt_errFn, reportUrlAsIs: true, }); - }, + } /** * @suppress {checkTypes} @@ -2531,7 +2567,7 @@ parseResponse: true, anonymizedEndpoint: '/comments/*/delete', }); - }, + } /** * Given a changeNum, gets the change. @@ -2550,7 +2586,7 @@ if (!res || !res.length) { return null; } return res[0]; }); - }, + } /** * @param {string|number} changeNum @@ -2563,7 +2599,7 @@ 'One of them must be invalid.'); } this._projectLookup[changeNum] = project; - }, + } /** * Checks in _projectLookup for the changeNum. If it exists, returns the @@ -2587,19 +2623,20 @@ this.setInProjectLookup(changeNum, change.project); return change.project; }); - }, + } /** * Alias for _changeBaseURL.then(send). + * * @todo(beckysiegel) clean up comments * @param {Gerrit.ChangeSendRequest} req * @return {!Promise<!Object>} */ _getChangeURLAndSend(req) { const anonymizedBaseUrl = req.patchNum ? - ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL; + ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL; const anonymizedEndpoint = req.reportEndpointAsIs ? - req.endpoint : req.anonymizedEndpoint; + req.endpoint : req.anonymizedEndpoint; return this._changeBaseURL(req.changeNum, req.patchNum).then(url => { return this._restApiHelper.send({ @@ -2611,21 +2648,22 @@ headers: req.headers, parseResponse: req.parseResponse, anonymizedUrl: anonymizedEndpoint ? - (anonymizedBaseUrl + anonymizedEndpoint) : undefined, + (anonymizedBaseUrl + anonymizedEndpoint) : undefined, }); }); - }, + } /** * Alias for _changeBaseURL.then(_fetchJSON). + * * @param {Gerrit.ChangeFetchRequest} req * @return {!Promise<!Object>} */ _getChangeURLAndFetch(req) { const anonymizedEndpoint = req.reportEndpointAsIs ? - req.endpoint : req.anonymizedEndpoint; + req.endpoint : req.anonymizedEndpoint; const anonymizedBaseUrl = req.patchNum ? - ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL; + ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL; return this._changeBaseURL(req.changeNum, req.patchNum).then(url => { return this._restApiHelper.fetchJSON({ url: url + req.endpoint, @@ -2633,13 +2671,14 @@ params: req.params, fetchOptions: req.fetchOptions, anonymizedUrl: anonymizedEndpoint ? - (anonymizedBaseUrl + anonymizedEndpoint) : undefined, + (anonymizedBaseUrl + anonymizedEndpoint) : undefined, }); }); - }, + } /** * Execute a change action or revision action on a change. + * * @param {number} changeNum * @param {string} method * @param {string} endpoint @@ -2658,10 +2697,11 @@ body: opt_payload, errFn: opt_errFn, }); - }, + } /** * Get blame information for the given diff. + * * @param {string|number} changeNum * @param {string|number} patchNum * @param {string} path @@ -2678,11 +2718,12 @@ params: opt_base ? {base: 't'} : undefined, anonymizedEndpoint: '/files/*/blame', }); - }, + } /** * Modify the given create draft request promise so that it fails and throws * an error if the response bears HTTP status 200 instead of HTTP 201. + * * @see Issue 7763 * @param {Promise} promise The original promise. * @return {Promise} The modified promise. @@ -2707,11 +2748,12 @@ } return result; }); - }, + } /** * Fetch a project dashboard definition. * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-dashboard + * * @param {string} project * @param {string} dashboard * @param {function(?Response, string=)=} opt_errFn @@ -2726,7 +2768,7 @@ errFn: opt_errFn, anonymizedUrl: '/projects/*/dashboards/*', }); - }, + } /** * @param {string} filter @@ -2742,7 +2784,7 @@ url: `/Documentation/?q=${encodedFilter}`, anonymizedUrl: '/Documentation/?*', }); - }, + } getMergeable(changeNum) { return this._getChangeURLAndFetch({ @@ -2751,7 +2793,7 @@ parseResponse: true, reportEndpointAsIs: true, }); - }, + } deleteDraftComments(query) { return this._restApiHelper.send({ @@ -2759,6 +2801,8 @@ url: '/accounts/self/drafts:delete', body: {query}, }); - }, - }); + } + } + + customElements.define(GrRestApiInterface.is, GrRestApiInterface); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html index ea71522..a598d0d 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-rest-api-interface</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -351,7 +351,6 @@ }); }); - test('server error', done => { const getResponseObjectStub = sandbox.stub(element, 'getResponseObject'); window.fetch.returns(Promise.resolve({ok: false})); @@ -691,7 +690,7 @@ test('setAccountStatus', () => { const sendStub = sandbox.stub(element._restApiHelper, 'send') - .returns(Promise.resolve('OOO')); + .returns(Promise.resolve('OOO')); element._cache.set('/accounts/self/detail', {}); return element.setAccountStatus('OOO').then(() => { assert.isTrue(sendStub.calledOnce); @@ -702,7 +701,7 @@ {status: 'OOO'}); assert.deepEqual(element._restApiHelper ._cache.get('/accounts/self/detail'), - {status: 'OOO'}); + {status: 'OOO'}); }); });
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js index 5cea96b..3908a00 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js
@@ -122,6 +122,7 @@ /** * Wraps calls to the underlying authenticated fetch function (_auth.fetch) * with timing and logging. + * * @param {Gerrit.FetchRequest} req */ fetch(req) { @@ -139,6 +140,7 @@ * Log information about a REST call. Because the elapsed time is determined * by this method, it should be called immediately after the request * finishes. + * * @param {Gerrit.FetchRequest} req * @param {number} startTime the time that the request was started. * @param {number} status the HTTP status of the response. The status value @@ -147,7 +149,7 @@ */ _logCall(req, startTime, status) { const method = (req.fetchOptions && req.fetchOptions.method) ? - req.fetchOptions.method : 'GET'; + req.fetchOptions.method : 'GET'; const endTime = Date.now(); const elapsed = (endTime - startTime); const startAt = new Date(startTime); @@ -171,6 +173,7 @@ * Returns a Promise that resolves to a native Response. * Doesn't do error checking. Supports cancel condition. Performs auth. * Validates auth expiry errors. + * * @param {Gerrit.FetchJSONRequest} req */ fetchRawJSON(req) { @@ -205,6 +208,7 @@ * Fetch JSON from url provided. * Returns a Promise that resolves to a parsed response. * Same as {@link fetchRawJSON}, plus error handling. + * * @param {Gerrit.FetchJSONRequest} req */ fetchJSON(req) { @@ -329,6 +333,7 @@ /** * Send an XHR. + * * @param {Gerrit.SendRequest} req * @return {Promise} */ @@ -339,7 +344,7 @@ options.headers.set( 'Content-Type', req.contentType || 'application/json'); options.body = typeof req.body === 'string' ? - req.body : JSON.stringify(req.body); + req.body : JSON.stringify(req.body); } if (req.headers) { if (!options.headers) { options.headers = new Headers(); } @@ -349,7 +354,7 @@ } } const url = req.url.startsWith('http') ? - req.url : this.getBaseUrl() + req.url; + req.url : this.getBaseUrl() + req.url; const fetchReq = { url, fetchOptions: options,
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html index 4eaf1bc..446cfc8 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-rest-api-helper</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js index e14b955..8310191 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js
@@ -60,6 +60,7 @@ /** * Is a part of _groupUpdates(). Creates a new batch of updates. + * * @param {Object} update instance of ReviewerUpdateInfo */ GrReviewerUpdatesParser.prototype._startBatch = function(update) { @@ -75,6 +76,7 @@ * Is a part of _groupUpdates(). Validates current batch: * - filters out updates that don't change reviewer state. * - updates current reviewer state. + * * @param {Object} update instance of ReviewerUpdateInfo */ GrReviewerUpdatesParser.prototype._completeBatch = function(update) { @@ -136,6 +138,7 @@ /** * Generates update message for reviewer state change. + * * @param {string} prev previous reviewer state. * @param {string} state current reviewer state. * @return {string} @@ -157,6 +160,7 @@ /** * Groups updates for same category (eg CC->CC) into a hash arrays of * reviewers. + * * @param {!Array<!Object>} updates Array of ReviewerUpdateItemInfo. * @return {!Object} Hash of arrays of AccountInfo, message as key. */ @@ -174,6 +178,7 @@ /** * Generates text messages for grouped reviewer updates. * Formats reviewer updates to a (not yet implemented) EventInfo instance. + * * @see https://gerrit-review.googlesource.com/c/94490/ */ GrReviewerUpdatesParser.prototype._formatUpdates = function() { @@ -203,15 +208,15 @@ messages.forEach((message, index) => { const messageDate = util.parseDate(message.date).getTime(); const nextMessageDate = index === messages.length - 1 ? null : - util.parseDate(messages[index + 1].date).getTime(); + util.parseDate(messages[index + 1].date).getTime(); for (const update of updates) { const date = util.parseDate(update.date).getTime(); - if (date >= messageDate - && (!nextMessageDate || date < nextMessageDate)) { + if (date >= messageDate && + (!nextMessageDate || date < nextMessageDate)) { const timestamp = util.parseDate(update.date).getTime() - GrReviewerUpdatesParser.MESSAGE_REVIEWERS_THRESHOLD_MILLIS; update.date = new Date(timestamp) - .toISOString().replace('T', ' ').replace('Z', '000000'); + .toISOString().replace('T', ' ').replace('Z', '000000'); } if (nextMessageDate && date > nextMessageDate) { break;
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html index fdf79af..ad3e4ca 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-reviewer-updates-parser</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js index 05267ba..c9f13de 100644 --- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js +++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
@@ -17,29 +17,29 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-select', + /** + * @appliesMixin Gerrit.FireMixin + */ + class GrSelect extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-select'; } - properties: { - bindValue: { - type: String, - notify: true, - observer: '_updateValue', - }, - }, - - behaviors: [ - Gerrit.FireBehavior, - ], - - listeners: { - 'change': '_valueChanged', - 'dom-change': '_updateValue', - }, + static get properties() { + return { + bindValue: { + type: String, + notify: true, + observer: '_updateValue', + }, + }; + } get nativeSelect() { return this.$$('select'); - }, + } _updateValue() { // It's possible to have a value of 0. @@ -53,21 +53,32 @@ this.nativeSelect.value = this.bindValue; }, 1); } - }, + } _valueChanged() { this.bindValue = this.nativeSelect.value; - }, + } focus() { this.nativeSelect.focus(); - }, + } + + created() { + super.created(); + this.addEventListener('change', + () => this._valueChanged()); + this.addEventListener('dom-change', + () => this._updateValue()); + } ready() { + super.ready(); // If not set via the property, set bind-value to the element value. if (this.bindValue == undefined && this.nativeSelect.options.length > 0) { this.bindValue = this.nativeSelect.value; } - }, - }); + } + } + + customElements.define(GrSelect.is, GrSelect); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html index b3abe5f..cae61056 100644 --- a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html +++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-select</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js index 2c546cc..9456991 100644 --- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js +++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
@@ -17,16 +17,22 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-shell-command', + class GrShellCommand extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-shell-command'; } - properties: { - command: String, - label: String, - }, + static get properties() { + return { + command: String, + label: String, + }; + } focusOnCopy() { this.$$('gr-copy-clipboard').focusOnCopy(); - }, - }); + } + } + + customElements.define(GrShellCommand.is, GrShellCommand); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html index 3f2f8ba..a4bdf58 100644 --- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html +++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-shell-command</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js index 9ae77d9..7950e51 100644 --- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js +++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
@@ -28,69 +28,73 @@ 'editablecontent:', ]; - Polymer({ - is: 'gr-storage', + class GrStorage extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-storage'; } - properties: { - _lastCleanup: Number, - /** @type {?Storage} */ - _storage: { - type: Object, - value() { - return window.localStorage; + static get properties() { + return { + _lastCleanup: Number, + /** @type {?Storage} */ + _storage: { + type: Object, + value() { + return window.localStorage; + }, }, - }, - _exceededQuota: { - type: Boolean, - value: false, - }, - }, + _exceededQuota: { + type: Boolean, + value: false, + }, + }; + } getDraftComment(location) { this._cleanupItems(); return this._getObject(this._getDraftKey(location)); - }, + } setDraftComment(location, message) { const key = this._getDraftKey(location); this._setObject(key, {message, updated: Date.now()}); - }, + } eraseDraftComment(location) { const key = this._getDraftKey(location); this._storage.removeItem(key); - }, + } getEditableContentItem(key) { this._cleanupItems(); return this._getObject(this._getEditableContentKey(key)); - }, + } setEditableContentItem(key, message) { this._setObject(this._getEditableContentKey(key), {message, updated: Date.now()}); - }, + } eraseEditableContentItem(key) { this._storage.removeItem(this._getEditableContentKey(key)); - }, + } _getDraftKey(location) { const range = location.range ? - `${location.range.start_line}-${location.range.start_character}` + + `${location.range.start_line}-${location.range.start_character}` + `-${location.range.end_character}-${location.range.end_line}` : - null; + null; let key = ['draft', location.changeNum, location.patchNum, location.path, location.line || ''].join(':'); if (range) { key = key + ':' + range; } return key; - }, + } _getEditableContentKey(key) { return `editablecontent:${key}`; - }, + } _cleanupItems() { // Throttle cleanup to the throttle interval. @@ -113,13 +117,13 @@ } } } - }, + } _getObject(key) { const serial = this._storage.getItem(key); if (!serial) { return null; } return JSON.parse(serial); - }, + } _setObject(key, obj) { if (this._exceededQuota) { return; } @@ -136,6 +140,8 @@ throw exc; } } - }, - }); + } + } + + customElements.define(GrStorage.is, GrStorage); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html index 0482584..5cd4d3e 100644 --- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html +++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
@@ -17,7 +17,7 @@ --> <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-storage</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js index 4c4b038..5b17a38 100644 --- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js +++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
@@ -52,73 +52,81 @@ {value: '😜', match: 'winking tongue ;)'}, ]; - Polymer({ - is: 'gr-textarea', - + /** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + */ + class GrTextarea extends Polymer.mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-textarea'; } /** * @event bind-value-changed */ - properties: { - autocomplete: Boolean, - disabled: Boolean, - rows: Number, - maxRows: Number, - placeholder: String, - text: { - type: String, - notify: true, - observer: '_handleTextChanged', - }, - hideBorder: { - type: Boolean, - value: false, - }, - /** Text input should be rendered in monspace font. */ - monospace: { - type: Boolean, - value: false, - }, - /** Text input should be rendered in code font, which is smaller than the + static get properties() { + return { + autocomplete: Boolean, + disabled: Boolean, + rows: Number, + maxRows: Number, + placeholder: String, + text: { + type: String, + notify: true, + observer: '_handleTextChanged', + }, + hideBorder: { + type: Boolean, + value: false, + }, + /** Text input should be rendered in monspace font. */ + monospace: { + type: Boolean, + value: false, + }, + /** Text input should be rendered in code font, which is smaller than the standard monospace font. */ - code: { - type: Boolean, - value: false, - }, - /** @type(?number) */ - _colonIndex: Number, - _currentSearchString: { - type: String, - observer: '_determineSuggestions', - }, - _hideAutocomplete: { - type: Boolean, - value: true, - }, - _index: Number, - _suggestions: Array, - // Offset makes dropdown appear below text. - _verticalOffset: { - type: Number, - value: 20, - readOnly: true, - }, - }, + code: { + type: Boolean, + value: false, + }, + /** @type(?number) */ + _colonIndex: Number, + _currentSearchString: { + type: String, + observer: '_determineSuggestions', + }, + _hideAutocomplete: { + type: Boolean, + value: true, + }, + _index: Number, + _suggestions: Array, + // Offset makes dropdown appear below text. + _verticalOffset: { + type: Number, + value: 20, + readOnly: true, + }, + }; + } - behaviors: [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - ], - - keyBindings: { - esc: '_handleEscKey', - tab: '_handleEnterByKey', - enter: '_handleEnterByKey', - up: '_handleUpKey', - down: '_handleDownKey', - }, + get keyBindings() { + return { + esc: '_handleEscKey', + tab: '_handleEnterByKey', + enter: '_handleEnterByKey', + up: '_handleUpKey', + down: '_handleDownKey', + }; + } ready() { + super.ready(); if (this.monospace) { this.classList.add('monospace'); } @@ -128,15 +136,15 @@ if (this.hideBorder) { this.$.textarea.classList.add('noBorder'); } - }, + } closeDropdown() { return this.$.emojiSuggestions.close(); - }, + } getNativeTextarea() { return this.$.textarea.textarea; - }, + } putCursorAtEnd() { const textarea = this.getNativeTextarea(); @@ -146,14 +154,14 @@ this.async(() => { textarea.focus(); }); - }, + } _handleEscKey(e) { if (this._hideAutocomplete) { return; } e.preventDefault(); e.stopPropagation(); this._resetEmojiDropdown(); - }, + } _handleUpKey(e) { if (this._hideAutocomplete) { return; } @@ -162,7 +170,7 @@ this.$.emojiSuggestions.cursorUp(); this.$.textarea.textarea.focus(); this.disableEnterKeyForSelectingEmoji = false; - }, + } _handleDownKey(e) { if (this._hideAutocomplete) { return; } @@ -171,7 +179,7 @@ this.$.emojiSuggestions.cursorDown(); this.$.textarea.textarea.focus(); this.disableEnterKeyForSelectingEmoji = false; - }, + } _handleEnterByKey(e) { if (this._hideAutocomplete || this.disableEnterKeyForSelectingEmoji) { @@ -180,11 +188,11 @@ e.preventDefault(); e.stopPropagation(); this._setEmoji(this.$.emojiSuggestions.getCurrentText()); - }, + } _handleEmojiSelect(e) { this._setEmoji(e.detail.selected.dataset.value); - }, + } _setEmoji(text) { const colonIndex = this._colonIndex; @@ -193,12 +201,13 @@ this.$.textarea.selectionEnd = colonIndex + 1; this.$.reporting.reportInteraction('select-emoji'); this._resetEmojiDropdown(); - }, + } _getText(value) { return this.text.substr(0, this._colonIndex || 0) + value + this.text.substr(this.$.textarea.selectionStart); - }, + } + /** * Uses a hidden element with the same width and styling of the textarea and * the text up until the point of interest. Then caratSpan element is added @@ -214,17 +223,17 @@ this.$.hiddenText.appendChild(caratSpan); this.$.emojiSuggestions.positionTarget = caratSpan; this._openEmojiDropdown(); - }, + } _getFontSize() { const fontSizePx = getComputedStyle(this).fontSize || '12px'; return parseInt(fontSizePx.substr(0, fontSizePx.length - 2), 10); - }, + } _getScrollTop() { return document.body.scrollTop; - }, + } /** * _handleKeydown used for key handling in the this.$.textarea AND all child @@ -271,12 +280,12 @@ this._updateCaratPosition(); } this.$.textarea.textarea.focus(); - }, + } _openEmojiDropdown() { this.$.emojiSuggestions.open(); this.$.reporting.reportInteraction('open-emoji-dropdown'); - }, + } _formatSuggestions(matchedSuggestions) { const suggestions = []; @@ -286,7 +295,7 @@ suggestions.push(suggestion); } this.set('_suggestions', suggestions); - }, + } _determineSuggestions(emojiText) { if (!emojiText.length) { @@ -299,7 +308,7 @@ this._formatSuggestions(matches); this.disableEnterKeyForSelectingEmoji = false; } - }, + } _resetEmojiDropdown() { // hide and reset the autocomplete dropdown. @@ -309,11 +318,13 @@ this.closeDropdown(); this._colonIndex = null; this.$.textarea.textarea.focus(); - }, + } _handleTextChanged(text) { this.dispatchEvent( new CustomEvent('value-changed', {detail: {value: text}})); - }, - }); + } + } + + customElements.define(GrTextarea.is, GrTextarea); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html index ae1de02..699deab 100644 --- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html +++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-textarea</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -209,17 +209,17 @@ assert.isTrue(formatSpy.lastCall.calledWithExactly( [{dataValue: '😂', value: '😂', match: 'tears :\')', text: '😂 tears :\')'}, - {dataValue: '😢', value: '😢', match: 'tear', text: '😢 tear'}, + {dataValue: '😢', value: '😢', match: 'tear', text: '😢 tear'}, ])); }); test('_formatSuggestions', () => { const matchedSuggestions = [{value: '😢', match: 'tear'}, - {value: '😂', match: 'tears'}]; + {value: '😂', match: 'tears'}]; element._formatSuggestions(matchedSuggestions); assert.deepEqual( [{value: '😢', dataValue: '😢', match: 'tear', text: '😢 tear'}, - {value: '😂', dataValue: '😂', match: 'tears', text: '😂 tears'}], + {value: '😂', dataValue: '😂', match: 'tears', text: '😂 tears'}], element._suggestions); });
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js index c5de8f4..22a5ecb 100644 --- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js +++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js
@@ -17,31 +17,38 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-tooltip-content', + /** + * @appliesMixin Gerrit.TooltipMixin + */ + class GrTooltipContent extends Polymer.mixinBehaviors( [ + Gerrit.TooltipBehavior, + ], Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element))) { + static get is() { return 'gr-tooltip-content'; } - properties: { - title: { - type: String, - reflectToAttribute: true, - }, - maxWidth: { - type: String, - reflectToAttribute: true, - }, - positionBelow: { - type: Boolean, - valye: false, - reflectToAttribute: true, - }, - showIcon: { - type: Boolean, - value: false, - }, - }, + static get properties() { + return { + title: { + type: String, + reflectToAttribute: true, + }, + maxWidth: { + type: String, + reflectToAttribute: true, + }, + positionBelow: { + type: Boolean, + valye: false, + reflectToAttribute: true, + }, + showIcon: { + type: Boolean, + value: false, + }, + }; + } + } - behaviors: [ - Gerrit.TooltipBehavior, - ], - }); + customElements.define(GrTooltipContent.is, GrTooltipContent); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html index f9350c6..4276195 100644 --- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html +++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
@@ -17,7 +17,7 @@ --> <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-storage</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js index fb87b558..170a442 100644 --- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js +++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
@@ -17,23 +17,29 @@ (function() { 'use strict'; - Polymer({ - is: 'gr-tooltip', + class GrTooltip extends Polymer.GestureEventListeners( + Polymer.LegacyElementMixin( + Polymer.Element)) { + static get is() { return 'gr-tooltip'; } - properties: { - text: String, - maxWidth: { - type: String, - observer: '_updateWidth', - }, - positionBelow: { - type: Boolean, - reflectToAttribute: true, - }, - }, + static get properties() { + return { + text: String, + maxWidth: { + type: String, + observer: '_updateWidth', + }, + positionBelow: { + type: Boolean, + reflectToAttribute: true, + }, + }; + } _updateWidth(maxWidth) { this.updateStyles({'--tooltip-max-width': maxWidth}); - }, - }); + } + } + + customElements.define(GrTooltip.is, GrTooltip); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html index f59f6e1..95f3922 100644 --- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html +++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html
@@ -17,7 +17,7 @@ --> <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-storage</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/revision-info/revision-info.html b/polygerrit-ui/app/elements/shared/revision-info/revision-info.html index fca8ae1..48e488a 100644 --- a/polygerrit-ui/app/elements/shared/revision-info/revision-info.html +++ b/polygerrit-ui/app/elements/shared/revision-info/revision-info.html
@@ -32,6 +32,7 @@ * example, with normal changes this will always return 1. For merge changes * wherein the revisions are merge commits this will return 2 or potentially * more. + * * @return {Number} */ RevisionInfo.prototype.getMaxParents = function() { @@ -45,6 +46,7 @@ /** * Get an object that maps revision numbers to the number of parents of the * commit of that revision. + * * @return {!Object} */ RevisionInfo.prototype.getParentCountMap = function() { @@ -68,13 +70,14 @@ /** * Get the commit ID of the (0-offset) indexed parent in the given revision * number. + * * @param {number|string} patchNum * @param {number} parentIndex (0-offset) * @return {string} */ RevisionInfo.prototype.getParentId = function(patchNum, parentIndex) { const rev = Object.values(this._change.revisions).find(rev => - Gerrit.PatchSetBehavior.patchNumEquals(rev._number, patchNum)); + Gerrit.PatchSetBehavior.patchNumEquals(rev._number, patchNum)); return rev.commit.parents[parentIndex].commit; };
diff --git a/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html b/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html index 7e5810b..187ce35 100644 --- a/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html +++ b/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>revision-info</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/embed/embed.html b/polygerrit-ui/app/embed/embed.html deleted file mode 100644 index 64e0137..0000000 --- a/polygerrit-ui/app/embed/embed.html +++ /dev/null
@@ -1,27 +0,0 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> -<script> - window.Gerrit = window.Gerrit || {}; -</script> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../elements/change/gr-change-view/gr-change-view.html"> -<link rel="import" href="../elements/core/gr-search-bar/gr-search-bar.html"> -<link rel="import" href="../elements/diff/gr-diff-view/gr-diff-view.html"> -<link rel="import" href="../elements/change-list/gr-change-list-view/gr-change-list-view.html"> -<link rel="import" href="../elements/change-list/gr-dashboard-view/gr-dashboard-view.html"> -<link rel="import" href="../elements/change-list/gr-embed-dashboard/gr-embed-dashboard.html"> -<link rel="import" href="../styles/themes/app-theme.html">
diff --git a/polygerrit-ui/app/embed/embed_test.html b/polygerrit-ui/app/embed/embed_test.html deleted file mode 100644 index 1e3f5d7..0000000 --- a/polygerrit-ui/app/embed/embed_test.html +++ /dev/null
@@ -1,98 +0,0 @@ -<!DOCTYPE html> -<!-- -@license -Copyright (C) 2017 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> -<title>embed_test</title> -<script src="/test/common-test-setup.js"></script> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> - -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<link rel="import" href="embed.html"/> - -<script>void(0);</script> - -<test-fixture id="change-view"> - <template> - <gr-change-view></gr-change-view> - </template> -</test-fixture> - -<test-fixture id="diff-view"> - <template> - <gr-diff-view></gr-diff-view> - </template> -</test-fixture> - -<test-fixture id="dashboard-view"> - <template> - <gr-dashboard-view></gr-dashboard-view> - </template> -</test-fixture> - -<test-fixture id="change-list-view"> - <template> - <gr-change-list-view></gr-change-list-view> - </template> -</test-fixture> - -<test-fixture id="change-list"> - <template> - <gr-change-list></gr-change-list> - </template> -</test-fixture> - -<test-fixture id="search-bar"> - <template> - <gr-search-bar></gr-search-bar> - </template> -</test-fixture> - -<script> - suite('embed test', () => { - test('gr-change-view is embedded', () => { - const element = fixture('change-view'); - assert.equal(element.is, 'gr-change-view'); - }); - - test('diff-view is embedded', () => { - const element = fixture('diff-view'); - assert.equal(element.is, 'gr-diff-view'); - }); - - test('dashboard-view is embedded', () => { - const element = fixture('dashboard-view'); - assert.equal(element.is, 'gr-dashboard-view'); - }); - - test('change-list-view is embedded', () => { - const element = fixture('change-list-view'); - assert.equal(element.is, 'gr-change-list-view'); - }); - - test('change-list is embedded', () => { - const element = fixture('change-list'); - assert.equal(element.is, 'gr-change-list'); - }); - - test('search-bar is embedded', () => { - const element = fixture('search-bar'); - assert.equal(element.is, 'gr-search-bar'); - }); - }); -</script>
diff --git a/polygerrit-ui/app/embed/test.html b/polygerrit-ui/app/embed/test.html deleted file mode 100644 index 955eaee..0000000 --- a/polygerrit-ui/app/embed/test.html +++ /dev/null
@@ -1,26 +0,0 @@ -<!DOCTYPE html> -<!-- -@license -Copyright (C) 2017 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> -<title>Embed Test Runner</title> -<meta charset="utf-8"> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script> - WCT.loadSuites(['../embed/embed_test.html']); -</script>
diff --git a/polygerrit-ui/app/embed_test.sh b/polygerrit-ui/app/embed_test.sh deleted file mode 100755 index 0d8f58f..0000000 --- a/polygerrit-ui/app/embed_test.sh +++ /dev/null
@@ -1,69 +0,0 @@ -#!/bin/sh - -set -ex - -t=$(mktemp -d || mktemp -d -t wct-XXXXXXXXXX) -components=$TEST_SRCDIR/gerrit/polygerrit-ui/app/test_components.zip -code=$TEST_SRCDIR/gerrit/polygerrit-ui/app/pg_code.zip - -echo $t -unzip -qd $t $components -unzip -qd $t $code -# Purge test/ directory contents coming from pg_code.zip. -rm -rf $t/test -mkdir -p $t/test -cp $TEST_SRCDIR/gerrit/polygerrit-ui/app/embed/test.html $t/test/ - -if [ "${WCT_HEADLESS_MODE:-0}" != "0" ]; then - CHROME_OPTIONS=[\'start-maximized\',\'headless\',\'disable-gpu\',\'no-sandbox\'] - FIREFOX_OPTIONS=[\'-headless\'] -else - CHROME_OPTIONS=[\'start-maximized\'] - FIREFOX_OPTIONS=[\'\'] -fi - -# For some reason wct tries to install selenium into its node_modules -# directory on first run. If you've installed into /usr/local and -# aren't running wct as root, you're screwed. Turning this option off -# through skipSeleniumInstall seems to still work, so there's that. - -# Sauce tests are disabled by default in order to run local tests -# only. Run it with (saucelabs.com account required; free for open -# source): WCT_ARGS='--plugin sauce' ./polygerrit-ui/app/embed_test.sh - -cat <<EOF > $t/wct.conf.js -module.exports = { - 'suites': ['test'], - 'webserver': { - 'pathMappings': [ - {'/components/bower_components': 'bower_components'} - ] - }, - 'plugins': { - 'local': { - 'skipSeleniumInstall': true, - 'browserOptions': { - 'chrome': ${CHROME_OPTIONS}, - 'firefox': ${FIREFOX_OPTIONS} - } - }, - 'sauce': { - 'disabled': true, - 'browsers': [ - 'OS X 10.12/chrome', - 'Windows 10/chrome', - 'Linux/firefox', - 'OS X 10.12/safari', - 'Windows 10/microsoftedge' - ] - } - } - }; -EOF - -export PATH="$(dirname $NPM):$PATH" - -cd $t -test -n "${WCT}" - -${WCT} ${WCT_ARGS}
diff --git a/polygerrit-ui/app/lint_test.sh b/polygerrit-ui/app/lint_test.sh index fce5fdd..4e7d8c9 100755 --- a/polygerrit-ui/app/lint_test.sh +++ b/polygerrit-ui/app/lint_test.sh
@@ -1,7 +1,14 @@ #!/bin/sh +# DEPRECATED: This file is only used by Gerrit CI for now +# To run eslint test on FE code, run `npm run eslint` instead. +# `bazel run lint_test` will be changed to be an alias to +# `npm run eslint` soon. + set -ex +echo "DEPRECATED: please run `npm run eslint` instead."; + npm_bin=$(which npm) && true if [ -z "$npm_bin" ]; then echo "NPM must be on the path." @@ -28,6 +35,6 @@ # Linking global eslint packages to the local project. Required to use eslint plugins with a global # eslint installation. -npm link eslint eslint-config-google eslint-plugin-html +npm link eslint eslint-config-google eslint-plugin-html eslint-plugin-jsdoc -${eslint_bin} -c ${UI_PATH}/.eslintrc.json --ignore-pattern 'node_modules/' --ignore-pattern 'bower_components/' --ignore-pattern 'gr-linked-text' --ignore-pattern 'scripts/vendor' --ext .html,.js ${UI_PATH} +${eslint_bin} -c ${UI_PATH}/.eslintrc.json --ignore-pattern 'node_modules/' --ignore-pattern 'bower_components/' --ignore-pattern 'scripts/vendor' --ext .html,.js ${UI_PATH}
diff --git a/polygerrit-ui/app/polylint_test.sh b/polygerrit-ui/app/polylint_test.sh index f6880a1..98cf06f 100755 --- a/polygerrit-ui/app/polylint_test.sh +++ b/polygerrit-ui/app/polylint_test.sh
@@ -8,13 +8,24 @@ exit 1 fi -npx_bin=$(which npx) -if [[ -z "$npx_bin" ]]; then - echo "NPX must be on the path." - echo "> npm i -g npx" +node_bin=$(which node) +if [[ -z "$node_bin" ]]; then + echo "node must be on the path." + exit 1 +fi + +polymer_bin=$(which polymer) +if [[ -z "$polymer_bin" ]]; then + polymer_bin=$(abs_path ./node_modules/polymer-cli/bin/polymer.js); +fi +if [[ -z "$polymer_bin" ]]; then + echo "polymer must be set or polymer-cli locally installed (npm install polymer-cli)." exit 1 fi unzip -o polygerrit-ui/polygerrit_components.bower_components.zip -d polygerrit-ui/app -npx polylint --root polygerrit-ui/app --input elements/gr-app.html --b 'bower_components' --verbose +#Can't use --root with polymer.json - see https://github.com/Polymer/tools/issues/2616 +#Change current directory to the root folder +cd polygerrit-ui/app +$polymer_bin lint --component-dir 'bower_components' --verbose
diff --git a/polygerrit-ui/app/polymer.json b/polygerrit-ui/app/polymer.json new file mode 100644 index 0000000..411c969 --- /dev/null +++ b/polygerrit-ui/app/polymer.json
@@ -0,0 +1,14 @@ +{ + "entrypoint": "elements/gr-app.html", + "sources": [ + "behaviors/**/*", + "elements/**/*", + "scripts/**/*", + "styles/*", + "types/**/*" + ], + "lint": { + "rules": ["polymer-2"], + "ignoreWarnings": ["deprecated-dom-call"] + } +}
diff --git a/polygerrit-ui/app/run_test.sh b/polygerrit-ui/app/run_test.sh index e9be18d..76a4fa1 100755 --- a/polygerrit-ui/app/run_test.sh +++ b/polygerrit-ui/app/run_test.sh
@@ -48,5 +48,4 @@ --test_env="DISPLAY=${DISPLAY}" \ --test_env="WCT_HEADLESS_MODE=${WCT_HEADLESS_MODE}" \ "$@" \ - //polygerrit-ui/app:embed_test \ //polygerrit-ui/app:wct_test
diff --git a/polygerrit-ui/app/samples/coverage-plugin.html b/polygerrit-ui/app/samples/coverage-plugin.html index f5f9c6e..711015e 100644 --- a/polygerrit-ui/app/samples/coverage-plugin.html +++ b/polygerrit-ui/app/samples/coverage-plugin.html
@@ -1,6 +1,5 @@ <dom-module id="coverage-plugin"> <script> - function populateWithDummyData(coverageData) { coverageData['NewFile'] = { linesMissingCoverage: [1, 2, 3], @@ -39,16 +38,16 @@ annotationApi.addLayer(context => { if (Object.keys(coverageData).length === 0) { - // Coverage data is not ready yet. + // Coverage data is not ready yet. return; } const path = context.path; const line = context.line; - // Highlight lines missing coverage with this background color if - // coverage should be displayed, else do nothing. - const annotationStyle = displayCoverage - ? coverageStyle - : emptyStyle; + // Highlight lines missing coverage with this background color if + // coverage should be displayed, else do nothing. + const annotationStyle = displayCoverage ? + coverageStyle : + emptyStyle; if (coverageData[path] && coverageData[path].changeNum === context.changeNum && coverageData[path].patchNum === context.patchNum) { @@ -61,6 +60,8 @@ }).enableToggleCheckbox('Display Coverage', checkbox => { // Checkbox is attached so now add the notifier that will be controlled // by the checkbox. + // Checkbox will only be added to the file diff page, in the top right + // section near the "Diff view". annotationApi.addNotifier(notifyFunc => { new Promise(resolve => setTimeout(resolve, 3000)).then(() => { populateWithDummyData(coverageData);
diff --git a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html index 25ca4c5..a35e034 100644 --- a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html +++ b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-display-name-utils</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -35,7 +35,6 @@ }, }; - test('getUserName name only', () => { const account = { name: 'test-name',
diff --git a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html index fb6b5d4..ac32efa 100644 --- a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html +++ b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-email-suggestions-provider</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> @@ -68,7 +68,7 @@ test('getSuggestions', done => { const getSuggestedAccountsStub = sandbox.stub(restAPI, 'getSuggestedAccounts') - .returns(Promise.resolve([account1, account2])); + .returns(Promise.resolve([account1, account2])); provider.getSuggestions('Some input').then(res => { assert.deepEqual(res, [account1, account2]);
diff --git a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html index b60aaa9..6908256 100644 --- a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html +++ b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-group-suggestions-provider</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js index c83e5a2..6b97cf6 100644 --- a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js +++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js
@@ -35,14 +35,15 @@ switch (usersType) { case Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER: return new GrReviewerSuggestionsProvider(restApi, changeNumber, - input => restApi.getChangeSuggestedReviewers(changeNumber, input)); + input => + restApi.getChangeSuggestedReviewers(changeNumber, input)); case Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.CC: return new GrReviewerSuggestionsProvider(restApi, changeNumber, - input => restApi.getChangeSuggestedCCs(changeNumber, input)); + input => restApi.getChangeSuggestedCCs(changeNumber, input)); case Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY: return new GrReviewerSuggestionsProvider(restApi, changeNumber, - input => restApi.getSuggestedAccounts( - `cansee:${changeNumber} ${input}`)); + input => restApi.getSuggestedAccounts( + `cansee:${changeNumber} ${input}`)); default: throw new Error(`Unknown users type: ${usersType}`); } @@ -65,9 +66,9 @@ this._loggedIn = loggedIn; }); this._initPromise = Promise.all([getConfigPromise, getLoggedInPromise]) - .then(() => { - this._initialized = true; - }); + .then(() => { + this._initialized = true; + }); return this._initPromise; }
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html index ca3c277..6fcc4dc 100644 --- a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html +++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html
@@ -18,7 +18,7 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-reviewer-suggestions-provider</title> -<script src="/test/common-test-setup.js"></script> + <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.html b/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.html index fef3872..84692ba 100644 --- a/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.html +++ b/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.html
@@ -16,7 +16,9 @@ --> <dom-module id="gr-change-metadata-shared-styles"> <template> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style> section { display: table-row;
diff --git a/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.html b/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.html index 834f64a..76e978c 100644 --- a/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.html +++ b/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.html
@@ -25,8 +25,14 @@ --> <dom-module id="gr-change-view-integration-shared-styles"> <template> - <style include="shared-styles"></style> + <style include="shared-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> <style> + :host { + border-top: 1px solid var(--border-color); + display: block; + } .header { color: var(--primary-text-color); background-color: var(--table-header-background-color);
diff --git a/polygerrit-ui/app/styles/gr-form-styles.html b/polygerrit-ui/app/styles/gr-form-styles.html index 3fe0a72..7c9ae0d 100644 --- a/polygerrit-ui/app/styles/gr-form-styles.html +++ b/polygerrit-ui/app/styles/gr-form-styles.html
@@ -61,7 +61,6 @@ } .gr-form-styles td, .gr-form-styles tfoot th { - height: 2em; padding: var(--spacing-s) 0; vertical-align: middle; } @@ -84,8 +83,7 @@ .gr-form-styles textarea { border: 1px solid var(--border-color); border-radius: var(--border-radius); - height: 2em; - padding: 0 var(--spacing-xs); + padding: var(--spacing-s); } .gr-form-styles td:last-child { width: 5em; @@ -97,23 +95,16 @@ .gr-form-styles iron-autogrow-textarea { border: none; height: auto; - min-height: 2em; + min-height: 4em; --iron-autogrow-textarea: { border: 1px solid var(--border-color); border-radius: var(--border-radius); box-sizing: border-box; - padding: var(--spacing-s) var(--spacing-xs) 0 var(--spacing-xs); + padding: var(--spacing-s); } } .gr-form-styles gr-autocomplete { - border: none; - --gr-autocomplete: { - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - height: 2em; - padding: 0 var(--spacing-xs); - width: 14em; - } + width: 14em; } @media only screen and (max-width: 40em) { .gr-form-styles section {
diff --git a/polygerrit-ui/app/styles/shared-styles.html b/polygerrit-ui/app/styles/shared-styles.html index 5314741..51b92e1 100644 --- a/polygerrit-ui/app/styles/shared-styles.html +++ b/polygerrit-ui/app/styles/shared-styles.html
@@ -42,10 +42,11 @@ input { background-color: inherit; border: 1px solid var(--border-color); + border-radius: var(--border-radius); box-sizing: border-box; color: var(--primary-text-color); margin: 0; - padding: 0; + padding: var(--spacing-s); } iron-autogrow-textarea { background-color: inherit;
diff --git a/polygerrit-ui/app/styles/themes/app-theme.html b/polygerrit-ui/app/styles/themes/app-theme.html index 56add09..53827b5 100644 --- a/polygerrit-ui/app/styles/themes/app-theme.html +++ b/polygerrit-ui/app/styles/themes/app-theme.html
@@ -36,33 +36,38 @@ --primary-button-text-color: white; /* Used on text color for change list that doesn't need user's attention. */ --reviewed-text-color: black; - --secondary-button-text-color: #212121; --tooltip-text-color: white; --vote-text-color-recommended: #388e3c; --vote-text-color-disliked: #d32f2f; /* background colors */ + /* primary background colors */ + --background-color-primary: #ffffff; + --background-color-secondary: #f8f9fa; + --background-color-tertiary: #f1f3f4; + /* directly derived from primary background colors */ + --chip-background-color: var(--background-color-tertiary); + --default-button-background-color: var(--background-color-primary); + --dialog-background-color: var(--background-color-primary); + --dropdown-background-color: var(--background-color-primary); + --expanded-background-color: var(--background-color-tertiary); + --select-background-color: var(--background-color-secondary); + --shell-command-background-color: var(--background-color-secondary); + --shell-command-decoration-background-color: var(--background-color-tertiary); + --table-header-background-color: var(--background-color-secondary); + --table-subheader-background-color: var(--background-color-tertiary); + --view-background-color: var(--background-color-primary); + /* unique background colors */ --assignee-highlight-color: #fcfad6; - --chip-background-color: #eee; --comment-background-color: #fcfad6; - --default-button-background-color: white; - --dialog-background-color: white; - --dropdown-background-color: white; + --robot-comment-background-color: #e8f0fe; --edit-mode-background-color: #ebf5fb; --emphasis-color: #fff9c4; - --expanded-background-color: #eee; --hover-background-color: rgba(161, 194, 250, 0.2); --primary-button-background-color: #2a66d9; - --secondary-button-background-color: white; - --select-background-color: #f8f8f8; --selection-background-color: rgba(161, 194, 250, 0.1); - --shell-command-background-color: #f5f5f5; - --shell-command-decoration-background-color: #ebebeb; - --table-header-background-color: #fafafa; - --table-subheader-background-color: #eaeaea; --tooltip-background-color: #333; --unresolved-comment-background-color: #fcfaa6; - --view-background-color: white; --vote-color-approved: #9fcc6b; --vote-color-disliked: #f7c4cb; --vote-color-neutral: #ebf5fb; @@ -70,11 +75,11 @@ --vote-color-rejected: #f7a1ad; /* misc colors */ - --border-color: #ddd; + --border-color: #dadce0; /* fonts */ --font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; - --monospace-font-family: 'Roboto Mono', Menlo, 'Lucida Console', Monaco, monospace; + --monospace-font-family: 'Roboto Mono', 'SF Mono', 'Lucida Console', Monaco, monospace; --font-size-code: 12px; /* 12px mono */ --font-size-mono: .929rem; /* 13px mono */ --font-size-small: .857rem; /* 12px */ @@ -102,9 +107,9 @@ --spacing-xxl: 24px; /* header and footer */ - --footer-background-color: #eee; + --footer-background-color: var(--background-color-tertiary); --footer-border-top: 1px solid var(--border-color); - --header-background-color: #eee; + --header-background-color: var(--background-color-tertiary); --header-border-bottom: 1px solid var(--border-color); --header-border-image: ''; --header-box-shadow: none; @@ -133,6 +138,8 @@ --light-rebased-add-highlight-color: #eef; --light-remove-add-highlight-color: #fff8dc; --light-remove-highlight-color: #ffebee; + --coverage-covered: #e0f2f1; + --coverage-not-covered: #ffd1a4; /* syntax colors */ --syntax-attr-color: #219; @@ -161,9 +168,14 @@ --syntax-title-color: #0000c0; --syntax-type-color: #2a66d9; --syntax-variable-color: var(--primary-text-color); + /* misc */ --border-radius: 4px; --reply-overlay-z-index: 1000; + + /* paper and iron component overrides */ + --iron-overlay-backdrop-background-color: black; + --iron-overlay-backdrop-opacity: 0.32; --iron-overlay-backdrop: { transition: none; };
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.html b/polygerrit-ui/app/styles/themes/dark-theme.html index 957cc25..cfc1f62 100644 --- a/polygerrit-ui/app/styles/themes/dark-theme.html +++ b/polygerrit-ui/app/styles/themes/dark-theme.html
@@ -30,39 +30,34 @@ --primary-text-color: #e8eaed; --link-color: #8ab4f8; --comment-text-color: var(--primary-text-color); - --deemphasized-text-color: #9e9e9e; + --deemphasized-text-color: #9aa0a6; --default-button-text-color: #8ab4f8; --error-text-color: red; --primary-button-text-color: var(--primary-text-color); /* Used on text color for change list doesn't need user's attention. */ --reviewed-text-color: #dadce0; - --secondary-button-text-color: var(--deemphasized-text-color); --tooltip-text-color: white; --vote-text-color-recommended: #388e3c; --vote-text-color-disliked: #d32f2f; /* background colors */ + /* primary background colors */ + --background-color-primary: #202124; + --background-color-secondary: #2f3034; + --background-color-tertiary: #3b3d3f; + /* directly derived from primary background colors */ + /* empty, because inheriting from app-theme is just fine + /* unique background colors */ --assignee-highlight-color: #3a361c; - --chip-background-color: #131416; --comment-background-color: #0b162b; - --default-button-background-color: #3c4043; - --dialog-background-color: #131416; - --dropdown-background-color: #131416; + --robot-comment-background-color: rgba(232, 234, 237, 0.08); --edit-mode-background-color: #5c0a36; --emphasis-color: #383f4a; - --expanded-background-color: #26282b; --hover-background-color: rgba(161, 194, 250, 0.2); --primary-button-background-color: var(--link-color); - --secondary-button-background-color: var(--primary-text-color); - --select-background-color: #3c4043; --selection-background-color: rgba(161, 194, 250, 0.1); - --shell-command-background-color: #5f5f5f; - --shell-command-decoration-background-color: #999; - --table-header-background-color: #131416; - --table-subheader-background-color: rgba(158, 158, 158, 0.24); --tooltip-background-color: #111; --unresolved-comment-background-color: #385a9a; - --view-background-color: #131416; --vote-color-approved: #7fb66b; --vote-color-disliked: #bf6874; --vote-color-neutral: #597280; @@ -78,9 +73,9 @@ /* spacing */ /* header and footer */ - --footer-background-color: #131416; + --footer-background-color: var(--background-color-tertiary); --footer-border-top: 1px solid var(--border-color); - --header-background-color: #3c4043; + --header-background-color: var(--background-color-tertiary); --header-border-bottom: 1px solid var(--border-color); --header-padding: 0 var(--spacing-l); --header-text-color: var(--primary-text-color); @@ -91,7 +86,7 @@ --dark-rebased-remove-highlight-color: rgba(255, 139, 6, 0.15); --dark-remove-highlight-color: #62110f; --diff-blank-background-color: #212121; - --diff-context-control-background-color: #131416; + --diff-context-control-background-color: #333311; --diff-context-control-border-color: var(--border-color); --diff-context-control-color: var(--deemphasized-text-color); --diff-highlight-range-color: rgba(0, 100, 200, 0.5); @@ -103,6 +98,8 @@ --light-rebased-add-highlight-color: #487165; --light-remove-add-highlight-color: #2f3f2f; --light-remove-highlight-color: #320404; + --coverage-covered: #e0f2f1; + --coverage-not-covered: #ffd1a4; /* syntax colors */ --syntax-attr-color: #80cbbf; @@ -134,6 +131,9 @@ /* misc */ + /* 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/template_test.sh b/polygerrit-ui/app/template_test.sh index 2782b65..d42f23f 100755 --- a/polygerrit-ui/app/template_test.sh +++ b/polygerrit-ui/app/template_test.sh
@@ -1,5 +1,11 @@ #!/bin/bash +# TODO(dmfilippov): Update template_test to support Polymer 2/Polymer 3 or delete it completely +# The following line temporary disable template tests. Existing implementation doesn't compatible +# with Polymer 2 & 3 class-based components. Polymer linter makes some checks regarding +# templates and binding, but not all. +exit 0 + set -ex node_bin=$(which node) && true
diff --git a/polygerrit-ui/app/test/common-test-setup.html b/polygerrit-ui/app/test/common-test-setup.html index c1d8bbd..ec89c87 100644 --- a/polygerrit-ui/app/test/common-test-setup.html +++ b/polygerrit-ui/app/test/common-test-setup.html
@@ -29,8 +29,8 @@ // This will cause the test to fail if there is a data binding // violation. throw new Error( - 'polymer-resin violation: ' + fmt - + JSON.stringify(args)); + 'polymer-resin violation: ' + fmt + + JSON.stringify(args)); } }, safeTypesBridge: Gerrit.SafeTypes.safeTypesBridge,
diff --git a/polygerrit-ui/app/test/common-test-setup.js b/polygerrit-ui/app/test/common-test-setup.js deleted file mode 100644 index 7ceff7e..0000000 --- a/polygerrit-ui/app/test/common-test-setup.js +++ /dev/null
@@ -1,26 +0,0 @@ -/** - * @license - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Helps looking up the proper iron-input element during the Polymer 2 - * transition. Polymer 2 uses the <iron-input> element, while Polymer 1 uses - * the nested <input is="iron-input"> element. - */ -window.ironInput = function(element) { - return Polymer.dom(element).querySelector( - Polymer.Element ? 'iron-input' : 'input[is=iron-input]'); -};
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html index cca7d04..26078cd 100644 --- a/polygerrit-ui/app/test/index.html +++ b/polygerrit-ui/app/test/index.html
@@ -117,6 +117,7 @@ 'diff/gr-diff-view/gr-diff-view_test.html', 'diff/gr-diff/gr-diff-group_test.html', 'diff/gr-diff/gr-diff_test.html', + 'diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html', 'diff/gr-patch-range-select/gr-patch-range-select_test.html', 'diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html', 'diff/gr-selection-action-box/gr-selection-action-box_test.html',
diff --git a/polygerrit-ui/app/types/polymer-behaviors.js b/polygerrit-ui/app/types/polymer-behaviors.js new file mode 100644 index 0000000..18e7ad9 --- /dev/null +++ b/polygerrit-ui/app/types/polymer-behaviors.js
@@ -0,0 +1,52 @@ +/** + * @license + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * For the purposes of template type checking, externs should be added for + * anything set on the window object. Note that sub-properties of these + * declared properties are considered something separate. + * + * This file is only for template type checking, not used in Gerrit code. + */ + +/* eslint-disable no-var */ +/* eslint-disable no-unused-vars */ + +function PolymerMixins() { + // This function must not be called. + // Due to an issue in polymer linter the linter can't + // process correctly some behaviors from Polymer library. + // To workaround this issue, here we define a minimal mixin to allow + // linter process our code correctly. You can add more properties to mixins + // if needed. + + // Important! Use mixins from these file only inside JSDoc comments. + // Do not use it in the real code + + /** + * @polymer + * @mixinFunction + * */ + Polymer.IronFitMixin = base => + class extends base { + static get properties() { + return { + positionTarget: Object, + }; + } + }; +}
diff --git a/polygerrit-ui/app/types/types.js b/polygerrit-ui/app/types/types.js index 8dd65cb..7c42d76 100644 --- a/polygerrit-ui/app/types/types.js +++ b/polygerrit-ui/app/types/types.js
@@ -99,6 +99,7 @@ * - headers is a key-value hash to describe HTTP headers for the request. * - parseResponse states whether the result should be parsed as a JSON * object using getResponseObject. + * * @typedef {{ * method: string, * url: string, @@ -146,6 +147,7 @@ * - cancelCondition is a function that, if provided and returns true, will * cancel the response after it resolves. * - params is a key-value hash to specify get params for the request URL. + * * @typedef {{ * url: string, * errFn: (function(?Response, string=)|null|undefined), @@ -217,6 +219,7 @@ * Note that the implied newline character at the end of each line is included * in the length calculation, and thus it is possible for the edits to span * newlines. + * * @typedef {!Array<number>} */ Gerrit.IntralineInfo; @@ -273,4 +276,27 @@ * makeSuggestionItem: function(Object): Gerrit.GrSuggestionItem, * }} */ -Gerrit.GrSuggestionsProvider; \ No newline at end of file +Gerrit.GrSuggestionsProvider; + +/** + * @typedef {{ + * patch_set: ?number, + * id: ?string, + * path: ?Object, + * side: ?string, + * parent: ?number, + * line: ?Object, + * in_reply_to: ?string, + * message: ?Object, + * updated: ?string, + * author: ?Object, + * tag: ?Object, + * unresolved: ?boolean, + * robot_id: ?string, + * robot_run_id: ?string, + * url: ?string, + * properties: ?Object, + * fix_suggestions: ?Object, + * }} + */ +Gerrit.Comment;
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go index 1a2d299..ba52ce8 100644 --- a/polygerrit-ui/server.go +++ b/polygerrit-ui/server.go
@@ -39,7 +39,7 @@ var ( plugins = flag.String("plugins", "", "comma seperated plugin paths to serve") - port = flag.String("port", ":8081", "Port to serve HTTP requests on") + port = flag.String("port", "localhost:8081", "address to serve HTTP requests on") host = flag.String("host", "gerrit-review.googlesource.com", "Host to proxy requests to") scheme = flag.String("scheme", "https", "URL scheme") cdnPattern = regexp.MustCompile("https://cdn.googlesource.com/polygerrit_ui/[0-9.]*") @@ -84,6 +84,9 @@ log.Println("Local plugins from", "../plugins") } else { http.HandleFunc("/plugins/", handleProxy) + // Serve local plugins from `plugins_` + http.Handle("/plugins_/", http.StripPrefix("/plugins_/", + http.FileServer(http.Dir("../plugins")))) } log.Println("Serving on port", *port) log.Fatal(http.ListenAndServe(*port, &server{}))
diff --git a/prologtests/examples/BUILD b/prologtests/examples/BUILD index f4ebe90..ebf2c68 100644 --- a/prologtests/examples/BUILD +++ b/prologtests/examples/BUILD
@@ -1,15 +1,12 @@ package(default_visibility = ["//visibility:public"]) -DUMMY = ["dummy.sh"] - -# Enable prologtests on newer Java versions again, when this Bazel bug is fixed: -# https://github.com/bazelbuild/bazel/issues/9391 sh_test( name = "test_examples", - srcs = select({ - "//:java11": DUMMY, - "//:java_next": DUMMY, - "//conditions:default": ["run.sh"], - }), - data = glob(["*.pl"]) + ["//:gerrit.war"], + srcs = ["run.sh"], + args = ["$(JAVA)"], + data = glob(["*.pl"]) + [ + "//:gerrit.war", + "@bazel_tools//tools/jdk:current_host_java_runtime", + ], + toolchains = ["@bazel_tools//tools/jdk:current_host_java_runtime"], )
diff --git a/prologtests/examples/run.sh b/prologtests/examples/run.sh index 947c153..b2883eb 100755 --- a/prologtests/examples/run.sh +++ b/prologtests/examples/run.sh
@@ -1,5 +1,14 @@ #!/bin/bash +# TODO(davido): Figure out what to do if running alone and not invoked from bazel +# $1 is equal to the $(JAVABASE)/bin/java make variable +JAVA=$1 + +# Checks whether or not the $1 is starting with a slash: '/' and thus considered to be +# an absolute path. If it is, then it is left as is, if it isn't then "$PWD/ is prepended +# (in sh_test case it is relative and thus the runfiles directory is prepended). +[[ "$JAVA" =~ ^(/|[^/]+$) ]] || JAVA="$PWD/$JAVA" + TESTS="t1 t2 t3" # Note that both t1.pl and t2.pl test code in rules.pl. @@ -36,7 +45,7 @@ # Unit tests do not need to define clauses in packages. # Use one prolog-shell per unit test, to avoid name collision. echo "### Running test ${T}.pl" - echo "[$T]." | java -jar ${GERRIT_WAR} prolog-shell -q -s load.pl + echo "[$T]." | "${JAVA}" -jar ${GERRIT_WAR} prolog-shell -q -s load.pl if [ "x$?" != "x0" ]; then echo "### Test ${T}.pl failed."
diff --git a/tools/BUILD b/tools/BUILD index 5531c3e..f0a4ffa 100644 --- a/tools/BUILD +++ b/tools/BUILD
@@ -53,6 +53,7 @@ "-Xep:ExpectedExceptionChecker:ERROR", "-Xep:Finally:ERROR", "-Xep:FloatingPointLiteralPrecision:ERROR", + "-Xep:FormatStringAnnotation:ERROR", "-Xep:FragmentInjection:ERROR", "-Xep:FragmentNotInstantiable:ERROR", "-Xep:FunctionalInterfaceClash:ERROR",
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl index fa5cbd1..0b4a52a 100644 --- a/tools/bzl/maven_jar.bzl +++ b/tools/bzl/maven_jar.bzl
@@ -146,7 +146,6 @@ parts = ctx.attr.artifact.split(":") - # TODO(davido): Only releases for now, implement handling snapshots jar, url = _maven_release(ctx, parts) binjar = jar + ".jar"
diff --git a/tools/dev-hooks/pre-commit b/tools/dev-hooks/pre-commit new file mode 100755 index 0000000..b77f382 --- /dev/null +++ b/tools/dev-hooks/pre-commit
@@ -0,0 +1,47 @@ +#!/bin/sh +# +# Part of Gerrit Code Review (https://www.gerritcodereview.com/) +# +# Copyright (C) 2019 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# To enable this hook: +# - copy this file or content to ".git/hooks/pre-commit" +# - (optional if you copied this file) make it executable: `chmod +x .git/hooks/pre-commit` + +set -ue + +# gitroot, default to . +gitroot=$(git rev-parse --show-cdup) +gitroot=${gitroot:-.}; + +# eslint +eslint=${gitroot}/node_modules/eslint/bin/eslint.js + +# Run eslint over changed frontend code +CHANGED_UI_FILES=$(git diff --cached --name-only -- '*.js' '*.html' | grep 'polygerrit-ui') && true +if [ "${CHANGED_UI_FILES}" ]; then + if $eslint --fix ${CHANGED_UI_FILES}; then + # Add again in case lint fix modified some files + git add ${CHANGED_UI_FILES} + exit 0 + else + echo "Failed to fix all linter issues."; + exit 1 + fi +else + echo "No UI files changed" + exit 0 +fi \ No newline at end of file
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml index fe2e6ca..4240a9b 100644 --- a/tools/maven/gerrit-acceptance-framework_pom.xml +++ b/tools/maven/gerrit-acceptance-framework_pom.xml
@@ -2,7 +2,7 @@ <modelVersion>4.0.0</modelVersion> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-acceptance-framework</artifactId> - <version>3.1.3-SNAPSHOT</version> + <version>3.2.0-SNAPSHOT</version> <packaging>jar</packaging> <name>Gerrit Code Review - Acceptance Test Framework</name> <description>Framework for Gerrit's acceptance tests</description>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml index 6ffe873..cf2b080 100644 --- a/tools/maven/gerrit-extension-api_pom.xml +++ b/tools/maven/gerrit-extension-api_pom.xml
@@ -2,7 +2,7 @@ <modelVersion>4.0.0</modelVersion> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-extension-api</artifactId> - <version>3.1.3-SNAPSHOT</version> + <version>3.2.0-SNAPSHOT</version> <packaging>jar</packaging> <name>Gerrit Code Review - Extension API</name> <description>API for Gerrit Extensions</description>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml index dd5a725..7d3c4f0 100644 --- a/tools/maven/gerrit-plugin-api_pom.xml +++ b/tools/maven/gerrit-plugin-api_pom.xml
@@ -2,7 +2,7 @@ <modelVersion>4.0.0</modelVersion> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-plugin-api</artifactId> - <version>3.1.3-SNAPSHOT</version> + <version>3.2.0-SNAPSHOT</version> <packaging>jar</packaging> <name>Gerrit Code Review - Plugin API</name> <description>API for Gerrit Plugins</description>
diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml index cc01869..9478283 100644 --- a/tools/maven/gerrit-war_pom.xml +++ b/tools/maven/gerrit-war_pom.xml
@@ -2,7 +2,7 @@ <modelVersion>4.0.0</modelVersion> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-war</artifactId> - <version>3.1.3-SNAPSHOT</version> + <version>3.2.0-SNAPSHOT</version> <packaging>war</packaging> <name>Gerrit Code Review - WAR</name> <description>Gerrit WAR</description>
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl index 1132d4a..fa8008c 100644 --- a/tools/nongoogle.bzl +++ b/tools/nongoogle.bzl
@@ -23,8 +23,8 @@ maven_jar( name = "dropwizard-core", - artifact = "io.dropwizard.metrics:metrics-core:4.0.7", - sha1 = "673899f605f52ca35836673ccfee97154a496a61", + artifact = "io.dropwizard.metrics:metrics-core:4.1.2", + sha1 = "bba231bbf3024c19e75622ec168821cbbd4261a4", ) SSHD_VERS = "2.3.0"
diff --git a/tools/polygerrit-updater/.gitignore b/tools/polygerrit-updater/.gitignore new file mode 100644 index 0000000..8619a37 --- /dev/null +++ b/tools/polygerrit-updater/.gitignore
@@ -0,0 +1,3 @@ +/.idea/ +/node_modules/ +/js/ \ No newline at end of file
diff --git a/tools/polygerrit-updater/package-lock.json b/tools/polygerrit-updater/package-lock.json new file mode 100644 index 0000000..9256997 --- /dev/null +++ b/tools/polygerrit-updater/package-lock.json
@@ -0,0 +1,18 @@ +{ + "name": "polygerrit-updater", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/node": { + "version": "12.7.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.12.tgz", + "integrity": "sha512-KPYGmfD0/b1eXurQ59fXD1GBzhSQfz6/lKBxkaHX9dKTzjXbK68Zt7yGUxUsCS1jeTy/8aL+d9JEr+S54mpkWQ==" + }, + "typescript": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz", + "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==" + } + } +}
diff --git a/tools/polygerrit-updater/package.json b/tools/polygerrit-updater/package.json new file mode 100644 index 0000000..3609dad --- /dev/null +++ b/tools/polygerrit-updater/package.json
@@ -0,0 +1,15 @@ +{ + "name": "polygerrit-updater", + "version": "1.0.0", + "description": "Polygerrit source code updater", + "scripts": { + "compile": "tsc", + "convert": "npm run compile && node js/src/index.js" + }, + "author": "", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^12.7.12", + "typescript": "^3.6.4" + } +}
diff --git a/tools/polygerrit-updater/readme.txt b/tools/polygerrit-updater/readme.txt new file mode 100644 index 0000000..2b2cea8 --- /dev/null +++ b/tools/polygerrit-updater/readme.txt
@@ -0,0 +1,56 @@ +This folder contains tool to update Polymer components to class based components. +This is a temporary tools, it will be removed in a few weeks. + +How to use this tool: initial steps +1) Important - Commit and push all your changes. Otherwise, you can loose you work. + +2) Ensure, that tools/polygerrit-updater is your current directory + +3) Run +npm install + +4) If you want to convert the whole project, run +npm run convert -- --i \ + --root ../../polygerrit-ui --src app/elements --r \ + --exclude app/elements/core/gr-reporting/gr-reporting.js \ + app/elements/diff/gr-comment-api/gr-comment-api-mock.js \ + app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js + +You can convert only specific files (can be useful if you want to convert some files in your change) +npm run convert -- --i \ + --root ../../polygerrit-ui + --src app/elements/file1.js \ + app/elements/folder/file2.js + +4) Search for the following string in all .js files: +//This file has the following problems with comments: + +If you find such string in a .js file - you must manually fix comments in this file. +(It is expected that you shouldn't have such problems) + +5) Go to the gerrit root folder and run +npm run eslintfix + +(If you are doing it for the first time, run the following command before in gerrit root folder: +npm run install) + +Fix error after eslintfix (if exists) + +6) If you are doing conversion for the whole project, make the followin changes: + +a) Add +<link rel="import" href="../../../types/polymer-behaviors.js"> +to +polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html + +b) Update polymer.json with the following rules: + "lint": { + "rules": ["polymer-2"], + "ignoreWarnings": ["deprecated-dom-call"] + } + + + +5) Commit changed files. + +6) You can update excluded files later.
diff --git a/tools/polygerrit-updater/src/funcToClassConversion/funcToClassBasedElementConverter.ts b/tools/polygerrit-updater/src/funcToClassConversion/funcToClassBasedElementConverter.ts new file mode 100644 index 0000000..b92a6e9 --- /dev/null +++ b/tools/polygerrit-updater/src/funcToClassConversion/funcToClassBasedElementConverter.ts
@@ -0,0 +1,131 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {LegacyLifecycleMethodsArray, LegacyPolymerComponent} from './polymerComponentParser'; +import {LifecycleMethodsBuilder} from './lifecycleMethodsBuilder'; +import {ClassBasedPolymerElement, PolymerElementBuilder} from './polymerElementBuilder'; +import * as codeUtils from '../utils/codeUtils'; +import * as ts from 'typescript'; + +export class PolymerFuncToClassBasedConverter { + public static convert(component: LegacyPolymerComponent): ClassBasedPolymerElement { + const legacySettings = component.componentSettings; + const reservedDeclarations = legacySettings.reservedDeclarations; + + if(!reservedDeclarations.is) { + throw new Error("Legacy component doesn't have 'is' property"); + } + const className = this.generateClassNameFromTagName(reservedDeclarations.is.data); + const updater = new PolymerElementBuilder(component, className); + updater.addIsAccessor(reservedDeclarations.is.data); + + if(reservedDeclarations.properties) { + updater.addPolymerPropertiesAccessor(reservedDeclarations.properties); + } + + updater.addMixin("Polymer.Element"); + updater.addMixin("Polymer.LegacyElementMixin"); + updater.addMixin("Polymer.GestureEventListeners"); + + if(reservedDeclarations._legacyUndefinedCheck) { + updater.addMixin("Polymer.LegacyDataMixin"); + } + + if(reservedDeclarations.behaviors) { + updater.addMixin("Polymer.mixinBehaviors", [reservedDeclarations.behaviors.data]); + const mixinNames = this.getMixinNamesFromBehaviors(reservedDeclarations.behaviors.data); + const jsDocLines = mixinNames.map(mixinName => { + return `@appliesMixin ${mixinName}`; + }); + updater.addClassJSDocComments(jsDocLines); + } + + if(reservedDeclarations.observers) { + updater.addPolymerPropertiesObservers(reservedDeclarations.observers.data); + } + + if(reservedDeclarations.keyBindings) { + updater.addKeyBindings(reservedDeclarations.keyBindings.data); + } + + + const lifecycleBuilder = new LifecycleMethodsBuilder(); + if (reservedDeclarations.listeners) { + lifecycleBuilder.addListeners(reservedDeclarations.listeners.data, legacySettings.ordinaryMethods); + } + + if (reservedDeclarations.hostAttributes) { + lifecycleBuilder.addHostAttributes(reservedDeclarations.hostAttributes.data); + } + + for(const name of LegacyLifecycleMethodsArray) { + const existingMethod = legacySettings.lifecycleMethods.get(name); + if(existingMethod) { + lifecycleBuilder.addLegacyLifecycleMethod(name, existingMethod) + } + } + + const newLifecycleMethods = lifecycleBuilder.buildNewMethods(); + updater.addLifecycleMethods(newLifecycleMethods); + + + updater.addOrdinaryMethods(legacySettings.ordinaryMethods); + updater.addOrdinaryGetAccessors(legacySettings.ordinaryGetAccessors); + updater.addOrdinaryShorthandProperties(legacySettings.ordinaryShorthandProperties); + updater.addOrdinaryPropertyAssignments(legacySettings.ordinaryPropertyAssignments); + + return updater.build(); + } + + private static generateClassNameFromTagName(tagName: string) { + let result = ""; + let nextUppercase = true; + for(const ch of tagName) { + if (ch === '-') { + nextUppercase = true; + continue; + } + result += nextUppercase ? ch.toUpperCase() : ch; + nextUppercase = false; + } + return result; + } + + private static getMixinNamesFromBehaviors(behaviors: ts.ArrayLiteralExpression): string[] { + return behaviors.elements.map((expression) => { + const propertyAccessExpression = codeUtils.assertNodeKind(expression, ts.SyntaxKind.PropertyAccessExpression) as ts.PropertyAccessExpression; + const namespaceName = codeUtils.assertNodeKind(propertyAccessExpression.expression, ts.SyntaxKind.Identifier) as ts.Identifier; + const behaviorName = propertyAccessExpression.name; + if(namespaceName.text === 'Gerrit') { + let behaviorNameText = behaviorName.text; + const suffix = 'Behavior'; + if(behaviorNameText.endsWith(suffix)) { + behaviorNameText = + behaviorNameText.substr(0, behaviorNameText.length - suffix.length); + } + const mixinName = behaviorNameText + 'Mixin'; + return `${namespaceName.text}.${mixinName}` + } else if(namespaceName.text === 'Polymer') { + let behaviorNameText = behaviorName.text; + if(behaviorNameText === "IronFitBehavior") { + return "Polymer.IronFitMixin"; + } else if(behaviorNameText === "IronOverlayBehavior") { + return ""; + } + throw new Error(`Unsupported behavior: ${propertyAccessExpression.getText()}`); + } + throw new Error(`Unsupported behavior name ${expression.getFullText()}`) + }).filter(name => name.length > 0); + } +}
diff --git a/tools/polygerrit-updater/src/funcToClassConversion/legacyPolymerFuncReplacer.ts b/tools/polygerrit-updater/src/funcToClassConversion/legacyPolymerFuncReplacer.ts new file mode 100644 index 0000000..57b7b8d --- /dev/null +++ b/tools/polygerrit-updater/src/funcToClassConversion/legacyPolymerFuncReplacer.ts
@@ -0,0 +1,74 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as ts from 'typescript'; +import * as codeUtils from '../utils/codeUtils' +import {LegacyPolymerComponent} from './polymerComponentParser'; +import {ClassBasedPolymerElement} from './polymerElementBuilder'; + +export class LegacyPolymerFuncReplaceResult { + public constructor( + private readonly transformationResult: ts.TransformationResult<ts.SourceFile>, + public readonly leadingComments: string[]) { + } + public get file(): ts.SourceFile { + return this.transformationResult.transformed[0]; + } + public dispose() { + this.transformationResult.dispose(); + } + +} + +export class LegacyPolymerFuncReplacer { + private readonly callStatement: ts.ExpressionStatement; + private readonly parentBlock: ts.Block; + private readonly callStatementIndexInBlock: number; + public constructor(private readonly legacyComponent: LegacyPolymerComponent) { + this.callStatement = codeUtils.assertNodeKind(legacyComponent.polymerFuncCallExpr.parent, ts.SyntaxKind.ExpressionStatement); + this.parentBlock = codeUtils.assertNodeKind(this.callStatement.parent, ts.SyntaxKind.Block); + this.callStatementIndexInBlock = this.parentBlock.statements.indexOf(this.callStatement); + if(this.callStatementIndexInBlock < 0) { + throw new Error("Internal error! Couldn't find statement in its own parent"); + } + } + public replace(classBasedElement: ClassBasedPolymerElement): LegacyPolymerFuncReplaceResult { + const classDeclarationWithComments = this.appendLeadingCommentToClassDeclaration(classBasedElement.classDeclaration); + return new LegacyPolymerFuncReplaceResult( + this.replaceLegacyPolymerFunction(classDeclarationWithComments.classDeclarationWithCommentsPlaceholder, classBasedElement.componentRegistration), + classDeclarationWithComments.leadingComments); + } + private appendLeadingCommentToClassDeclaration(classDeclaration: ts.ClassDeclaration): {classDeclarationWithCommentsPlaceholder: ts.ClassDeclaration, leadingComments: string[]} { + const text = this.callStatement.getFullText(); + let classDeclarationWithCommentsPlaceholder = classDeclaration; + const leadingComments: string[] = []; + ts.forEachLeadingCommentRange(text, 0, (pos, end, kind, hasTrailingNewLine) => { + classDeclarationWithCommentsPlaceholder = codeUtils.addReplacableCommentBeforeNode(classDeclarationWithCommentsPlaceholder, String(leadingComments.length)); + leadingComments.push(text.substring(pos, end)); + }); + return { + classDeclarationWithCommentsPlaceholder: classDeclarationWithCommentsPlaceholder, + leadingComments: leadingComments + } + } + private replaceLegacyPolymerFunction(classDeclaration: ts.ClassDeclaration, componentRegistration: ts.ExpressionStatement): ts.TransformationResult<ts.SourceFile> { + const newStatements = Array.from(this.parentBlock.statements); + newStatements.splice(this.callStatementIndexInBlock, 1, classDeclaration, componentRegistration); + + const updatedBlock = ts.getMutableClone(this.parentBlock); + updatedBlock.statements = ts.createNodeArray(newStatements); + return codeUtils.replaceNode(this.legacyComponent.parsedFile, this.parentBlock, updatedBlock); + + } +} \ No newline at end of file
diff --git a/tools/polygerrit-updater/src/funcToClassConversion/lifecycleMethodsBuilder.ts b/tools/polygerrit-updater/src/funcToClassConversion/lifecycleMethodsBuilder.ts new file mode 100644 index 0000000..e9e13f5 --- /dev/null +++ b/tools/polygerrit-updater/src/funcToClassConversion/lifecycleMethodsBuilder.ts
@@ -0,0 +1,140 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as ts from 'typescript'; +import * as codeUtils from '../utils/codeUtils'; +import {LegacyLifecycleMethodName, OrdinaryMethods} from './polymerComponentParser'; + +interface LegacyLifecycleMethodContent { + codeAtMethodStart: ts.Statement[]; + existingMethod?: ts.MethodDeclaration; + codeAtMethodEnd: ts.Statement[]; +} + +export interface LifecycleMethod { + originalPos: number;//-1 - no original method exists + method: ts.MethodDeclaration; + name: LegacyLifecycleMethodName; +} + +export class LifecycleMethodsBuilder { + private readonly methods: Map<LegacyLifecycleMethodName, LegacyLifecycleMethodContent> = new Map(); + + private getMethodContent(name: LegacyLifecycleMethodName): LegacyLifecycleMethodContent { + if(!this.methods.has(name)) { + this.methods.set(name, { + codeAtMethodStart: [], + codeAtMethodEnd: [] + }); + } + return this.methods.get(name)!; + } + + public addListeners(legacyListeners: ts.ObjectLiteralExpression, legacyOrdinaryMethods: OrdinaryMethods) { + for(const listener of legacyListeners.properties) { + const propertyAssignment = codeUtils.assertNodeKind(listener, ts.SyntaxKind.PropertyAssignment) as ts.PropertyAssignment; + if(!propertyAssignment.name) { + throw new Error("Listener must have event name"); + } + let eventNameLiteral: ts.StringLiteral; + let commentsToRestore: string[] = []; + if(propertyAssignment.name.kind === ts.SyntaxKind.StringLiteral) { + //We don't loose comment in this case, because we keep literal as is + eventNameLiteral = propertyAssignment.name; + } else if(propertyAssignment.name.kind === ts.SyntaxKind.Identifier) { + eventNameLiteral = ts.createStringLiteral(propertyAssignment.name.text); + commentsToRestore = codeUtils.getLeadingComments(propertyAssignment); + } else { + throw new Error(`Unsupported type ${ts.SyntaxKind[propertyAssignment.name.kind]}`); + } + + const handlerLiteral = codeUtils.assertNodeKind(propertyAssignment.initializer, ts.SyntaxKind.StringLiteral) as ts.StringLiteral; + const handlerImpl = legacyOrdinaryMethods.get(handlerLiteral.text); + if(!handlerImpl) { + throw new Error(`Can't find event handler '${handlerLiteral.text}'`); + } + const eventHandlerAccess = ts.createPropertyAccess(ts.createThis(), handlerLiteral.text); + //ts.forEachChild(handler) + const args: ts.Identifier[] = handlerImpl.parameters.map((arg) => codeUtils.assertNodeKind(arg.name, ts.SyntaxKind.Identifier)); + const eventHandlerCall = ts.createCall(eventHandlerAccess, [], args); + let arrowFunc = ts.createArrowFunction([], [], handlerImpl.parameters, undefined, undefined, eventHandlerCall); + arrowFunc = codeUtils.addNewLineBeforeNode(arrowFunc); + + const methodContent = this.getMethodContent("created"); + //See https://polymer-library.polymer-project.org/3.0/docs/devguide/gesture-events for a list of events + if(["down", "up", "tap", "track"].indexOf(eventNameLiteral.text) >= 0) { + const methodCall = ts.createCall(codeUtils.createNameExpression("Polymer.Gestures.addListener"), [], [ts.createThis(), eventNameLiteral, arrowFunc]); + methodContent.codeAtMethodEnd.push(ts.createExpressionStatement(methodCall)); + } + else { + let methodCall = ts.createCall(ts.createPropertyAccess(ts.createThis(), "addEventListener"), [], [eventNameLiteral, arrowFunc]); + methodCall = codeUtils.restoreLeadingComments(methodCall, commentsToRestore); + methodContent.codeAtMethodEnd.push(ts.createExpressionStatement(methodCall)); + } + } + } + + public addHostAttributes(legacyHostAttributes: ts.ObjectLiteralExpression) { + for(const listener of legacyHostAttributes.properties) { + const propertyAssignment = codeUtils.assertNodeKind(listener, ts.SyntaxKind.PropertyAssignment) as ts.PropertyAssignment; + if(!propertyAssignment.name) { + throw new Error("Listener must have event name"); + } + let attributeNameLiteral: ts.StringLiteral; + if(propertyAssignment.name.kind === ts.SyntaxKind.StringLiteral) { + attributeNameLiteral = propertyAssignment.name; + } else if(propertyAssignment.name.kind === ts.SyntaxKind.Identifier) { + attributeNameLiteral = ts.createStringLiteral(propertyAssignment.name.text); + } else { + throw new Error(`Unsupported type ${ts.SyntaxKind[propertyAssignment.name.kind]}`); + } + let attributeValueLiteral: ts.StringLiteral | ts.NumericLiteral; + if(propertyAssignment.initializer.kind === ts.SyntaxKind.StringLiteral) { + attributeValueLiteral = propertyAssignment.initializer as ts.StringLiteral; + } else if(propertyAssignment.initializer.kind === ts.SyntaxKind.NumericLiteral) { + attributeValueLiteral = propertyAssignment.initializer as ts.NumericLiteral; + } else { + throw new Error(`Unsupported type ${ts.SyntaxKind[propertyAssignment.initializer.kind]}`); + } + const methodCall = ts.createCall(ts.createPropertyAccess(ts.createThis(), "_ensureAttribute"), [], [attributeNameLiteral, attributeValueLiteral]); + this.getMethodContent("ready").codeAtMethodEnd.push(ts.createExpressionStatement(methodCall)); + } + } + + public addLegacyLifecycleMethod(name: LegacyLifecycleMethodName, method: ts.MethodDeclaration) { + const content = this.getMethodContent(name); + if(content.existingMethod) { + throw new Error(`Legacy lifecycle method ${name} already added`); + } + content.existingMethod = method; + } + + public buildNewMethods(): LifecycleMethod[] { + const result = []; + for(const [name, content] of this.methods) { + const newMethod = this.createLifecycleMethod(name, content.existingMethod, content.codeAtMethodStart, content.codeAtMethodEnd); + if(!newMethod) continue; + result.push({ + name, + originalPos: content.existingMethod ? content.existingMethod.pos : -1, + method: newMethod + }) + } + return result; + } + + private createLifecycleMethod(name: string, methodDecl: ts.MethodDeclaration | undefined, codeAtStart: ts.Statement[], codeAtEnd: ts.Statement[]): ts.MethodDeclaration | undefined { + return codeUtils.createMethod(name, methodDecl, codeAtStart, codeAtEnd, true); + } +} \ No newline at end of file
diff --git a/tools/polygerrit-updater/src/funcToClassConversion/polymerComponentParser.ts b/tools/polygerrit-updater/src/funcToClassConversion/polymerComponentParser.ts new file mode 100644 index 0000000..6006608 --- /dev/null +++ b/tools/polygerrit-updater/src/funcToClassConversion/polymerComponentParser.ts
@@ -0,0 +1,301 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as ts from "typescript"; +import * as fs from "fs"; +import * as path from "path"; +import { unexpectedValue } from "../utils/unexpectedValue"; +import * as codeUtils from "../utils/codeUtils"; +import {CommentsParser} from '../utils/commentsParser'; + +export class LegacyPolymerComponentParser { + public constructor(private readonly rootDir: string, private readonly htmlFiles: Set<string>) { + } + public async parse(jsFile: string): Promise<ParsedPolymerComponent | null> { + const sourceFile: ts.SourceFile = this.parseJsFile(jsFile); + const legacyComponent = this.tryParseLegacyComponent(sourceFile); + if (legacyComponent) { + return legacyComponent; + } + return null; + } + private parseJsFile(jsFile: string): ts.SourceFile { + return ts.createSourceFile(jsFile, fs.readFileSync(path.resolve(this.rootDir, jsFile)).toString(), ts.ScriptTarget.ES2015, true); + } + + private tryParseLegacyComponent(sourceFile: ts.SourceFile): ParsedPolymerComponent | null { + const polymerFuncCalls: ts.CallExpression[] = []; + + function addPolymerFuncCall(node: ts.Node) { + if(node.kind === ts.SyntaxKind.CallExpression) { + const callExpression: ts.CallExpression = node as ts.CallExpression; + if(callExpression.expression.kind === ts.SyntaxKind.Identifier) { + const identifier = callExpression.expression as ts.Identifier; + if(identifier.text === "Polymer") { + polymerFuncCalls.push(callExpression); + } + } + } + ts.forEachChild(node, addPolymerFuncCall); + } + + addPolymerFuncCall(sourceFile); + + + if (polymerFuncCalls.length === 0) { + return null; + } + if (polymerFuncCalls.length > 1) { + throw new Error("Each .js file must contain only one Polymer component"); + } + const parsedPath = path.parse(sourceFile.fileName); + const htmlFullPath = path.format({ + dir: parsedPath.dir, + name: parsedPath.name, + ext: ".html" + }); + if (!this.htmlFiles.has(htmlFullPath)) { + throw new Error("Legacy .js component dosn't have associated .html file"); + } + + const polymerFuncCall = polymerFuncCalls[0]; + if(polymerFuncCall.arguments.length !== 1) { + throw new Error("The Polymer function must be called with exactly one parameter"); + } + const argument = polymerFuncCall.arguments[0]; + if(argument.kind !== ts.SyntaxKind.ObjectLiteralExpression) { + throw new Error("The parameter for Polymer function must be ObjectLiteralExpression (i.e. '{...}')"); + } + const infoArg = argument as ts.ObjectLiteralExpression; + + return { + jsFile: sourceFile.fileName, + htmlFile: htmlFullPath, + parsedFile: sourceFile, + polymerFuncCallExpr: polymerFuncCalls[0], + componentSettings: this.parseLegacyComponentSettings(infoArg), + }; + } + + private parseLegacyComponentSettings(info: ts.ObjectLiteralExpression): LegacyPolymerComponentSettings { + const props: Map<string, ts.ObjectLiteralElementLike> = new Map(); + for(const property of info.properties) { + const name = property.name; + if (name === undefined) { + throw new Error("Property name is not defined"); + } + switch(name.kind) { + case ts.SyntaxKind.Identifier: + case ts.SyntaxKind.StringLiteral: + if (props.has(name.text)) { + throw new Error(`Property ${name.text} appears more than once`); + } + props.set(name.text, property); + break; + case ts.SyntaxKind.ComputedPropertyName: + continue; + default: + unexpectedValue(ts.SyntaxKind[name.kind]); + } + } + + if(props.has("_noAccessors")) { + throw new Error("_noAccessors is not supported"); + } + + const legacyLifecycleMethods: LegacyLifecycleMethods = new Map(); + for(const name of LegacyLifecycleMethodsArray) { + const methodDecl = this.getLegacyMethodDeclaration(props, name); + if(methodDecl) { + legacyLifecycleMethods.set(name, methodDecl); + } + } + + const ordinaryMethods: OrdinaryMethods = new Map(); + const ordinaryShorthandProperties: OrdinaryShorthandProperties = new Map(); + const ordinaryGetAccessors: OrdinaryGetAccessors = new Map(); + const ordinaryPropertyAssignments: OrdinaryPropertyAssignments = new Map(); + for(const [name, val] of props) { + if(RESERVED_NAMES.hasOwnProperty(name)) continue; + switch(val.kind) { + case ts.SyntaxKind.MethodDeclaration: + ordinaryMethods.set(name, val as ts.MethodDeclaration); + break; + case ts.SyntaxKind.ShorthandPropertyAssignment: + ordinaryShorthandProperties.set(name, val as ts.ShorthandPropertyAssignment); + break; + case ts.SyntaxKind.GetAccessor: + ordinaryGetAccessors.set(name, val as ts.GetAccessorDeclaration); + break; + case ts.SyntaxKind.PropertyAssignment: + ordinaryPropertyAssignments.set(name, val as ts.PropertyAssignment); + break; + default: + throw new Error(`Unsupported element kind: ${ts.SyntaxKind[val.kind]}`); + } + //ordinaryMethods.set(name, tsUtils.assertNodeKind(val, ts.SyntaxKind.MethodDeclaration) as ts.MethodDeclaration); + } + + const eventsComments: string[] = this.getEventsComments(info.getFullText()); + + return { + reservedDeclarations: { + is: this.getStringLiteralValueWithComments(this.getLegacyPropertyInitializer(props, "is")), + _legacyUndefinedCheck: this.getBooleanLiteralValueWithComments(this.getLegacyPropertyInitializer(props, "_legacyUndefinedCheck")), + properties: this.getObjectLiteralExpressionWithComments(this.getLegacyPropertyInitializer(props, "properties")), + behaviors: this.getArrayLiteralExpressionWithComments(this.getLegacyPropertyInitializer(props, "behaviors")), + observers: this.getArrayLiteralExpressionWithComments(this.getLegacyPropertyInitializer(props, "observers")), + listeners: this.getObjectLiteralExpressionWithComments(this.getLegacyPropertyInitializer(props, "listeners")), + hostAttributes: this.getObjectLiteralExpressionWithComments(this.getLegacyPropertyInitializer(props, "hostAttributes")), + keyBindings: this.getObjectLiteralExpressionWithComments(this.getLegacyPropertyInitializer(props, "keyBindings")), + }, + eventsComments: eventsComments, + lifecycleMethods: legacyLifecycleMethods, + ordinaryMethods: ordinaryMethods, + ordinaryShorthandProperties: ordinaryShorthandProperties, + ordinaryGetAccessors: ordinaryGetAccessors, + ordinaryPropertyAssignments: ordinaryPropertyAssignments, + }; + } + + private convertLegacyProeprtyInitializer<T>(initializer: LegacyPropertyInitializer | undefined, converter: (exp: ts.Expression) => T): DataWithComments<T> | undefined { + if(!initializer) { + return undefined; + } + return { + data: converter(initializer.data), + leadingComments: initializer.leadingComments, + } + } + + private getObjectLiteralExpressionWithComments(initializer: LegacyPropertyInitializer | undefined): DataWithComments<ts.ObjectLiteralExpression> | undefined { + return this.convertLegacyProeprtyInitializer(initializer, + expr => codeUtils.getObjectLiteralExpression(expr)); + } + + private getStringLiteralValueWithComments(initializer: LegacyPropertyInitializer | undefined): DataWithComments<string> | undefined { + return this.convertLegacyProeprtyInitializer(initializer, + expr => codeUtils.getStringLiteralValue(expr)); + } + + private getBooleanLiteralValueWithComments(initializer: LegacyPropertyInitializer | undefined): DataWithComments<boolean> | undefined { + return this.convertLegacyProeprtyInitializer(initializer, + expr => codeUtils.getBooleanLiteralValue(expr)); + } + + + private getArrayLiteralExpressionWithComments(initializer: LegacyPropertyInitializer | undefined): DataWithComments<ts.ArrayLiteralExpression> | undefined { + return this.convertLegacyProeprtyInitializer(initializer, + expr => codeUtils.getArrayLiteralExpression(expr)); + } + + private getLegacyPropertyInitializer(props: Map<String, ts.ObjectLiteralElementLike>, propName: string): LegacyPropertyInitializer | undefined { + const property = props.get(propName); + if (!property) { + return undefined; + } + const assignment = codeUtils.getPropertyAssignment(property); + if (!assignment) { + return undefined; + } + const comments: string[] = codeUtils.getLeadingComments(property) + .filter(c => !this.isEventComment(c)); + return { + data: assignment.initializer, + leadingComments: comments, + }; + } + + private isEventComment(comment: string): boolean { + return comment.indexOf('@event') >= 0; + } + + private getEventsComments(polymerComponentSource: string): string[] { + return CommentsParser.collectAllComments(polymerComponentSource) + .filter(c => this.isEventComment(c)); + } + + private getLegacyMethodDeclaration(props: Map<String, ts.ObjectLiteralElementLike>, propName: string): ts.MethodDeclaration | undefined { + const property = props.get(propName); + if (!property) { + return undefined; + } + return codeUtils.assertNodeKind(property, ts.SyntaxKind.MethodDeclaration) as ts.MethodDeclaration; + } + +} + +export type ParsedPolymerComponent = LegacyPolymerComponent; + +export interface LegacyPolymerComponent { + jsFile: string; + htmlFile: string; + parsedFile: ts.SourceFile; + polymerFuncCallExpr: ts.CallExpression; + componentSettings: LegacyPolymerComponentSettings; +} + +export interface LegacyReservedDeclarations { + is?: DataWithComments<string>; + _legacyUndefinedCheck?: DataWithComments<boolean>; + properties?: DataWithComments<ts.ObjectLiteralExpression>; + behaviors?: DataWithComments<ts.ArrayLiteralExpression>, + observers? :DataWithComments<ts.ArrayLiteralExpression>, + listeners? :DataWithComments<ts.ObjectLiteralExpression>, + hostAttributes?: DataWithComments<ts.ObjectLiteralExpression>, + keyBindings?: DataWithComments<ts.ObjectLiteralExpression>, +} + +export const LegacyLifecycleMethodsArray = <const>["beforeRegister", "registered", "created", "ready", "attached" , "detached", "attributeChanged"]; +export type LegacyLifecycleMethodName = typeof LegacyLifecycleMethodsArray[number]; +export type LegacyLifecycleMethods = Map<LegacyLifecycleMethodName, ts.MethodDeclaration>; +export type OrdinaryMethods = Map<string, ts.MethodDeclaration>; +export type OrdinaryShorthandProperties = Map<string, ts.ShorthandPropertyAssignment>; +export type OrdinaryGetAccessors = Map<string, ts.GetAccessorDeclaration>; +export type OrdinaryPropertyAssignments = Map<string, ts.PropertyAssignment>; +export type ReservedName = LegacyLifecycleMethodName | keyof LegacyReservedDeclarations; +export const RESERVED_NAMES: {[x in ReservedName]: boolean} = { + attached: true, + detached: true, + ready: true, + created: true, + beforeRegister: true, + registered: true, + attributeChanged: true, + is: true, + _legacyUndefinedCheck: true, + properties: true, + behaviors: true, + observers: true, + listeners: true, + hostAttributes: true, + keyBindings: true, +}; + +export interface LegacyPolymerComponentSettings { + reservedDeclarations: LegacyReservedDeclarations; + lifecycleMethods: LegacyLifecycleMethods, + ordinaryMethods: OrdinaryMethods, + ordinaryShorthandProperties: OrdinaryShorthandProperties, + ordinaryGetAccessors: OrdinaryGetAccessors, + ordinaryPropertyAssignments: OrdinaryPropertyAssignments, + eventsComments: string[]; +} + +export interface DataWithComments<T> { + data: T; + leadingComments: string[]; +} + +type LegacyPropertyInitializer = DataWithComments<ts.Expression>; \ No newline at end of file
diff --git a/tools/polygerrit-updater/src/funcToClassConversion/polymerElementBuilder.ts b/tools/polygerrit-updater/src/funcToClassConversion/polymerElementBuilder.ts new file mode 100644 index 0000000..d6e113c --- /dev/null +++ b/tools/polygerrit-updater/src/funcToClassConversion/polymerElementBuilder.ts
@@ -0,0 +1,142 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {DataWithComments, LegacyPolymerComponent, LegacyReservedDeclarations, OrdinaryGetAccessors, OrdinaryMethods, OrdinaryPropertyAssignments, OrdinaryShorthandProperties} from './polymerComponentParser'; +import * as ts from 'typescript'; +import * as codeUtils from '../utils/codeUtils'; +import {LifecycleMethod} from './lifecycleMethodsBuilder'; +import {PolymerClassBuilder} from '../utils/polymerClassBuilder'; +import {SyntaxKind} from 'typescript'; + +export interface ClassBasedPolymerElement { + classDeclaration: ts.ClassDeclaration; + componentRegistration: ts.ExpressionStatement; + eventsComments: string[]; + generatedComments: string[]; +} + +export class PolymerElementBuilder { + private readonly reservedDeclarations: LegacyReservedDeclarations; + private readonly classBuilder: PolymerClassBuilder; + private mixins: ts.ExpressionWithTypeArguments | null; + + public constructor(private readonly legacyComponent: LegacyPolymerComponent, className: string) { + this.reservedDeclarations = legacyComponent.componentSettings.reservedDeclarations; + this.classBuilder = new PolymerClassBuilder(className); + this.mixins = null; + } + + public addIsAccessor(tagName: string) { + this.classBuilder.addIsAccessor(this.createIsAccessor(tagName)); + } + + public addPolymerPropertiesAccessor(legacyProperties: DataWithComments<ts.ObjectLiteralExpression>) { + const returnStatement = ts.createReturn(legacyProperties.data); + const block = ts.createBlock([returnStatement]); + let propertiesAccessor = ts.createGetAccessor(undefined, [ts.createModifier(ts.SyntaxKind.StaticKeyword)], "properties", [], undefined, block); + if(legacyProperties.leadingComments.length > 0) { + propertiesAccessor = codeUtils.restoreLeadingComments(propertiesAccessor, legacyProperties.leadingComments); + } + this.classBuilder.addPolymerPropertiesAccessor(legacyProperties.data.pos, propertiesAccessor); + } + + public addPolymerPropertiesObservers(legacyObservers: ts.ArrayLiteralExpression) { + const returnStatement = ts.createReturn(legacyObservers); + const block = ts.createBlock([returnStatement]); + const propertiesAccessor = ts.createGetAccessor(undefined, [ts.createModifier(ts.SyntaxKind.StaticKeyword)], "observers", [], undefined, block); + + this.classBuilder.addPolymerObserversAccessor(legacyObservers.pos, propertiesAccessor); + } + + public addKeyBindings(keyBindings: ts.ObjectLiteralExpression) { + //In Polymer 2 keyBindings must be a property with get accessor + const returnStatement = ts.createReturn(keyBindings); + const block = ts.createBlock([returnStatement]); + const keyBindingsAccessor = ts.createGetAccessor(undefined, [], "keyBindings", [], undefined, block); + + this.classBuilder.addGetAccessor(keyBindings.pos, keyBindingsAccessor); + } + public addOrdinaryMethods(ordinaryMethods: OrdinaryMethods) { + for(const [name, method] of ordinaryMethods) { + this.classBuilder.addMethod(method.pos, method); + } + } + + public addOrdinaryGetAccessors(ordinaryGetAccessors: OrdinaryGetAccessors) { + for(const [name, accessor] of ordinaryGetAccessors) { + this.classBuilder.addGetAccessor(accessor.pos, accessor); + } + } + + public addOrdinaryShorthandProperties(ordinaryShorthandProperties: OrdinaryShorthandProperties) { + for (const [name, property] of ordinaryShorthandProperties) { + this.classBuilder.addClassFieldInitializer(property.name, property.name); + } + } + + public addOrdinaryPropertyAssignments(ordinaryPropertyAssignments: OrdinaryPropertyAssignments) { + for (const [name, property] of ordinaryPropertyAssignments) { + const propertyName = codeUtils.assertNodeKind(property.name, ts.SyntaxKind.Identifier) as ts.Identifier; + this.classBuilder.addClassFieldInitializer(propertyName, property.initializer); + } + } + + public addMixin(name: string, mixinArguments?: ts.Expression[]) { + let fullMixinArguments: ts.Expression[] = []; + if(mixinArguments) { + fullMixinArguments.push(...mixinArguments); + } + if(this.mixins) { + fullMixinArguments.push(this.mixins.expression); + } + if(fullMixinArguments.length > 0) { + this.mixins = ts.createExpressionWithTypeArguments([], ts.createCall(codeUtils.createNameExpression(name), [], fullMixinArguments.length > 0 ? fullMixinArguments : undefined)); + } + else { + this.mixins = ts.createExpressionWithTypeArguments([], codeUtils.createNameExpression(name)); + } + } + + public addClassJSDocComments(lines: string[]) { + this.classBuilder.addClassJSDocComments(lines); + } + + public build(): ClassBasedPolymerElement { + if(this.mixins) { + this.classBuilder.setBaseType(this.mixins); + } + const className = this.classBuilder.className; + const callExpression = ts.createCall(ts.createPropertyAccess(ts.createIdentifier("customElements"), "define"), undefined, [ts.createPropertyAccess(ts.createIdentifier(className), "is"), ts.createIdentifier(className)]); + const classBuilderResult = this.classBuilder.build(); + return { + classDeclaration: classBuilderResult.classDeclaration, + generatedComments: classBuilderResult.generatedComments, + componentRegistration: ts.createExpressionStatement(callExpression), + eventsComments: this.legacyComponent.componentSettings.eventsComments, + }; + } + + private createIsAccessor(tagName: string): ts.GetAccessorDeclaration { + const returnStatement = ts.createReturn(ts.createStringLiteral(tagName)); + const block = ts.createBlock([returnStatement]); + const accessor = ts.createGetAccessor([], [ts.createModifier(ts.SyntaxKind.StaticKeyword)], "is", [], undefined, block); + return codeUtils.addReplacableCommentAfterNode(accessor, "eventsComments"); + } + + public addLifecycleMethods(newLifecycleMethods: LifecycleMethod[]) { + for(const lifecycleMethod of newLifecycleMethods) { + this.classBuilder.addLifecycleMethod(lifecycleMethod.name, lifecycleMethod.originalPos, lifecycleMethod.method); + } + } +}
diff --git a/tools/polygerrit-updater/src/funcToClassConversion/updatedFileWriter.ts b/tools/polygerrit-updater/src/funcToClassConversion/updatedFileWriter.ts new file mode 100644 index 0000000..a147f50 --- /dev/null +++ b/tools/polygerrit-updater/src/funcToClassConversion/updatedFileWriter.ts
@@ -0,0 +1,248 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {LegacyPolymerComponent} from './polymerComponentParser'; +import * as ts from 'typescript'; +import * as codeUtils from '../utils/codeUtils'; +import * as path from "path"; +import * as fs from "fs"; +import {LegacyPolymerFuncReplaceResult} from './legacyPolymerFuncReplacer'; +import {CommentsParser} from '../utils/commentsParser'; + +export interface UpdatedFileWriterParameters { + out: string; + inplace: boolean; + writeOutput: boolean; + rootDir: string; +} + +interface Replacement { + start: number; + length: number; + newText: string; +} + +const elementRegistrationRegex = /^(\s*)customElements.define\((\w+).is, \w+\);$/m; +const maxLineLength = 80; + +export class UpdatedFileWriter { + public constructor(private readonly component: LegacyPolymerComponent, private readonly params: UpdatedFileWriterParameters) { + } + + public write(replaceResult: LegacyPolymerFuncReplaceResult, eventsComments: string[], generatedComments: string[]) { + const options: ts.PrinterOptions = { + removeComments: false, + newLine: ts.NewLineKind.LineFeed, + }; + const printer = ts.createPrinter(options); + let newContent = codeUtils.applyNewLines(printer.printFile(replaceResult.file)); + //ts printer doesn't keep original formatting of the file (spacing, new lines, comments, etc...). + //The following code tries restore original formatting + + const existingComments = this.collectAllComments(newContent, []); + + newContent = this.restoreEventsComments(newContent, eventsComments, existingComments); + newContent = this.restoreLeadingComments(newContent, replaceResult.leadingComments); + newContent = this.restoreFormating(printer, newContent); + newContent = this.splitLongLines(newContent); + newContent = this.addCommentsWarnings(newContent, generatedComments); + + if (this.params.writeOutput) { + const outDir = this.params.inplace ? this.params.rootDir : this.params.out; + const fullOutPath = path.resolve(outDir, this.component.jsFile); + const fullOutDir = path.dirname(fullOutPath); + if (!fs.existsSync(fullOutDir)) { + fs.mkdirSync(fullOutDir, { + recursive: true, + mode: fs.lstatSync(this.params.rootDir).mode + }); + } + fs.writeFileSync(fullOutPath, newContent); + } + } + + private restoreEventsComments(content: string, eventsComments: string[], existingComments: Map<string, number>): string { + //In some cases Typescript compiler keep existing comments. These comments + // must not be restored here + eventsComments = eventsComments.filter(c => !existingComments.has(this.getNormalizedComment(c))); + return codeUtils.replaceComment(content, "eventsComments", "\n" + eventsComments.join("\n\n") + "\n"); + } + + private restoreLeadingComments(content: string, leadingComments: string[]): string { + return leadingComments.reduce( + (newContent, comment, commentIndex) => + codeUtils.replaceComment(newContent, String(commentIndex), comment), + content); + } + + private restoreFormating(printer: ts.Printer, newContent: string): string { + const originalFile = this.component.parsedFile; + const newFile = ts.createSourceFile(originalFile.fileName, newContent, originalFile.languageVersion, true, ts.ScriptKind.JS); + const textMap = new Map<ts.SyntaxKind, Map<string, Set<string>>>(); + const comments = new Set<string>(); + this.collectAllStrings(printer, originalFile, textMap); + + const replacements: Replacement[] = []; + this.collectReplacements(printer, newFile, textMap, replacements); + replacements.sort((a, b) => b.start - a.start); + let result = newFile.getFullText(); + let prevReplacement: Replacement | null = null; + for (const replacement of replacements) { + if (prevReplacement) { + if (replacement.start + replacement.length > prevReplacement.start) { + throw new Error('Internal error! Replacements must not intersect'); + } + } + result = result.substring(0, replacement.start) + replacement.newText + result.substring(replacement.start + replacement.length); + prevReplacement = replacement; + } + return result; + } + + private splitLongLines(content: string): string { + content = content.replace(elementRegistrationRegex, (match, indent, className) => { + if (match.length > maxLineLength) { + return `${indent}customElements.define(${className}.is,\n` + + `${indent} ${className});`; + } + else { + return match; + } + }); + + return content + .replace( + "Polymer.LegacyDataMixin(Polymer.GestureEventListeners(Polymer.LegacyElementMixin(Polymer.Element)))", + "Polymer.LegacyDataMixin(\nPolymer.GestureEventListeners(\nPolymer.LegacyElementMixin(\nPolymer.Element)))") + .replace( + "Polymer.GestureEventListeners(Polymer.LegacyElementMixin(Polymer.Element))", + "Polymer.GestureEventListeners(\nPolymer.LegacyElementMixin(\nPolymer.Element))"); + + } + + private addCommentsWarnings(newContent: string, generatedComments: string[]): string { + const expectedComments = this.collectAllComments(this.component.parsedFile.getFullText(), generatedComments); + const newComments = this.collectAllComments(newContent, []); + const commentsWarnings = []; + for (const [text, count] of expectedComments) { + const newCount = newComments.get(text); + if (!newCount) { + commentsWarnings.push(`Comment '${text}' is missing in the new content.`); + } + else if (newCount != count) { + commentsWarnings.push(`Comment '${text}' appears ${newCount} times in the new file and ${count} times in the old file.`); + } + } + + for (const [text, newCount] of newComments) { + if (!expectedComments.has(text)) { + commentsWarnings.push(`Comment '${text}' appears only in the new content`); + } + } + if (commentsWarnings.length === 0) { + return newContent; + } + let commentsProblemStr = ""; + if (commentsWarnings.length > 0) { + commentsProblemStr = commentsWarnings.join("-----------------------------\n"); + console.log(commentsProblemStr); + } + + return "//This file has the following problems with comments:\n" + commentsProblemStr + "\n" + newContent; + + } + + private collectAllComments(content: string, additionalComments: string[]): Map<string, number> { + const comments = CommentsParser.collectAllComments(content); + comments.push(...additionalComments); + const result = new Map<string, number>(); + for (const comment of comments) { + let normalizedComment = this.getNormalizedComment(comment); + const count = result.get(normalizedComment); + if (count) { + result.set(normalizedComment, count + 1); + } else { + result.set(normalizedComment, 1); + } + } + return result; + } + + private getNormalizedComment(comment: string): string { + if(comment.startsWith('/**')) { + comment = comment.replace(/^\s+\*/gm, "*"); + } + return comment; + } + + private collectAllStrings(printer: ts.Printer, node: ts.Node, map: Map<ts.SyntaxKind, Map<string, Set<string>>>) { + const formattedText = printer.printNode(ts.EmitHint.Unspecified, node, node.getSourceFile()) + const originalText = node.getFullText(); + this.addIfNotExists(map, node.kind, formattedText, originalText); + ts.forEachChild(node, child => this.collectAllStrings(printer, child, map)); + } + + private collectReplacements(printer: ts.Printer, node: ts.Node, map: Map<ts.SyntaxKind, Map<string, Set<string>>>, replacements: Replacement[]) { + if(node.kind === ts.SyntaxKind.ThisKeyword || node.kind === ts.SyntaxKind.Identifier || node.kind === ts.SyntaxKind.StringLiteral || node.kind === ts.SyntaxKind.NumericLiteral) { + return; + } + const replacement = this.getReplacement(printer, node, map); + if(replacement) { + replacements.push(replacement); + return; + } + ts.forEachChild(node, child => this.collectReplacements(printer, child, map, replacements)); + } + + private addIfNotExists(map: Map<ts.SyntaxKind, Map<string, Set<string>>>, kind: ts.SyntaxKind, formattedText: string, originalText: string) { + let mapForKind = map.get(kind); + if(!mapForKind) { + mapForKind = new Map(); + map.set(kind, mapForKind); + } + + let existingOriginalText = mapForKind.get(formattedText); + if(!existingOriginalText) { + existingOriginalText = new Set<string>(); + mapForKind.set(formattedText, existingOriginalText); + //throw new Error(`Different formatting of the same string exists. Kind: ${ts.SyntaxKind[kind]}.\nFormatting 1:\n${originalText}\nFormatting2:\n${existingOriginalText}\n `); + } + existingOriginalText.add(originalText); + } + + private getReplacement(printer: ts.Printer, node: ts.Node, map: Map<ts.SyntaxKind, Map<string, Set<string>>>): Replacement | undefined { + const replacementsForKind = map.get(node.kind); + if(!replacementsForKind) { + return; + } + // Use printer instead of getFullText to "isolate" node content. + // node.getFullText returns text with indents from the original file. + const newText = printer.printNode(ts.EmitHint.Unspecified, node, node.getSourceFile()); + const originalSet = replacementsForKind.get(newText); + if(!originalSet || originalSet.size === 0) { + return; + } + if(originalSet.size >= 2) { + console.log(`Multiple replacements possible. Formatting of some lines can be changed`); + } + const replacementText: string = originalSet.values().next().value; + const nodeText = node.getFullText(); + return { + start: node.pos, + length: nodeText.length,//Do not use newText here! + newText: replacementText, + } + } + +} \ No newline at end of file
diff --git a/tools/polygerrit-updater/src/index.ts b/tools/polygerrit-updater/src/index.ts new file mode 100644 index 0000000..1b7c315 --- /dev/null +++ b/tools/polygerrit-updater/src/index.ts
@@ -0,0 +1,168 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as fs from "fs"; +import * as path from "path"; +import {LegacyPolymerComponent, LegacyPolymerComponentParser} from './funcToClassConversion/polymerComponentParser'; +import {ClassBasedPolymerElement} from './funcToClassConversion/polymerElementBuilder'; +import {PolymerFuncToClassBasedConverter} from './funcToClassConversion/funcToClassBasedElementConverter'; +import {LegacyPolymerFuncReplacer} from './funcToClassConversion/legacyPolymerFuncReplacer'; +import {UpdatedFileWriter} from './funcToClassConversion/updatedFileWriter'; +import {CommandLineParser} from './utils/commandLineParser'; + +interface UpdaterParameters { + htmlFiles: Set<string>; + jsFiles: Set<string>; + out: string; + inplace: boolean; + writeOutput: boolean; + rootDir: string; +} + +interface InputFilesFilter { + includeDir(path: string): boolean; + includeFile(path: string): boolean; +} + +function addFile(filePath: string, params: UpdaterParameters, filter: InputFilesFilter) { + const parsedPath = path.parse(filePath); + const ext = parsedPath.ext.toLowerCase(); + const relativePath = path.relative(params.rootDir, filePath); + if(!filter.includeFile(relativePath)) return; + if(relativePath.startsWith("../")) { + throw new Error(`${filePath} is not in rootDir ${params.rootDir}`); + } + if(ext === ".html") { + params.htmlFiles.add(relativePath); + } if(ext === ".js") { + params.jsFiles.add(relativePath); + } +} + +function addDirectory(dirPath: string, params: UpdaterParameters, recursive: boolean, filter: InputFilesFilter): void { + const entries = fs.readdirSync(dirPath, {withFileTypes: true}); + for(const entry of entries) { + const dirEnt = entry as fs.Dirent; + const fullPath = path.join(dirPath, dirEnt.name); + const relativePath = path.relative(params.rootDir, fullPath); + if(dirEnt.isDirectory()) { + if (!filter.includeDir(relativePath)) continue; + if(recursive) { + addDirectory(fullPath, params, recursive, filter); + } + } + else if(dirEnt.isFile()) { + addFile(fullPath, params, filter); + } else { + throw Error(`Unsupported dir entry '${entry.name}' in '${fullPath}'`); + } + } +} + +async function updateLegacyComponent(component: LegacyPolymerComponent, params: UpdaterParameters) { + const classBasedElement: ClassBasedPolymerElement = PolymerFuncToClassBasedConverter.convert(component); + + const replacer = new LegacyPolymerFuncReplacer(component); + const replaceResult = replacer.replace(classBasedElement); + try { + const writer = new UpdatedFileWriter(component, params); + writer.write(replaceResult, classBasedElement.eventsComments, classBasedElement.generatedComments); + } + finally { + replaceResult.dispose(); + } +} + +async function main() { + const params: UpdaterParameters = await getParams(); + if(params.jsFiles.size === 0) { + console.log("No files found"); + return; + } + const legacyPolymerComponentParser = new LegacyPolymerComponentParser(params.rootDir, params.htmlFiles) + for(const jsFile of params.jsFiles) { + console.log(`Processing ${jsFile}`); + const legacyComponent = await legacyPolymerComponentParser.parse(jsFile); + if(legacyComponent) { + await updateLegacyComponent(legacyComponent, params); + continue; + } + } +} + +interface CommandLineParameters { + src: string[]; + recursive: boolean; + excludes: string[]; + out: string; + inplace: boolean; + noOutput: boolean; + rootDir: string; +} + +async function getParams(): Promise<UpdaterParameters> { + const parser = new CommandLineParser({ + src: CommandLineParser.createStringArrayOption("src", ".js file or folder to process", []), + recursive: CommandLineParser.createBooleanOption("r", "process folder recursive", false), + excludes: CommandLineParser.createStringArrayOption("exclude", "List of file prefixes to exclude. If relative file path starts with one of the prefixes, it will be excluded", []), + out: CommandLineParser.createStringOption("out", "Output folder.", null), + rootDir: CommandLineParser.createStringOption("root", "Root directory for src files", "/"), + inplace: CommandLineParser.createBooleanOption("i", "Update files inplace", false), + noOutput: CommandLineParser.createBooleanOption("noout", "Do everything, but do not write anything to files", false), + }); + const commandLineParams: CommandLineParameters = parser.parse(process.argv) as CommandLineParameters; + + const params: UpdaterParameters = { + htmlFiles: new Set(), + jsFiles: new Set(), + writeOutput: !commandLineParams.noOutput, + inplace: commandLineParams.inplace, + out: commandLineParams.out, + rootDir: path.resolve(commandLineParams.rootDir) + }; + + if(params.writeOutput && !params.inplace && !params.out) { + throw new Error("You should specify output directory (--out directory_name)"); + } + + const filter = new ExcludeFilesFilter(commandLineParams.excludes); + for(const srcPath of commandLineParams.src) { + const resolvedPath = path.resolve(params.rootDir, srcPath); + if(fs.lstatSync(resolvedPath).isFile()) { + addFile(resolvedPath, params, filter); + } else { + addDirectory(resolvedPath, params, commandLineParams.recursive, filter); + } + } + return params; +} + +class ExcludeFilesFilter implements InputFilesFilter { + public constructor(private readonly excludes: string[]) { + } + includeDir(path: string): boolean { + return this.excludes.every(exclude => !path.startsWith(exclude)); + } + + includeFile(path: string): boolean { + return this.excludes.every(exclude => !path.startsWith(exclude)); + } +} + +main().then(() => { + process.exit(0); +}).catch(e => { + console.error(e); + process.exit(1); +});
diff --git a/tools/polygerrit-updater/src/utils/codeUtils.ts b/tools/polygerrit-updater/src/utils/codeUtils.ts new file mode 100644 index 0000000..53a7f0d --- /dev/null +++ b/tools/polygerrit-updater/src/utils/codeUtils.ts
@@ -0,0 +1,183 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as ts from 'typescript'; +import {SyntaxKind} from 'typescript'; +import {Node} from 'typescript'; + +export function assertNodeKind<T extends U, U extends ts.Node>(node: U, expectedKind: ts.SyntaxKind): T { + if (node.kind !== expectedKind) { + throw new Error(`Invlid node kind. Expected: ${ts.SyntaxKind[expectedKind]}, actual: ${ts.SyntaxKind[node.kind]}`); + } + return node as T; +} + +export function assertNodeKindOrUndefined<T extends U, U extends ts.Node>(node: U | undefined, expectedKind: ts.SyntaxKind): T | undefined { + if (!node) { + return undefined; + } + return assertNodeKind<T, U>(node, expectedKind); +} + +export function getPropertyAssignment(expression?: ts.ObjectLiteralElementLike): ts.PropertyAssignment | undefined { + return assertNodeKindOrUndefined(expression, ts.SyntaxKind.PropertyAssignment); +} + +export function getStringLiteralValue(expression: ts.Expression): string { + const literal: ts.StringLiteral = assertNodeKind(expression, ts.SyntaxKind.StringLiteral); + return literal.text; +} + +export function getBooleanLiteralValue(expression: ts.Expression): boolean { + if (expression.kind === ts.SyntaxKind.TrueKeyword) { + return true; + } + if (expression.kind === ts.SyntaxKind.FalseKeyword) { + return false; + } + throw new Error(`Invalid expression kind - ${expression.kind}`); +} + +export function getObjectLiteralExpression(expression: ts.Expression): ts.ObjectLiteralExpression { + return assertNodeKind(expression, ts.SyntaxKind.ObjectLiteralExpression); +} + +export function getArrayLiteralExpression(expression: ts.Expression): ts.ArrayLiteralExpression { + return assertNodeKind(expression, ts.SyntaxKind.ArrayLiteralExpression); +} + +export function replaceNode(file: ts.SourceFile, originalNode: ts.Node, newNode: ts.Node): ts.TransformationResult<ts.SourceFile> { + const nodeReplacerTransformer: ts.TransformerFactory<ts.SourceFile> = (context: ts.TransformationContext) => { + const visitor: ts.Visitor = (node) => { + if(node === originalNode) { + return newNode; + } + return ts.visitEachChild(node, visitor, context); + }; + + + return source => ts.visitNode(source, visitor); + }; + return ts.transform(file, [nodeReplacerTransformer]); +} + +export type NameExpression = ts.Identifier | ts.ThisExpression | ts.PropertyAccessExpression; +export function createNameExpression(fullPath: string): NameExpression { + const parts = fullPath.split("."); + let result: NameExpression = parts[0] === "this" ? ts.createThis() : ts.createIdentifier(parts[0]); + for(let i = 1; i < parts.length; i++) { + result = ts.createPropertyAccess(result, parts[i]); + } + return result; +} + +const generatedCommentNewLineAfterText = "-Generated code - new line after - 9cb292bc-5d88-4c5e-88f4-49535c93beb9 -"; +const generatedCommentNewLineBeforeText = "-Generated code - new line-before - 9cb292bc-5d88-4c5e-88f4-49535c93beb9 -"; +const generatedCommentNewLineAfterRegExp = new RegExp("//" + generatedCommentNewLineAfterText, 'g'); +const generatedCommentNewLineBeforeRegExp = new RegExp("//" + generatedCommentNewLineBeforeText + "\n", 'g'); +const replacableCommentText = "- Replacepoint - 9cb292bc-5d88-4c5e-88f4-49535c93beb9 -"; + +export function addNewLineAfterNode<T extends ts.Node>(node: T): T { + const comment = ts.getSyntheticTrailingComments(node); + if(comment && comment.some(c => c.text === generatedCommentNewLineAfterText)) { + return node; + } + return ts.addSyntheticTrailingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, generatedCommentNewLineAfterText, true); +} + +export function addNewLineBeforeNode<T extends ts.Node>(node: T): T { + const comment = ts.getSyntheticLeadingComments(node); + if(comment && comment.some(c => c.text === generatedCommentNewLineBeforeText)) { + return node; + } + return ts.addSyntheticLeadingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, generatedCommentNewLineBeforeText, true); +} + + +export function applyNewLines(text: string): string { + return text.replace(generatedCommentNewLineAfterRegExp, "").replace(generatedCommentNewLineBeforeRegExp, ""); + +} +export function addReplacableCommentAfterNode<T extends ts.Node>(node: T, name: string): T { + return ts.addSyntheticTrailingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, replacableCommentText + name, true); +} + +export function addReplacableCommentBeforeNode<T extends ts.Node>(node: T, name: string): T { + return ts.addSyntheticLeadingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, replacableCommentText + name, true); +} + +export function replaceComment(text: string, commentName: string, newContent: string): string { + return text.replace("//" + replacableCommentText + commentName, newContent); +} + +export function createMethod(name: string, methodDecl: ts.MethodDeclaration | undefined, codeAtStart: ts.Statement[], codeAtEnd: ts.Statement[], callSuperMethod: boolean): ts.MethodDeclaration | undefined { + if(!methodDecl && (codeAtEnd.length > 0 || codeAtEnd.length > 0)) { + methodDecl = ts.createMethod([], [], undefined, name, undefined, [], [],undefined, ts.createBlock([])); + } + if(!methodDecl) { + return; + } + if (!methodDecl.body) { + throw new Error("Method must have a body"); + } + if(methodDecl.parameters.length > 0) { + throw new Error("Methods with parameters are not supported"); + } + let newStatements = [...codeAtStart]; + if(callSuperMethod) { + const superCall: ts.CallExpression = ts.createCall(ts.createPropertyAccess(ts.createSuper(), assertNodeKind(methodDecl.name, ts.SyntaxKind.Identifier) as ts.Identifier), [], []); + const superCallExpression = ts.createExpressionStatement(superCall); + newStatements.push(superCallExpression); + } + newStatements.push(...codeAtEnd); + const newBody = ts.getMutableClone(methodDecl.body); + + newStatements = newStatements.map(m => addNewLineAfterNode(m)); + newStatements.splice(codeAtStart.length + 1, 0, ...newBody.statements); + + newBody.statements = ts.createNodeArray(newStatements); + + const newMethod = ts.getMutableClone(methodDecl); + newMethod.body = newBody; + + return newMethod; +} + +export function restoreLeadingComments<T extends Node>(node: T, originalComments: string[]): T { + if(originalComments.length === 0) { + return node; + } + for(const comment of originalComments) { + if(comment.startsWith("//")) { + node = ts.addSyntheticLeadingComment(node, SyntaxKind.SingleLineCommentTrivia, comment.substr(2), true); + } else if(comment.startsWith("/*")) { + if(!comment.endsWith("*/")) { + throw new Error(`Not support comment: ${comment}`); + } + node = ts.addSyntheticLeadingComment(node, SyntaxKind.MultiLineCommentTrivia, comment.substr(2, comment.length - 4), true); + } else { + throw new Error(`Not supported comment: ${comment}`); + } + } + return node; +} + +export function getLeadingComments(node: ts.Node): string[] { + const nodeText = node.getFullText(); + const commentRanges = ts.getLeadingCommentRanges(nodeText, 0); + if(!commentRanges) { + return []; + } + return commentRanges.map(range => nodeText.substring(range.pos, range.end)) +} \ No newline at end of file
diff --git a/tools/polygerrit-updater/src/utils/commandLineParser.ts b/tools/polygerrit-updater/src/utils/commandLineParser.ts new file mode 100644 index 0000000..658b7ff --- /dev/null +++ b/tools/polygerrit-updater/src/utils/commandLineParser.ts
@@ -0,0 +1,134 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed un der 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. + +export class CommandLineParser { + public static createStringArrayOption(optionName: string, help: string, defaultValue: string[]): CommandLineArgument { + return new StringArrayOption(optionName, help, defaultValue); + } + public static createBooleanOption(optionName: string, help: string, defaultValue: boolean): CommandLineArgument { + return new BooleanOption(optionName, help, defaultValue); + } + public static createStringOption(optionName: string, help: string, defaultValue: string | null): CommandLineArgument { + return new StringOption(optionName, help, defaultValue); + } + + public constructor(private readonly argumentTypes: {[name: string]: CommandLineArgument}) { + } + public parse(argv: string[]): object { + const result = Object.assign({}); + let index = 2; //argv[0] - node interpreter, argv[1] - index.js + for(const argumentField in this.argumentTypes) { + result[argumentField] = this.argumentTypes[argumentField].getDefaultValue(); + } + while(index < argv.length) { + let knownArgument = false; + for(const argumentField in this.argumentTypes) { + const argumentType = this.argumentTypes[argumentField]; + const argumentValue = argumentType.tryRead(argv, index); + if(argumentValue) { + knownArgument = true; + index += argumentValue.consumed; + result[argumentField] = argumentValue.value; + break; + } + } + if(!knownArgument) { + throw new Error(`Unknown argument ${argv[index]}`); + } + } + return result; + } +} + +interface CommandLineArgumentReadResult { + consumed: number; + value: any; +} + +export interface CommandLineArgument { + getDefaultValue(): any; + tryRead(argv: string[], startIndex: number): CommandLineArgumentReadResult | null; +} + +abstract class CommandLineOption implements CommandLineArgument { + protected constructor(protected readonly optionName: string, protected readonly help: string, private readonly defaultValue: any) { + } + public tryRead(argv: string[], startIndex: number): CommandLineArgumentReadResult | null { + if(argv[startIndex] !== "--" + this.optionName) { + return null; + } + const readArgumentsResult = this.readArguments(argv, startIndex + 1); + if(!readArgumentsResult) { + return null; + } + readArgumentsResult.consumed++; // Add option name + return readArgumentsResult; + } + public getDefaultValue(): any { + return this.defaultValue; + } + + protected abstract readArguments(argv: string[], startIndex: number) : CommandLineArgumentReadResult | null; +} + +class StringArrayOption extends CommandLineOption { + public constructor(optionName: string, help: string, defaultValue: string[]) { + super(optionName, help, defaultValue); + } + + protected readArguments(argv: string[], startIndex: number): CommandLineArgumentReadResult { + const result = []; + let index = startIndex; + while(index < argv.length) { + if(argv[index].startsWith("--")) { + break; + } + result.push(argv[index]); + index++; + } + return { + consumed: index - startIndex, + value: result + } + } +} + +class BooleanOption extends CommandLineOption { + public constructor(optionName: string, help: string, defaultValue: boolean) { + super(optionName, help, defaultValue); + } + + protected readArguments(argv: string[], startIndex: number): CommandLineArgumentReadResult { + return { + consumed: 0, + value: true + } + } +} + +class StringOption extends CommandLineOption { + public constructor(optionName: string, help: string, defaultValue: string | null) { + super(optionName, help, defaultValue); + } + + protected readArguments(argv: string[], startIndex: number): CommandLineArgumentReadResult | null { + if(startIndex >= argv.length) { + return null; + } + return { + consumed: 1, + value: argv[startIndex] + } + } +}
diff --git a/tools/polygerrit-updater/src/utils/commentsParser.ts b/tools/polygerrit-updater/src/utils/commentsParser.ts new file mode 100644 index 0000000..b849829 --- /dev/null +++ b/tools/polygerrit-updater/src/utils/commentsParser.ts
@@ -0,0 +1,79 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +enum CommentScannerState { + Text, + SingleLineComment, + MultLineComment +} +export class CommentsParser { + public static collectAllComments(text: string): string[] { + const result: string[] = []; + let state = CommentScannerState.Text; + let pos = 0; + function readSingleLineComment() { + const startPos = pos; + while(pos < text.length && text[pos] !== '\n') { + pos++; + } + return text.substring(startPos, pos); + } + function readMultiLineComment() { + const startPos = pos; + while(pos < text.length) { + if(pos < text.length - 1 && text[pos] === '*' && text[pos + 1] === '/') { + pos += 2; + break; + } + pos++; + } + return text.substring(startPos, pos); + } + + function skipString(lastChar: string) { + pos++; + while(pos < text.length) { + if(text[pos] === lastChar) { + pos++; + return; + } else if(text[pos] === '\\') { + pos+=2; + continue; + } + pos++; + } + } + + + while(pos < text.length - 1) { + if(text[pos] === '/' && text[pos + 1] === '/') { + result.push(readSingleLineComment()); + } else if(text[pos] === '/' && text[pos + 1] === '*') { + result.push(readMultiLineComment()); + } else if(text[pos] === "'") { + skipString("'"); + } else if(text[pos] === '"') { + skipString('"'); + } else if(text[pos] === '`') { + skipString('`'); + } else if(text[pos] == '/') { + skipString('/'); + } { + pos++; + } + + } + return result; + } +}
diff --git a/tools/polygerrit-updater/src/utils/polymerClassBuilder.ts b/tools/polygerrit-updater/src/utils/polymerClassBuilder.ts new file mode 100644 index 0000000..b1a4320 --- /dev/null +++ b/tools/polygerrit-updater/src/utils/polymerClassBuilder.ts
@@ -0,0 +1,270 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as ts from 'typescript'; +import * as codeUtils from './codeUtils'; +import {LegacyLifecycleMethodName, LegacyLifecycleMethodsArray} from '../funcToClassConversion/polymerComponentParser'; +import {SyntaxKind} from 'typescript'; + +enum PolymerClassMemberType { + IsAccessor, + Constructor, + PolymerPropertiesAccessor, + PolymerObserversAccessor, + Method, + ExistingLifecycleMethod, + NewLifecycleMethod, + GetAccessor, +} + +type PolymerClassMember = PolymerClassIsAccessor | PolymerClassConstructor | PolymerClassExistingLifecycleMethod | PolymerClassNewLifecycleMethod | PolymerClassSimpleMember; + +interface PolymerClassExistingLifecycleMethod { + member: ts.MethodDeclaration; + memberType: PolymerClassMemberType.ExistingLifecycleMethod; + name: string; + lifecycleOrder: number; + originalPos: number; +} + +interface PolymerClassNewLifecycleMethod { + member: ts.MethodDeclaration; + memberType: PolymerClassMemberType.NewLifecycleMethod; + name: string; + lifecycleOrder: number; + originalPos: -1 +} + +interface PolymerClassIsAccessor { + member: ts.GetAccessorDeclaration; + memberType: PolymerClassMemberType.IsAccessor; + originalPos: -1 +} + +interface PolymerClassConstructor { + member: ts.ConstructorDeclaration; + memberType: PolymerClassMemberType.Constructor; + originalPos: -1 +} + +interface PolymerClassSimpleMember { + memberType: PolymerClassMemberType.PolymerPropertiesAccessor | PolymerClassMemberType.PolymerObserversAccessor | PolymerClassMemberType.Method | PolymerClassMemberType.GetAccessor; + member: ts.ClassElement; + originalPos: number; +} + +export interface PolymerClassBuilderResult { + classDeclaration: ts.ClassDeclaration; + generatedComments: string[]; +} + +export class PolymerClassBuilder { + private readonly members: PolymerClassMember[] = []; + public readonly constructorStatements: ts.Statement[] = []; + private baseType: ts.ExpressionWithTypeArguments | undefined; + private classJsDocComments: string[]; + + public constructor(public readonly className: string) { + this.classJsDocComments = []; + } + + public addIsAccessor(accessor: ts.GetAccessorDeclaration) { + this.members.push({ + member: accessor, + memberType: PolymerClassMemberType.IsAccessor, + originalPos: -1 + }); + } + + public addPolymerPropertiesAccessor(originalPos: number, accessor: ts.GetAccessorDeclaration) { + this.members.push({ + member: accessor, + memberType: PolymerClassMemberType.PolymerPropertiesAccessor, + originalPos: originalPos + }); + } + + public addPolymerObserversAccessor(originalPos: number, accessor: ts.GetAccessorDeclaration) { + this.members.push({ + member: accessor, + memberType: PolymerClassMemberType.PolymerObserversAccessor, + originalPos: originalPos + }); + } + + + public addClassFieldInitializer(name: string | ts.Identifier, initializer: ts.Expression) { + const assignment = ts.createAssignment(ts.createPropertyAccess(ts.createThis(), name), initializer); + this.constructorStatements.push(codeUtils.addNewLineAfterNode(ts.createExpressionStatement(assignment))); + } + public addMethod(originalPos: number, method: ts.MethodDeclaration) { + this.members.push({ + member: method, + memberType: PolymerClassMemberType.Method, + originalPos: originalPos + }); + } + + public addGetAccessor(originalPos: number, accessor: ts.GetAccessorDeclaration) { + this.members.push({ + member: accessor, + memberType: PolymerClassMemberType.GetAccessor, + originalPos: originalPos + }); + } + + public addLifecycleMethod(name: LegacyLifecycleMethodName, originalPos: number, method: ts.MethodDeclaration) { + const lifecycleOrder = LegacyLifecycleMethodsArray.indexOf(name); + if(lifecycleOrder < 0) { + throw new Error(`Invalid lifecycle name`); + } + if(originalPos >= 0) { + this.members.push({ + member: method, + memberType: PolymerClassMemberType.ExistingLifecycleMethod, + originalPos: originalPos, + name: name, + lifecycleOrder: lifecycleOrder + }) + } else { + this.members.push({ + member: method, + memberType: PolymerClassMemberType.NewLifecycleMethod, + name: name, + lifecycleOrder: lifecycleOrder, + originalPos: -1 + }) + } + } + + public setBaseType(type: ts.ExpressionWithTypeArguments) { + if(this.baseType) { + throw new Error("Class can have only one base type"); + } + this.baseType = type; + } + + public build(): PolymerClassBuilderResult { + let heritageClauses: ts.HeritageClause[] = []; + if (this.baseType) { + const extendClause = ts.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [this.baseType]); + heritageClauses.push(extendClause); + } + const finalMembers: PolymerClassMember[] = []; + const isAccessors = this.members.filter(member => member.memberType === PolymerClassMemberType.IsAccessor); + if(isAccessors.length !== 1) { + throw new Error("Class must have exactly one 'is'"); + } + finalMembers.push(isAccessors[0]); + const constructorMember = this.createConstructor(); + if(constructorMember) { + finalMembers.push(constructorMember); + } + + const newLifecycleMethods: PolymerClassNewLifecycleMethod[] = []; + this.members.forEach(member => { + if(member.memberType === PolymerClassMemberType.NewLifecycleMethod) { + newLifecycleMethods.push(member); + } + }); + + const methodsWithKnownPosition = this.members.filter(member => member.originalPos >= 0); + methodsWithKnownPosition.sort((a, b) => a.originalPos - b.originalPos); + + finalMembers.push(...methodsWithKnownPosition); + + + for(const newLifecycleMethod of newLifecycleMethods) { + //Number of methods is small - use brute force solution + let closestNextIndex = -1; + let closestNextOrderDiff: number = LegacyLifecycleMethodsArray.length; + let closestPrevIndex = -1; + let closestPrevOrderDiff: number = LegacyLifecycleMethodsArray.length; + for (let i = 0; i < finalMembers.length; i++) { + const member = finalMembers[i]; + if (member.memberType !== PolymerClassMemberType.NewLifecycleMethod && member.memberType !== PolymerClassMemberType.ExistingLifecycleMethod) { + continue; + } + const orderDiff = member.lifecycleOrder - newLifecycleMethod.lifecycleOrder; + if (orderDiff > 0) { + if (orderDiff < closestNextOrderDiff) { + closestNextIndex = i; + closestNextOrderDiff = orderDiff; + } + } else if (orderDiff < 0) { + if (orderDiff < closestPrevOrderDiff) { + closestPrevIndex = i; + closestPrevOrderDiff = orderDiff; + } + } + } + let insertIndex; + if (closestNextIndex !== -1 || closestPrevIndex !== -1) { + insertIndex = closestNextOrderDiff < closestPrevOrderDiff ? + closestNextIndex : closestPrevIndex + 1; + } else { + insertIndex = Math.max( + finalMembers.findIndex(m => m.memberType === PolymerClassMemberType.Constructor), + finalMembers.findIndex(m => m.memberType === PolymerClassMemberType.IsAccessor), + finalMembers.findIndex(m => m.memberType === PolymerClassMemberType.PolymerPropertiesAccessor), + finalMembers.findIndex(m => m.memberType === PolymerClassMemberType.PolymerObserversAccessor), + ); + if(insertIndex < 0) { + insertIndex = finalMembers.length; + } else { + insertIndex++;//Insert after + } + } + finalMembers.splice(insertIndex, 0, newLifecycleMethod); + } + //Asserts about finalMembers + const nonConstructorMembers = finalMembers.filter(m => m.memberType !== PolymerClassMemberType.Constructor); + + if(nonConstructorMembers.length !== this.members.length) { + throw new Error(`Internal error! Some methods are missed`); + } + let classDeclaration = ts.createClassDeclaration(undefined, undefined, this.className, undefined, heritageClauses, finalMembers.map(m => m.member)) + const generatedComments: string[] = []; + if(this.classJsDocComments.length > 0) { + const commentContent = '*\n' + this.classJsDocComments.map(line => `* ${line}`).join('\n') + '\n'; + classDeclaration = ts.addSyntheticLeadingComment(classDeclaration, ts.SyntaxKind.MultiLineCommentTrivia, commentContent, true); + generatedComments.push(`/*${commentContent}*/`); + } + return { + classDeclaration, + generatedComments, + }; + + } + + private createConstructor(): PolymerClassConstructor | null { + if(this.constructorStatements.length === 0) { + return null; + } + let superCall: ts.CallExpression = ts.createCall(ts.createSuper(), [], []); + const superCallExpression = ts.createExpressionStatement(superCall); + const statements = [superCallExpression, ...this.constructorStatements]; + const constructorDeclaration = ts.createConstructor([], [], [], ts.createBlock(statements, true)); + + return { + memberType: PolymerClassMemberType.Constructor, + member: constructorDeclaration, + originalPos: -1 + }; + } + + public addClassJSDocComments(lines: string[]) { + this.classJsDocComments.push(...lines); + } +} \ No newline at end of file
diff --git a/tools/polygerrit-updater/src/utils/unexpectedValue.ts b/tools/polygerrit-updater/src/utils/unexpectedValue.ts new file mode 100644 index 0000000..690c283 --- /dev/null +++ b/tools/polygerrit-updater/src/utils/unexpectedValue.ts
@@ -0,0 +1,17 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export function unexpectedValue<T>(x: T): never { + throw new Error(`Unexpected value '${x}'`); +}
diff --git a/tools/polygerrit-updater/tsconfig.json b/tools/polygerrit-updater/tsconfig.json new file mode 100644 index 0000000..37ff1b2 --- /dev/null +++ b/tools/polygerrit-updater/tsconfig.json
@@ -0,0 +1,67 @@ +{ + "compilerOptions": { + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./js", /* Redirect output structure to the directory. */ + "rootDir": ".", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + "paths": { + "*": [ "node_modules/*" ] + }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + }, + "include": ["./src/**/*"] +}
diff --git a/version.bzl b/version.bzl index d758905..fb1e5ca 100644 --- a/version.bzl +++ b/version.bzl
@@ -2,4 +2,4 @@ # Used by :api_install and :api_deploy targets # when talking to the destination repository. # -GERRIT_VERSION = "3.1.3-SNAPSHOT" +GERRIT_VERSION = "3.2.0-SNAPSHOT"