gerrit (empty) → 0.1.0.0
raw patch · 18 files changed
+1527/−0 lines, 18 filesdep +aesondep +basedep +bytestring
Dependencies added: aeson, base, bytestring, containers, directory, gerrit, http-client, http-client-tls, tasty, tasty-hunit, text
Files
- CHANGELOG.md +5/−0
- LICENSE +201/−0
- README.md +20/−0
- gerrit.cabal +72/−0
- src/Gerrit.hs +96/−0
- src/Gerrit/Client.hs +79/−0
- src/Gerrit/Data.hs +168/−0
- src/Gerrit/Event.hs +448/−0
- test/Spec.hs +111/−0
- test/data/EventChangeAbandoned.json +49/−0
- test/data/EventChangeMerged.json +47/−0
- test/data/EventCommentAdded.json +70/−0
- test/data/EventPatchsetCreated.json +50/−0
- test/data/EventProjectCreated.json +6/−0
- test/data/EventRefUpdated.json +15/−0
- test/data/GerritChange.json +84/−0
- test/data/ReviewInput.json +1/−0
- test/data/ReviewResult.json +5/−0
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Changelog++## 0.1.0.0 (2021-05-30)++- Basic Gerrit API data types.
+ LICENSE view
@@ -0,0 +1,201 @@+ Apache License+ Version 2.0, January 2004+ http://www.apache.org/licenses/++ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION++ 1. Definitions.++ "License" shall mean the terms and conditions for use, reproduction,+ and distribution as defined by Sections 1 through 9 of this document.++ "Licensor" shall mean the copyright owner or entity authorized by+ the copyright owner that is granting the License.++ "Legal Entity" shall mean the union of the acting entity and all+ other entities that control, are controlled by, or are under common+ control with that entity. For the purposes of this definition,+ "control" means (i) the power, direct or indirect, to cause the+ direction or management of such entity, whether by contract or+ otherwise, or (ii) ownership of fifty percent (50%) or more of the+ outstanding shares, or (iii) beneficial ownership of such entity.++ "You" (or "Your") shall mean an individual or Legal Entity+ exercising permissions granted by this License.++ "Source" form shall mean the preferred form for making modifications,+ including but not limited to software source code, documentation+ source, and configuration files.++ "Object" form shall mean any form resulting from mechanical+ transformation or translation of a Source form, including but+ not limited to compiled object code, generated documentation,+ and conversions to other media types.++ "Work" shall mean the work of authorship, whether in Source or+ Object form, made available under the License, as indicated by a+ copyright notice that is included in or attached to the work+ (an example is provided in the Appendix below).++ "Derivative Works" shall mean any work, whether in Source or Object+ form, that is based on (or derived from) the Work and for which the+ editorial revisions, annotations, elaborations, or other modifications+ represent, as a whole, an original work of authorship. For the purposes+ of this License, Derivative Works shall not include works that remain+ separable from, or merely link (or bind by name) to the interfaces of,+ the Work and Derivative Works thereof.++ "Contribution" shall mean any work of authorship, including+ the original version of the Work and any modifications or additions+ to that Work or Derivative Works thereof, that is intentionally+ submitted to Licensor for inclusion in the Work by the copyright owner+ or by an individual or Legal Entity authorized to submit on behalf of+ the copyright owner. For the purposes of this definition, "submitted"+ means any form of electronic, verbal, or written communication sent+ to the Licensor or its representatives, including but not limited to+ communication on electronic mailing lists, source code control systems,+ and issue tracking systems that are managed by, or on behalf of, the+ Licensor for the purpose of discussing and improving the Work, but+ excluding communication that is conspicuously marked or otherwise+ designated in writing by the copyright owner as "Not a Contribution."++ "Contributor" shall mean Licensor and any individual or Legal Entity+ on behalf of whom a Contribution has been received by Licensor and+ subsequently incorporated within the Work.++ 2. Grant of Copyright License. Subject to the terms and conditions of+ this License, each Contributor hereby grants to You a perpetual,+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable+ copyright license to reproduce, prepare Derivative Works of,+ publicly display, publicly perform, sublicense, and distribute the+ Work and such Derivative Works in Source or Object form.++ 3. Grant of Patent License. Subject to the terms and conditions of+ this License, each Contributor hereby grants to You a perpetual,+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable+ (except as stated in this section) patent license to make, have made,+ use, offer to sell, sell, import, and otherwise transfer the Work,+ where such license applies only to those patent claims licensable+ by such Contributor that are necessarily infringed by their+ Contribution(s) alone or by combination of their Contribution(s)+ with the Work to which such Contribution(s) was submitted. If You+ institute patent litigation against any entity (including a+ cross-claim or counterclaim in a lawsuit) alleging that the Work+ or a Contribution incorporated within the Work constitutes direct+ or contributory patent infringement, then any patent licenses+ granted to You under this License for that Work shall terminate+ as of the date such litigation is filed.++ 4. Redistribution. You may reproduce and distribute copies of the+ Work or Derivative Works thereof in any medium, with or without+ modifications, and in Source or Object form, provided that You+ meet the following conditions:++ (a) You must give any other recipients of the Work or+ Derivative Works a copy of this License; and++ (b) You must cause any modified files to carry prominent notices+ stating that You changed the files; and++ (c) You must retain, in the Source form of any Derivative Works+ that You distribute, all copyright, patent, trademark, and+ attribution notices from the Source form of the Work,+ excluding those notices that do not pertain to any part of+ the Derivative Works; and++ (d) If the Work includes a "NOTICE" text file as part of its+ distribution, then any Derivative Works that You distribute must+ include a readable copy of the attribution notices contained+ within such NOTICE file, excluding those notices that do not+ pertain to any part of the Derivative Works, in at least one+ of the following places: within a NOTICE text file distributed+ as part of the Derivative Works; within the Source form or+ documentation, if provided along with the Derivative Works; or,+ within a display generated by the Derivative Works, if and+ wherever such third-party notices normally appear. The contents+ of the NOTICE file are for informational purposes only and+ do not modify the License. You may add Your own attribution+ notices within Derivative Works that You distribute, alongside+ or as an addendum to the NOTICE text from the Work, provided+ that such additional attribution notices cannot be construed+ as modifying the License.++ You may add Your own copyright statement to Your modifications and+ may provide additional or different license terms and conditions+ for use, reproduction, or distribution of Your modifications, or+ for any such Derivative Works as a whole, provided Your use,+ reproduction, and distribution of the Work otherwise complies with+ the conditions stated in this License.++ 5. Submission of Contributions. Unless You explicitly state otherwise,+ any Contribution intentionally submitted for inclusion in the Work+ by You to the Licensor shall be under the terms and conditions of+ this License, without any additional terms or conditions.+ Notwithstanding the above, nothing herein shall supersede or modify+ the terms of any separate license agreement you may have executed+ with Licensor regarding such Contributions.++ 6. Trademarks. This License does not grant permission to use the trade+ names, trademarks, service marks, or product names of the Licensor,+ except as required for reasonable and customary use in describing the+ origin of the Work and reproducing the content of the NOTICE file.++ 7. Disclaimer of Warranty. Unless required by applicable law or+ agreed to in writing, Licensor provides the Work (and each+ Contributor provides its Contributions) on an "AS IS" BASIS,+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or+ implied, including, without limitation, any warranties or conditions+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A+ PARTICULAR PURPOSE. You are solely responsible for determining the+ appropriateness of using or redistributing the Work and assume any+ risks associated with Your exercise of permissions under this License.++ 8. Limitation of Liability. In no event and under no legal theory,+ whether in tort (including negligence), contract, or otherwise,+ unless required by applicable law (such as deliberate and grossly+ negligent acts) or agreed to in writing, shall any Contributor be+ liable to You for damages, including any direct, indirect, special,+ incidental, or consequential damages of any character arising as a+ result of this License or out of the use or inability to use the+ Work (including but not limited to damages for loss of goodwill,+ work stoppage, computer failure or malfunction, or any and all+ other commercial damages or losses), even if such Contributor+ has been advised of the possibility of such damages.++ 9. Accepting Warranty or Additional Liability. While redistributing+ the Work or Derivative Works thereof, You may choose to offer,+ and charge a fee for, acceptance of support, warranty, indemnity,+ or other liability obligations and/or rights consistent with this+ License. However, in accepting such obligations, You may act only+ on Your own behalf and on Your sole responsibility, not on behalf+ of any other Contributor, and only if You agree to indemnify,+ defend, and hold each Contributor harmless for any liability+ incurred by, or claims asserted against, such Contributor by reason+ of your accepting any such warranty or additional liability.++ END OF TERMS AND CONDITIONS++ APPENDIX: How to apply the Apache License to your work.++ To apply the Apache License to your work, attach the following+ boilerplate notice, with the fields enclosed by brackets "[]"+ replaced with your own identifying information. (Don't include+ the brackets!) The text should be enclosed in the appropriate+ comment syntax for the file format. We also recommend that a+ file or class name and description of purpose be included on the+ same "printed page" as the copyright notice for easier+ identification within third-party archives.++ Copyright [yyyy] [name of copyright owner]++ 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.
+ README.md view
@@ -0,0 +1,20 @@+# gerrit++[](https://hackage.haskell.org/package/gerrit)+[](LICENSE)++A client library for [gerrit](https://www.gerritcodereview.com/).++## Features++- JSON decoder for API endpoints and events stream.+- HTTP client helper functions.++## Contribute++Run tests:++```+$ cabal build+$ cabal test+```
+ gerrit.cabal view
@@ -0,0 +1,72 @@+cabal-version: 2.4+name: gerrit+version: 0.1.0.0+synopsis: A gerrit client library+description:+ Gerrit is a client library to interface with https://www.gerritcodereview.com/+ .+ Use this library to query and post to gerrit REST API.+ .+homepage: https://github.com/softwarefactory-project/gerrit-haskell#readme+bug-reports: https://github.com/softwarefactory-project/gerrit-haskell/issues+license: Apache-2.0+license-file: LICENSE+author: Tristan de Cacqueray+maintainer: tdecacqu@redhat.com+copyright: 2020 Red Hat+category: Development+build-type: Simple+extra-doc-files: CHANGELOG.md+ README.md+extra-source-files: test/data/*.json+tested-with: GHC == 8.10.4++source-repository head+ type: git+ location: https://github.com/softwarefactory-project/gerrit-haskell.git++common common-options+ build-depends: base >= 4.11.0.0 && < 5+ , aeson >= 1.0.0.0 && < 1.6+ , bytestring < 0.11+ ghc-options: -Wall+ -Wcompat+ -Widentities+ -Wincomplete-uni-patterns+ -Wincomplete-record-updates+ -Wredundant-constraints+ if impl(ghc >= 8.2)+ ghc-options: -fhide-source-paths+ if impl(ghc >= 8.4)+ ghc-options: -Wmissing-export-lists+ -Wpartial-fields+ if impl(ghc >= 8.10)+ ghc-options: -Wunused-packages++ default-language: Haskell2010++library+ import: common-options+ build-depends: containers >= 0.5.8.0 && < 0.7+ , http-client >= 0.5.0 && < 0.8+ , http-client-tls >= 0.2.0 && < 0.4+ , text >= 0.11.1.0 && < 1.3++ hs-source-dirs: src+ exposed-modules: Gerrit+ , Gerrit.Data+ , Gerrit.Event+ other-modules: Gerrit.Client++test-suite gerrit-test+ import: common-options+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Spec.hs+ build-depends: directory+ , gerrit+ , tasty >= 0.11.2 && < 1.5+ , tasty-hunit >= 0.10 && < 0.11+ ghc-options: -threaded+ -rtsopts+ -with-rtsopts=-N
+ src/Gerrit.hs view
@@ -0,0 +1,96 @@+{-# LANGUAGE OverloadedStrings #-}++-- | This module contains the gerrit client library+module Gerrit+ ( -- * Client+ GerritClient,+ withClient,++ -- * Api+ getVersion,+ getChange,+ queryChanges,+ postReview,++ -- * Main data types+ GerritVersion (..),+ GerritQuery (..),+ GerritChange (..),+ GerritChangeStatus (..),+ ReviewResult (..),++ -- * Convenient functions+ changeUrl,+ hasLabel,+ )+where++import Control.Exception (try)+import qualified Data.Map as M+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import qualified Data.Text as T+import Gerrit.Client+import Gerrit.Data+import Network.HTTP.Client (HttpException)++-- | Return the url of a 'GerritChange'+changeUrl :: GerritClient -> GerritChange -> T.Text+changeUrl client change = baseUrl client <> T.pack (show (number change))++-- | Get the server version+getVersion :: GerritClient -> IO GerritVersion+getVersion = gerritGet "config/server/version"++-- | Search for changes+queryChanges :: Int -> [GerritQuery] -> GerritClient -> IO [GerritChange]+queryChanges count queries = gerritGet ("changes/?" <> queryString)+ where+ queryString = T.intercalate "&" [changeString, countString, option]+ changeString = "q=" <> T.intercalate "+" (map queryText queries)+ countString = "n=" <> T.pack (show count)+ option = "o=CURRENT_REVISION&o=DETAILED_LABELS"++getChange :: Int -> GerritClient -> IO (Maybe GerritChange)+getChange changeNumber client = do+ res <-+ try+ ( gerritGet ("changes/" <> T.pack (show changeNumber) <> "?o=CURRENT_REVISION&o=DETAILED_LABELS") client+ ) ::+ IO (Either HttpException GerritChange)+ pure $ case res of+ Right change -> Just change+ Left _err -> Nothing++-- | Post a review+postReview ::+ -- | The change to review+ GerritChange ->+ -- | A message+ Text ->+ -- | A label+ Text ->+ -- | A vote+ Int ->+ -- | The client+ GerritClient ->+ -- | Returns the ReviewResult+ IO ReviewResult+postReview change message label value' = gerritPost urlPath review+ where+ urlPath = "changes/" <> changeId <> "/revisions/" <> revHash <> "/review"+ changeId = Gerrit.Data.id change+ revHash = fromMaybe "" (Gerrit.Data.current_revision change)+ review =+ ReviewInput+ { riMessage = Just message,+ riLabels = Just (M.fromList [(label, value')])+ }++-- | Check if a gerrit change as a label+hasLabel :: T.Text -> Int -> GerritChange -> Bool+hasLabel label labelValue change = case M.lookup label (labels change) of+ Just gerritLabel ->+ (> 0) $+ length $ filter (\vote -> fromMaybe 0 (value vote) == labelValue) (Gerrit.Data.all gerritLabel)+ _ -> False
+ src/Gerrit/Client.hs view
@@ -0,0 +1,79 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++-- | This module contains the internal gerrit REST client+module Gerrit.Client+ ( GerritClient (baseUrl),+ withClient,+ gerritGet,+ gerritPost,+ )+where++import Data.Aeson (FromJSON, ToJSON, eitherDecode, encode)+import qualified Data.ByteString.Lazy as BSL+import Data.Maybe (fromMaybe)+import Data.Text (Text, unpack)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Network.HTTP.Client+import Network.HTTP.Client.TLS (tlsManagerSettings)+import System.Environment (lookupEnv)++-- | The GerritClient record, use 'withClient' to create+data GerritClient = GerritClient+ { baseUrl :: Text,+ manager :: Manager,+ auth :: Maybe (Text, Text)+ }++-- | Create the 'GerritClient'+withClient ::+ -- | The gerrit api url+ Text ->+ -- | A username (password is read from GERRIT_PASSWORD environment)+ Maybe Text ->+ -- | The callback+ (GerritClient -> IO ()) ->+ -- | withClient performs the IO+ IO ()+withClient url username callBack =+ do+ manager <- newManager tlsManagerSettings+ auth <- case username of+ Just user -> do+ pass <- lookupEnv "GERRIT_PASSWORD"+ pure $ Just (user, T.pack $ fromMaybe "" pass)+ _ -> pure Nothing+ callBack (GerritClient {..})+ where+ baseUrl = T.dropWhileEnd (== '/') url <> "/"++gerritDecode :: (FromJSON a, Applicative f) => Response BSL.ByteString -> f a+gerritDecode response = case eitherDecode $ BSL.drop 5 $ responseBody response of+ Left err -> error $ "Decoding of " <> show (responseBody response) <> " failed with: " <> err+ Right a -> pure a++gerritPost :: (ToJSON a, FromJSON b) => Text -> a -> GerritClient -> IO b+gerritPost path postData GerritClient {..} =+ do+ initRequest <- case auth of+ Just (user, pass) ->+ applyBasicAuth (T.encodeUtf8 user) (T.encodeUtf8 pass)+ <$> parseUrlThrow (unpack $ baseUrl <> "a/" <> path)+ Nothing -> parseUrlThrow (unpack $ baseUrl <> path)+ let request =+ initRequest+ { method = "POST",+ requestHeaders = requestHeaders initRequest <> [("Content-Type", "application/json; charset=UTF-8")],+ requestBody = RequestBodyLBS $ encode postData+ }+ response <- httpLbs request manager+ gerritDecode response++gerritGet :: (FromJSON a) => Text -> GerritClient -> IO a+gerritGet path GerritClient {..} =+ do+ request <- parseUrlThrow (unpack $ baseUrl <> path)+ response <- httpLbs request manager+ gerritDecode response
+ src/Gerrit/Data.hs view
@@ -0,0 +1,168 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}++-- | This module contains the gerrit data type+module Gerrit.Data+ ( -- * Main data types+ GerritVersion (..),+ GerritQuery (..),+ GerritChangeStatus (..),+ GerritChange (..),+ GerritRevision (..),+ GerritLabel (..),+ GerritAccount (..),+ GerritLabelVote (..),+ GerritDetailedLabelVote (..),+ GerritDetailedLabel (..),+ ReviewResult (..),+ ReviewInput (..),++ -- * Convenient functions+ queryText,+ )+where++import Data.Aeson+import Data.Char (isUpper, toLower)+import qualified Data.Map as M+import Data.Text (Text)+import qualified Data.Text as T+import GHC.Generics (Generic)++newtype GerritVersion = GerritVersion Text+ deriving (Show, Generic)+ deriving anyclass (FromJSON)++-- https://gerrit-review.googlesource.com/Documentation/json.html+data GerritRevisionKind = REWORK | TRIVIAL_REBASE | MERGE_FIRST_PARENT_UPDATE | NO_CODE_CHANGE | NO_CHANGE+ deriving (Eq, Show, Generic, FromJSON)++data GerritChangeStatus = NEW | MERGED | ABANDONED | DRAFT+ deriving (Eq, Show, Generic, FromJSON)++data GerritLabelVote = REJECTED | APPROVED | DISLIKED | RECOMMENDED+ deriving (Eq, Show, Ord, Generic)++-- We use a custom parseJSON to decode Label Vote as lowercase+instance FromJSON GerritLabelVote where+ parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = map toLower}++instance FromJSONKey GerritLabelVote where+ fromJSONKey = genericFromJSONKey defaultJSONKeyOptions {keyModifier = map toLower}++-- | Modify record attribute to match json schema+-- Remove the prefix and use snakecase+customParseJSON :: String -> Options+customParseJSON prefix = defaultOptions {fieldLabelModifier = recordToJson}+ where+ recordToJson = updateCase . drop (length prefix)+ updateCase [] = []+ updateCase (x : xs) = toLower x : updateCase' xs+ updateCase' [] = []+ updateCase' (x : xs)+ | isUpper x = '_' : toLower x : updateCase' xs+ | otherwise = x : updateCase' xs++-- https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html+data ReviewResult = ReviewResult+ { rrLabels :: Maybe (M.Map Text Int),+ rrReady :: Maybe Int+ }+ deriving (Eq, Show, Ord, Generic)++instance FromJSON ReviewResult where+ parseJSON = genericParseJSON $ customParseJSON "rr"++instance ToJSON ReviewResult where+ toJSON = genericToJSON $ customParseJSON "rr"++data ReviewInput = ReviewInput+ { riMessage :: Maybe Text,+ riLabels :: Maybe (M.Map Text Int)+ }+ deriving (Eq, Show, Ord, Generic)++instance FromJSON ReviewInput where+ parseJSON = genericParseJSON $ customParseJSON "ri"++instance ToJSON ReviewInput where+ toJSON = genericToJSON $ (customParseJSON "ri") {omitNothingFields = True}++-- https://gerrit-review.googlesource.com/Documentation/user-search.html+data GerritQuery+ = Status GerritChangeStatus+ | Owner Text+ | CommitMessage Text+ | Project Text+ | ChangeId Text++-- | Convert a GerritQuery object to the search terms+queryText :: GerritQuery -> Text+queryText (Status stat) = "status:" <> T.toLower (T.pack $ show stat)+queryText (Owner owner) = "owner:" <> owner+queryText (CommitMessage message) = "message:" <> message+queryText (Project project') = "project:" <> project'+queryText (ChangeId changeId) = "change:" <> changeId++data GerritRevision = GerritRevision+ { ref :: Text,+ kind :: GerritRevisionKind+ }+ deriving (Show, Generic, FromJSON)++newtype GerritAccount = GerritAccount+ { unused_account_id :: Int+ }+ deriving (Show, Generic)++-- We use a cusom parseJSON to decode `_account_id` as `account_id`+instance FromJSON GerritAccount where+ parseJSON = genericParseJSON aesonOptions++newtype GerritLabel+ = GerritLabel (M.Map GerritLabelVote GerritAccount)+ deriving (Show, Generic)+ deriving anyclass (FromJSON)++data GerritDetailedLabelVote = GerritDetailedLabelVote+ { value :: Maybe Int,+ account_id :: Int+ }+ deriving (Show, Generic)++-- We use a cusom parseJSON to decode record field properly+instance FromJSON GerritDetailedLabelVote where+ parseJSON = genericParseJSON aesonOptions++data GerritDetailedLabel = GerritDetailedLabel+ { all :: [GerritDetailedLabelVote],+ default_value :: Int+ }+ deriving (Show, Generic, FromJSON)++data GerritChange = GerritChange+ { id :: Text,+ project :: Text,+ branch :: Text,+ subject :: Text,+ status :: GerritChangeStatus,+ mergeable :: Maybe Bool,+ revisions :: M.Map Text (Maybe GerritRevision),+ current_revision :: Maybe Text,+ number :: Int,+ labels :: M.Map Text GerritDetailedLabel+ }+ deriving (Show, Generic)++-- We use a cusom parseJSON to decode `_number` as `number`+instance FromJSON GerritChange where+ parseJSON = genericParseJSON aesonOptions++aesonOptions :: Options+aesonOptions = defaultOptions {fieldLabelModifier = recordToJson}+ where+ recordToJson "number" = "_number"+ recordToJson "account_id" = "_account_id"+ recordToJson n = n
+ src/Gerrit/Event.hs view
@@ -0,0 +1,448 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++-- | Gerrit stream event data type+module Gerrit.Event+ ( -- * Main event data types+ EventType (..),+ Event (..),++ -- * Common data types+ User (..),+ PatchSet (..),+ Change (..),+ Ref (..),++ -- * Individual event data types+ AssigneeChanged (..),+ ChangeAbandoned (..),+ ChangeDeleted (..),+ ChangeMerged (..),+ ChangeRestored (..),+ CommentAdded (..),+ HashtagsChanged (..),+ ProjectCreated (..),+ PatchsetCreated (..),+ RefUpdated (..),+ ReviewerAdded (..),+ ReviewerDeleted (..),+ TopicChanged (..),+ WorkInProgressStateChanged (..),+ PrivateStateChanged (..),+ VoteDeleted (..),++ -- * Convenient functions+ getChange,+ getUser,+ getEventType,+ eventName,+ )+where++import Control.Monad (mzero)+import Data.Aeson+ ( FromJSON (..),+ Options (fieldLabelModifier),+ Value (Object, String),+ defaultOptions,+ genericParseJSON,+ (.:),+ )+import Data.Char (toLower)+import Data.Text (Text)+import qualified Data.Text as Text+import GHC.Generics (Generic)++aesonOption :: Text -> Options+aesonOption prefix = defaultOptions {fieldLabelModifier = recordToJson}+ where+ recordToJson = updateCase . drop (Text.length prefix)+ updateCase [] = []+ updateCase (x : xs) = toLower x : xs++-------------------------------------------------------------------------------+-- Shared types+-------------------------------------------------------------------------------+data User = User+ { userName :: Maybe Text,+ userEmail :: Maybe Text,+ userUsername :: Maybe Text+ }+ deriving (Show, Eq, Generic)++instance FromJSON User where+ parseJSON = genericParseJSON $ aesonOption "user"++data PatchSet = PatchSet+ { patchSetNumber :: Int,+ patchSetRef :: Text,+ patchSetUploader :: User,+ patchsetAuthor :: User+ }+ deriving (Show, Eq, Generic)++instance FromJSON PatchSet where+ parseJSON = genericParseJSON $ aesonOption "patchSet"++data Change = Change+ { changeProject :: Text,+ changeBranch :: Text,+ changeSubject :: Text,+ changeUrl :: Text,+ changeOwner :: User+ }+ deriving (Show, Eq, Generic)++instance FromJSON Change where+ parseJSON = genericParseJSON $ aesonOption "change"++data Ref = Ref+ { refOldRev :: Text,+ refNewRev :: Text,+ refRefName :: Text,+ refProject :: Text+ }+ deriving (Show, Eq, Generic)++instance FromJSON Ref where+ parseJSON = genericParseJSON $ aesonOption "ref"++-------------------------------------------------------------------------------+-- Individual Event data types+-------------------------------------------------------------------------------+data AssigneeChanged = AssigneeChanged+ { assigneeChangedChange :: Change,+ assigneeChangedChanger :: User+ }+ deriving (Show, Eq, Generic)++data ChangeAbandoned = ChangeAbandoned+ { changeAbandonedProject :: Text,+ changeAbandonedAbandoner :: User,+ changeAbandonedChange :: Change,+ changeAbandonedPatchSet :: PatchSet+ }+ deriving (Show, Eq, Generic)++data ChangeDeleted = ChangeDeleted+ { changeDeletedChange :: Change,+ changeDeletedDeleter :: User+ }+ deriving (Show, Eq, Generic)++data ChangeMerged = ChangeMerged+ { changeMergedProject :: Text,+ changeMergedSubmitter :: User,+ changeMergedChange :: Change,+ changeMergedPatchSet :: PatchSet+ }+ deriving (Show, Eq, Generic)++data ChangeRestored = ChangeRestored+ { changeRestoredChange :: Change,+ changeRestoredPatchSet :: PatchSet,+ changeRestoredRestorer :: User,+ changeRestoredReason :: Text+ }+ deriving (Show, Eq, Generic)++data CommentAdded = CommentAdded+ { commentAddedChange :: Change,+ commentAddedPatchSet :: PatchSet,+ commentAddedAuthor :: User+ }+ deriving (Show, Eq, Generic)++data HashtagsChanged = HashtagsChanged+ { hashtagsChangedChange :: Change,+ hashtagsChangedEditor :: User+ }+ deriving (Show, Eq, Generic)++data ProjectCreated = ProjectCreated+ { projectCreatedProjectName :: Text,+ projectCreatedHeadName :: Text+ }+ deriving (Show, Eq, Generic)++data PatchsetCreated = PatchsetCreated+ { patchsetCreatedUploader :: User,+ patchsetCreatedChange :: Change,+ patchsetCreatedPatchSet :: PatchSet+ }+ deriving (Show, Eq, Generic)++data RefUpdated = RefUpdated+ { refUpdatedSubmitter :: User,+ refUpdatedRefUpdate :: Ref+ }+ deriving (Show, Eq, Generic)++data ReviewerAdded = ReviewerAdded+ { reviewerAddedChange :: Change,+ reviewerAddedPatchSet :: PatchSet,+ reviewerAddedAdder :: User+ }+ deriving (Show, Eq, Generic)++data ReviewerDeleted = ReviewerDeleted+ { reviewerDeletedChange :: Change,+ reviewerDeletedPatchSet :: PatchSet,+ reviewerDeletedRemover :: User+ }+ deriving (Show, Eq, Generic)++data TopicChanged = TopicChanged+ { topicChangedChange :: Change,+ topicChangedChanger :: User,+ topicChangedOldTopic :: Text+ }+ deriving (Show, Eq, Generic)++data WorkInProgressStateChanged = WorkInProgressStateChanged+ { workInProgressStateChangedChange :: Change,+ workInProgressStateChangedPatchSet :: PatchSet,+ workInProgressStateChangedChanger :: User+ }+ deriving (Show, Eq, Generic)++data PrivateStateChanged = PrivateStateChanged+ { privateStateChangedChange :: Change,+ privateStateChangedPatchSet :: PatchSet,+ privateStateChangedChanger :: User+ }+ deriving (Show, Eq, Generic)++data VoteDeleted = VoteDeleted+ { voteDeletedChange :: Change,+ voteDeletedPatchSet :: PatchSet,+ voteDeletedRemover :: User+ }+ deriving (Show, Eq, Generic)++-------------------------------------------------------------------------------+-- Events FromJSON instances+-------------------------------------------------------------------------------+instance FromJSON AssigneeChanged where+ parseJSON = genericParseJSON $ aesonOption "assigneeChanged"++instance FromJSON ChangeAbandoned where+ parseJSON = genericParseJSON $ aesonOption "changeAbandoned"++instance FromJSON ChangeDeleted where+ parseJSON = genericParseJSON $ aesonOption "changeDeleted"++instance FromJSON ChangeMerged where+ parseJSON = genericParseJSON $ aesonOption "changeMerged"++instance FromJSON ChangeRestored where+ parseJSON = genericParseJSON $ aesonOption "changeRestored"++instance FromJSON CommentAdded where+ parseJSON = genericParseJSON $ aesonOption "commentAdded"++instance FromJSON HashtagsChanged where+ parseJSON = genericParseJSON $ aesonOption "hashtagsChanged"++instance FromJSON ProjectCreated where+ parseJSON = genericParseJSON $ aesonOption "projectCreated"++instance FromJSON PatchsetCreated where+ parseJSON = genericParseJSON $ aesonOption "patchsetCreated"++instance FromJSON RefUpdated where+ parseJSON = genericParseJSON $ aesonOption "refUpdated"++instance FromJSON ReviewerAdded where+ parseJSON = genericParseJSON $ aesonOption "reviewerAdded"++instance FromJSON ReviewerDeleted where+ parseJSON = genericParseJSON $ aesonOption "reviewerDeleted"++instance FromJSON TopicChanged where+ parseJSON = genericParseJSON $ aesonOption "topicChanged"++instance FromJSON WorkInProgressStateChanged where+ parseJSON = genericParseJSON $ aesonOption "workInProgressStateChanged"++instance FromJSON PrivateStateChanged where+ parseJSON = genericParseJSON $ aesonOption "privateStateChanged"++instance FromJSON VoteDeleted where+ parseJSON = genericParseJSON $ aesonOption "voteDeleted"++-- | The event "type" data type+data EventType+ = AssigneeChangedEvent+ | ChangeAbandonedEvent+ | ChangeDeletedEvent+ | ChangeMergedEvent+ | ChangeRestoredEvent+ | CommentAddedEvent+ | DroppedOutputEvent+ | HashtagsChangedEvent+ | ProjectCreatedEvent+ | PatchsetCreatedEvent+ | RefUpdatedEvent+ | ReviewerAddedEvent+ | ReviewerDeletedEvent+ | TopicChangedEvent+ | WorkInProgressStateChangedEvent+ | PrivateStateChangedEvent+ | VoteDeletedEvent+ deriving (Show, Eq)++instance FromJSON EventType where+ parseJSON (String v) = case v of+ "assignee-changed" -> pure AssigneeChangedEvent+ "change-abandoned" -> pure ChangeAbandonedEvent+ "change-deleted" -> pure ChangeDeletedEvent+ "change-merged" -> pure ChangeMergedEvent+ "change-restored" -> pure ChangeRestoredEvent+ "comment-added" -> pure CommentAddedEvent+ "dropped-output" -> pure DroppedOutputEvent+ "hashtags-changed" -> pure HashtagsChangedEvent+ "project-created" -> pure ProjectCreatedEvent+ "patchset-created" -> pure PatchsetCreatedEvent+ "ref-updated" -> pure RefUpdatedEvent+ "reviewer-added" -> pure ReviewerAddedEvent+ "reviewer-deleted" -> pure ReviewerDeletedEvent+ "topic-changed" -> pure TopicChangedEvent+ "wip-state-changed" -> pure WorkInProgressStateChangedEvent+ "private-state-changed" -> pure PrivateStateChangedEvent+ "vote-deleted" -> pure VoteDeletedEvent+ _ -> mzero+ parseJSON _ = mzero++-- | The main event union data type+data Event+ = EventAssigneeChanged AssigneeChanged+ | EventChangeAbandoned ChangeAbandoned+ | EventChangeDeleted ChangeDeleted+ | EventChangeMerged ChangeMerged+ | EventChangeRestored ChangeRestored+ | EventCommentAdded CommentAdded+ | EventDroppedOutput+ | EventHashtagsChanged HashtagsChanged+ | EventProjectCreated ProjectCreated+ | EventPatchsetCreated PatchsetCreated+ | EventRefUpdated RefUpdated+ | EventReviewerAdded ReviewerAdded+ | EventReviewerDeleted ReviewerDeleted+ | EventTopicChanged TopicChanged+ | EventWorkInProgressStateChanged WorkInProgressStateChanged+ | EventPrivateStateChanged PrivateStateChanged+ | EventVoteDeleted VoteDeleted+ deriving (Show, Eq)++instance FromJSON Event where+ parseJSON o@(Object v) = do+ eType <- v .: "type"+ case eType of+ AssigneeChangedEvent -> EventAssigneeChanged <$> parseJSON o+ ChangeAbandonedEvent -> EventChangeAbandoned <$> parseJSON o+ ChangeDeletedEvent -> EventChangeDeleted <$> parseJSON o+ ChangeMergedEvent -> EventChangeMerged <$> parseJSON o+ ChangeRestoredEvent -> EventChangeRestored <$> parseJSON o+ CommentAddedEvent -> EventCommentAdded <$> parseJSON o+ DroppedOutputEvent -> pure EventDroppedOutput+ HashtagsChangedEvent -> EventHashtagsChanged <$> parseJSON o+ ProjectCreatedEvent -> EventProjectCreated <$> parseJSON o+ PatchsetCreatedEvent -> EventPatchsetCreated <$> parseJSON o+ RefUpdatedEvent -> EventRefUpdated <$> parseJSON o+ ReviewerAddedEvent -> EventReviewerAdded <$> parseJSON o+ ReviewerDeletedEvent -> EventReviewerDeleted <$> parseJSON o+ TopicChangedEvent -> EventTopicChanged <$> parseJSON o+ WorkInProgressStateChangedEvent -> EventWorkInProgressStateChanged <$> parseJSON o+ PrivateStateChangedEvent -> EventPrivateStateChanged <$> parseJSON o+ VoteDeletedEvent -> EventVoteDeleted <$> parseJSON o+ parseJSON _ = mzero++-------------------------------------------------------------------------------+-- Utilities function+-------------------------------------------------------------------------------++-- | Get the associated change+getChange :: Event -> Maybe Change+getChange event = case event of+ EventAssigneeChanged AssigneeChanged {..} -> Just assigneeChangedChange+ EventChangeAbandoned ChangeAbandoned {..} -> Just changeAbandonedChange+ EventChangeDeleted ChangeDeleted {..} -> Just changeDeletedChange+ EventChangeMerged ChangeMerged {..} -> Just changeMergedChange+ EventChangeRestored ChangeRestored {..} -> Just changeRestoredChange+ EventCommentAdded CommentAdded {..} -> Just commentAddedChange+ EventDroppedOutput -> Nothing+ EventHashtagsChanged HashtagsChanged {..} -> Just hashtagsChangedChange+ EventProjectCreated _ -> Nothing+ EventPatchsetCreated PatchsetCreated {..} -> Just patchsetCreatedChange+ EventRefUpdated _ -> Nothing+ EventReviewerAdded ReviewerAdded {..} -> Just reviewerAddedChange+ EventReviewerDeleted ReviewerDeleted {..} -> Just reviewerDeletedChange+ EventTopicChanged TopicChanged {..} -> Just topicChangedChange+ EventWorkInProgressStateChanged WorkInProgressStateChanged {..} -> Just workInProgressStateChangedChange+ EventPrivateStateChanged PrivateStateChanged {..} -> Just privateStateChangedChange+ EventVoteDeleted VoteDeleted {..} -> Just voteDeletedChange++-- | Get the associated author+getUser :: Event -> Maybe User+getUser event = case event of+ EventAssigneeChanged AssigneeChanged {..} -> Just assigneeChangedChanger+ EventChangeAbandoned ChangeAbandoned {..} -> Just changeAbandonedAbandoner+ EventChangeDeleted ChangeDeleted {..} -> Just changeDeletedDeleter+ EventChangeMerged ChangeMerged {..} -> Just changeMergedSubmitter+ EventChangeRestored ChangeRestored {..} -> Just changeRestoredRestorer+ EventCommentAdded CommentAdded {..} -> Just commentAddedAuthor+ EventDroppedOutput -> Nothing+ EventHashtagsChanged HashtagsChanged {..} -> Just hashtagsChangedEditor+ EventProjectCreated _ -> Nothing+ EventPatchsetCreated PatchsetCreated {..} -> Just patchsetCreatedUploader+ EventRefUpdated _ -> Nothing+ EventReviewerAdded ReviewerAdded {..} -> Just reviewerAddedAdder+ EventReviewerDeleted ReviewerDeleted {..} -> Just reviewerDeletedRemover+ EventTopicChanged TopicChanged {..} -> Just topicChangedChanger+ EventWorkInProgressStateChanged WorkInProgressStateChanged {..} -> Just workInProgressStateChangedChanger+ EventPrivateStateChanged PrivateStateChanged {..} -> Just privateStateChangedChanger+ EventVoteDeleted VoteDeleted {..} -> Just voteDeletedRemover++-- | Get the 'EventType' back from an 'Event'+getEventType :: Event -> EventType+getEventType event = case event of+ EventAssigneeChanged _ -> AssigneeChangedEvent+ EventChangeAbandoned _ -> ChangeAbandonedEvent+ EventChangeDeleted _ -> ChangeDeletedEvent+ EventChangeMerged _ -> ChangeMergedEvent+ EventChangeRestored _ -> ChangeRestoredEvent+ EventCommentAdded _ -> CommentAddedEvent+ EventDroppedOutput -> DroppedOutputEvent+ EventHashtagsChanged _ -> HashtagsChangedEvent+ EventProjectCreated _ -> ProjectCreatedEvent+ EventPatchsetCreated _ -> PatchsetCreatedEvent+ EventRefUpdated _ -> RefUpdatedEvent+ EventReviewerAdded _ -> ReviewerAddedEvent+ EventReviewerDeleted _ -> ReviewerDeletedEvent+ EventTopicChanged _ -> TopicChangedEvent+ EventWorkInProgressStateChanged _ -> WorkInProgressStateChangedEvent+ EventPrivateStateChanged _ -> PrivateStateChangedEvent+ EventVoteDeleted _ -> VoteDeletedEvent++-- | Get the text representation suitable for event subscription.+eventName :: EventType -> Text+eventName eventType = case eventType of+ AssigneeChangedEvent -> "assignee-changed"+ ChangeAbandonedEvent -> "change-abandoned"+ ChangeDeletedEvent -> "change-deleted"+ ChangeMergedEvent -> "change-merged"+ ChangeRestoredEvent -> "change-restored"+ CommentAddedEvent -> "comment-added"+ DroppedOutputEvent -> "dropped-output"+ HashtagsChangedEvent -> "hashtags-changed"+ ProjectCreatedEvent -> "project-created"+ PatchsetCreatedEvent -> "patchset-created"+ RefUpdatedEvent -> "ref-updated"+ ReviewerAddedEvent -> "reviewer-added"+ ReviewerDeletedEvent -> "reviewer-deleted"+ TopicChangedEvent -> "topic-changed"+ WorkInProgressStateChangedEvent -> "wip-state-changed"+ PrivateStateChangedEvent -> "private-state-changed"+ VoteDeletedEvent -> "vote-deleted"
+ test/Spec.hs view
@@ -0,0 +1,111 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++module Main (main) where++import Data.Aeson (decode, encode)+import Data.ByteString.Lazy (ByteString)+import qualified Data.ByteString.Lazy as BSL+import Data.Maybe (fromJust)+import Gerrit+import Gerrit.Data as Gerrit+import qualified Gerrit.Event as Event+import System.Directory (listDirectory)+import Test.Tasty+import Test.Tasty.HUnit++main :: IO ()+main =+ do+ files <- listDirectory "./test/data"+ dataFiles <- traverse (BSL.readFile . ("./test/data/" <>)) files+ withClient "http://example.com/r" Nothing $ \client ->+ defaultMain (tests (zip files dataFiles) client)++tests :: [(FilePath, ByteString)] -> GerritClient -> TestTree+tests dataFiles client = testGroup "Tests" $ [unitTests] <> encodingTests dataFiles client++unitTests :: TestTree+unitTests =+ testGroup+ "unitTests"+ [ testCase "queryString status" $+ assertEqual "Query string is valid" "status:new" (queryText (Status NEW))+ ]++encodingTests :: [(FilePath, ByteString)] -> GerritClient -> [TestTree]+encodingTests dataFiles client =+ [ testGroup+ "FromJSON"+ [ testCase "Test GerritChange.json" $+ assertBool "GerritChange is decoded" $+ isChange $ decode $ getRaw "GerritChange.json",+ testCase "Test EventPatchsetCreated.json" $+ assertBool "EventPatchsetCreated is decoded" $+ isEventPatchsetCreated $ decode $ getRaw "EventPatchsetCreated.json",+ testCase "Test EventChangeMerged.json" $+ assertBool "EventChangeMerged is decoded" $+ isChangeMergedEvent $ decode $ getRaw "EventChangeMerged.json",+ testCase "Test EventCommentAdded.json" $+ assertBool "EventCommentAdded is decoded" $+ isCommentEvent $ decode $ getRaw "EventCommentAdded.json",+ testCase "Test EventChangeAbandoned.json" $+ assertBool "EventChangeAbandoned is decoded" $+ isAbandonedEvent $ decode $ getRaw "EventChangeAbandoned.json",+ testCase "Test EventProjectCreated.json" $+ assertBool "EventProjectCreated is decoded" $+ isEventProjectCreated $ decode $ getRaw "EventProjectCreated.json",+ testCase "Test EventRefUpdated.json" $+ assertBool "EventRefUpdated is decoded" $+ isEventRefUpdated $ decode $ getRaw "EventRefUpdated.json",+ testCase "Test ReviewResult.json" $+ assertBool "ReviewResult is decoded" $+ isReviewResult $ decode $ getRaw "ReviewResult.json",+ testCase "Test changeUrl works" $+ assertEqual "Change url is correct" "http://example.com/r/18839" testGetUrl,+ testCase "Test hasLabel works" $+ assertBool "Label not found" testIsApproved+ ],+ testGroup+ "toJSON"+ [ testCase "Test ReviewInput.json" $+ assertEqual "Review is encoded" (getRaw "ReviewInput.json") (prettyEncode reviewInput)+ ]+ ]+ where+ prettyEncode obj = encode obj <> "\n"+ reviewInput = Gerrit.ReviewInput (Just "Thanks!") Nothing+ getRaw :: FilePath -> ByteString+ getRaw fp = fromJust $ lookup fp dataFiles+ isReviewResult :: Maybe ReviewResult -> Bool+ isReviewResult = \case+ Just _ -> True+ Nothing -> False+ isChangeMergedEvent = \case+ Just (Event.EventChangeMerged _) -> True+ _ -> False+ isEventPatchsetCreated = \case+ Just (Event.EventPatchsetCreated _) -> True+ _ -> False+ isCommentEvent = \case+ Just (Event.EventCommentAdded _) -> True+ _ -> False+ isAbandonedEvent = \case+ Just (Event.EventChangeAbandoned _) -> True+ _ -> False+ isEventProjectCreated = \case+ Just (Event.EventProjectCreated _) -> True+ _ -> False+ isEventRefUpdated = \case+ Just (Event.EventRefUpdated _) -> True+ _ -> False+ isChange :: Maybe GerritChange -> Bool+ isChange = \case+ Just _ -> True+ Nothing -> False+ testIsApproved = case decode (getRaw "GerritChange.json") of+ Just change -> hasLabel "Code-Review" 1 change+ Nothing -> False+ testGetUrl = case decode (getRaw "GerritChange.json") of+ Just change -> changeUrl client change+ Nothing -> "decode-failed"
+ test/data/EventChangeAbandoned.json view
@@ -0,0 +1,49 @@+{+ "abandoner": {+ "name": "Tristan de Cacqueray",+ "email": "tdecacqu@redhat.com",+ "username": "TristanCacqueray"+ },+ "reason": "Duplicated changeid",+ "patchSet": {+ "number": 1,+ "revision": "39ff07f7277ad4e85a94df1e130ec3e0dc55e9c7",+ "parents": ["b20838415c86314d73ff5bb5532b1a9a43d38043"],+ "ref": "refs/changes/16/21916/1",+ "uploader": {+ "name": "Tristan de Cacqueray",+ "email": "tdecacqu@redhat.com",+ "username": "TristanCacqueray"+ },+ "createdOn": 1622284534,+ "author": {+ "name": "Tristan de Cacqueray",+ "email": "tdecacqu@redhat.com",+ "username": "TristanCacqueray"+ },+ "kind": "REWORK",+ "sizeInsertions": 158,+ "sizeDeletions": 23+ },+ "change": {+ "project": "software-factory/gerritbot-matrix",+ "branch": "master",+ "id": "Ie795336f3b1814a7e3d65a33c391fe3f64472fac",+ "number": 21916,+ "subject": "Group related event to emit a single message",+ "owner": {+ "name": "Tristan de Cacqueray",+ "email": "tdecacqu@redhat.com",+ "username": "TristanCacqueray"+ },+ "url": "https://softwarefactory-project.io/r/c/software-factory/gerritbot-matrix/+/21916",+ "commitMessage": "Group related event to emit a single message\n\nFixes https://github.com/softwarefactory-project/gerritbot-matrix/issues/1\nChange-Id: If4b38b79a421b9e257bdf82077ab4c9801a3a447\n\nChange-Id: Ie795336f3b1814a7e3d65a33c391fe3f64472fac\n",+ "createdOn": 1622284534,+ "status": "ABANDONED"+ },+ "project": "software-factory/gerritbot-matrix",+ "refName": "refs/heads/master",+ "changeKey": { "id": "Ie795336f3b1814a7e3d65a33c391fe3f64472fac" },+ "type": "change-abandoned",+ "eventCreatedOn": 1622285180+}
+ test/data/EventChangeMerged.json view
@@ -0,0 +1,47 @@+{+ "submitter": { "name": "Zuul", "username": "zuul" },+ "newRev": "d7d5dc4a739aafcb0a3edfb758e15f878ad3292f",+ "patchSet": {+ "number": 2,+ "revision": "4cc63b82c3c0114d29b50e2593af21504aa10f57",+ "parents": ["e61b385d95d4a8969bc09ab9163b550da8f715a3"],+ "ref": "refs/changes/05/21905/2",+ "uploader": {+ "name": "Tristan de Cacqueray",+ "email": "tdecacqu@redhat.com",+ "username": "TristanCacqueray"+ },+ "createdOn": 1622229383,+ "author": {+ "name": "Tristan de Cacqueray",+ "email": "tdecacqu@redhat.com",+ "username": "TristanCacqueray"+ },+ "kind": "REWORK",+ "sizeInsertions": 201,+ "sizeDeletions": 1+ },+ "change": {+ "project": "software-factory/gerrit-haskell",+ "branch": "master",+ "id": "I7c926701b9e2b57b934917672bb1397851119ae0",+ "number": 21905,+ "subject": "Add Gerrit.Event data type",+ "owner": {+ "name": "Tristan de Cacqueray",+ "email": "tdecacqu@redhat.com",+ "username": "TristanCacqueray"+ },+ "url": "https://softwarefactory-project.io/r/c/software-factory/gerrit-haskell/+/21905",+ "commitMessage": "Add Gerrit.Event data type\n\nChange-Id: I7c926701b9e2b57b934917672bb1397851119ae0\n",+ "createdOn": 1622228651,+ "status": "MERGED"+ },+ "project": "software-factory/gerrit-haskell",+ "refName": "refs/heads/master",+ "changeKey": {+ "id": "I7c926701b9e2b57b934917672bb1397851119ae0"+ },+ "type": "change-merged",+ "eventCreatedOn": 1622233316+}
+ test/data/EventCommentAdded.json view
@@ -0,0 +1,70 @@+{+ "author": {+ "name": "Zuul CI",+ "email": "zuul@softwarefactory-project.io",+ "username": "zuul"+ },+ "approvals": [+ {+ "type": "Verified",+ "description": "Verified",+ "value": "0"+ },+ {+ "type": "Code-Review",+ "description": "Code-Review",+ "value": "0"+ },+ {+ "type": "Workflow",+ "description": "Workflow",+ "value": "0"+ }+ ],+ "comment": "Patch Set 3:\n\nStarting check jobs.",+ "patchSet": {+ "number": 3,+ "revision": "938356461cd9fcb470e195fd1ef1907c06db730a",+ "parents": [+ "e61b385d95d4a8969bc09ab9163b550da8f715a3"+ ],+ "ref": "refs/changes/05/21905/3",+ "uploader": {+ "name": "Tristan de Cacqueray",+ "email": "tdecacqu@redhat.com",+ "username": "TristanCacqueray"+ },+ "createdOn": 1622229429,+ "author": {+ "name": "Tristan de Cacqueray",+ "email": "tdecacqu@redhat.com",+ "username": "TristanCacqueray"+ },+ "kind": "REWORK",+ "sizeInsertions": 203,+ "sizeDeletions": 1+ },+ "change": {+ "project": "software-factory/gerrit-haskell",+ "branch": "master",+ "id": "I7c926701b9e2b57b934917672bb1397851119ae0",+ "number": 21905,+ "subject": "Add Gerrit.Event data type",+ "owner": {+ "name": "Tristan de Cacqueray",+ "email": "tdecacqu@redhat.com",+ "username": "TristanCacqueray"+ },+ "url": "https://softwarefactory-project.io/r/c/software-factory/gerrit-haskell/+/21905",+ "commitMessage": "Add Gerrit.Event data type\n\nChange-Id: I7c926701b9e2b57b934917672bb1397851119ae0\n",+ "createdOn": 1622228651,+ "status": "NEW"+ },+ "project": "software-factory/gerrit-haskell",+ "refName": "refs/heads/master",+ "changeKey": {+ "id": "I7c926701b9e2b57b934917672bb1397851119ae0"+ },+ "type": "comment-added",+ "eventCreatedOn": 1622229444+}
+ test/data/EventPatchsetCreated.json view
@@ -0,0 +1,50 @@+{+ "uploader": {+ "name": "Tristan de Cacqueray",+ "email": "tdecacqu@redhat.com",+ "username": "TristanCacqueray"+ },+ "patchSet": {+ "number": 2,+ "revision": "4cc63b82c3c0114d29b50e2593af21504aa10f57",+ "parents": ["e61b385d95d4a8969bc09ab9163b550da8f715a3"],+ "ref": "refs/changes/05/21905/2",+ "uploader": {+ "name": "Tristan de Cacqueray",+ "email": "tdecacqu@redhat.com",+ "username": "TristanCacqueray"+ },+ "createdOn": 1622229383,+ "author": {+ "name": "Tristan de Cacqueray",+ "email": "tdecacqu@redhat.com",+ "username": "TristanCacqueray"+ },+ "kind": "REWORK",+ "sizeInsertions": 201,+ "sizeDeletions": 1+ },+ "change": {+ "project": "software-factory/gerrit-haskell",+ "branch": "master",+ "id": "I7c926701b9e2b57b934917672bb1397851119ae0",+ "number": 21905,+ "subject": "Add Gerrit.Event data type",+ "owner": {+ "name": "Tristan de Cacqueray",+ "email": "tdecacqu@redhat.com",+ "username": "TristanCacqueray"+ },+ "url": "https://softwarefactory-project.io/r/c/software-factory/gerrit-haskell/+/21905",+ "commitMessage": "Add Gerrit.Event data type\n\nChange-Id: I7c926701b9e2b57b934917672bb1397851119ae0\n",+ "createdOn": 1622228651,+ "status": "NEW"+ },+ "project": "software-factory/gerrit-haskell",+ "refName": "refs/heads/master",+ "changeKey": {+ "id": "I7c926701b9e2b57b934917672bb1397851119ae0"+ },+ "type": "patchset-created",+ "eventCreatedOn": 1622229383+}
+ test/data/EventProjectCreated.json view
@@ -0,0 +1,6 @@+{+ "projectName": "software-factory/gerritbot-matrix",+ "headName": "refs/heads/master",+ "type": "project-created",+ "eventCreatedOn": 1622230530+}
+ test/data/EventRefUpdated.json view
@@ -0,0 +1,15 @@+{+ "submitter": {+ "name": "Tristan de Cacqueray",+ "email": "tdecacqu@redhat.com",+ "username": "TristanCacqueray"+ },+ "refUpdate": {+ "oldRev": "0000000000000000000000000000000000000000",+ "newRev": "aaf4e7f7437e010ba19ed259be6391a9a658dbaa",+ "refName": "refs/changes/05/21905/1",+ "project": "software-factory/gerrit-haskell"+ },+ "type": "ref-updated",+ "eventCreatedOn": 1622228651+}
+ test/data/GerritChange.json view
@@ -0,0 +1,84 @@+{+ "id": "software-factory%2Fgerrit-haskell~master~Ic6add806c49bccaf6e15d4ef1c62522d56eb9384",+ "project": "software-factory/gerrit-haskell",+ "branch": "master",+ "hashtags": [],+ "change_id": "Ic6add806c49bccaf6e15d4ef1c62522d56eb9384",+ "subject": "Initial summon",+ "status": "NEW",+ "created": "2020-07-15 19:39:55.000000000",+ "updated": "2020-07-15 23:01:14.000000000",+ "submit_type": "MERGE_IF_NECESSARY",+ "mergeable": true,+ "insertions": 295,+ "deletions": 0,+ "unresolved_comment_count": 0,+ "_number": 18839,+ "owner": {+ "_account_id": 7+ },+ "labels": {+ "Code-Review": {+ "all": [+ {+ "value": 1,+ "permitted_voting_range": {+ "min": -1,+ "max": 1+ },+ "_account_id": 458+ }+ ],+ "values": {+ "-2": "Do not submit",+ "-1": "I would prefer that you didn't submit this",+ " 0": "No score",+ "+1": "Looks good to me, but someone else must approve",+ "+2": "Looks good to me (core reviewer)"+ },+ "default_value": 0+ },+ "Verified": {+ "all": [+ {+ "value": 1,+ "date": "2020-07-17 13:02:15.000000000",+ "permitted_voting_range": {+ "min": -2,+ "max": 2+ },+ "_account_id": 458+ },+ {+ "_account_id": 42+ }+ ],+ "values": {+ "-2": "Fails",+ "-1": "Doesn't seem to work",+ " 0": "No score",+ "+1": "Works for me",+ "+2": "Verified"+ },+ "default_value": 0+ }+ },+ "current_revision": "155658085799464e8d198edb7c8bd62de9ec9ae6",+ "revisions": {+ "155658085799464e8d198edb7c8bd62de9ec9ae6": {+ "kind": "REWORK",+ "_number": 3,+ "created": "2020-07-15 23:01:02.000000000",+ "uploader": {+ "_account_id": 7+ },+ "ref": "refs/changes/39/18839/3",+ "fetch": {+ "anonymous http": {+ "url": "https://softwarefactory-project.io/r/software-factory/gerrit-haskell",+ "ref": "refs/changes/39/18839/3"+ }+ }+ }+ }+}
+ test/data/ReviewInput.json view
@@ -0,0 +1,1 @@+{"message":"Thanks!"}
+ test/data/ReviewResult.json view
@@ -0,0 +1,5 @@+{+ "labels": {+ "Code-Review": -1+ }+}