packages feed

gitlab-haskell 1.4.1.0 → 1.5.0.0

raw patch · 10 files changed

+250/−62 lines, 10 filesdep +exceptionsdep +safe-exceptionsPVP ok

version bump matches the API change (PVP)

Dependencies added: exceptions, safe-exceptions

API changes (from Hackage documentation)

+ GitLab.Types: GitLabMaxRetriesError :: HttpException -> GitLabError
+ GitLab.Types: [userAgent] :: GitLabServerConfig -> Text
+ GitLab.Types: instance Control.Monad.Catch.MonadCatch m => Control.Monad.Catch.MonadCatch (GitLab.Types.GitLabT m)
+ GitLab.Types: instance Control.Monad.Catch.MonadThrow m => Control.Monad.Catch.MonadThrow (GitLab.Types.GitLabT m)
- GitLab.API.Boards: boardList :: Project -> IssueBoard -> Int -> GitLab (Maybe BoardIssue)
+ GitLab.API.Boards: boardList :: Project -> IssueBoard -> Int -> GitLab (Either (Response ByteString) (Maybe BoardIssue))
- GitLab.Types: GitLabServerConfig :: Text -> AuthMethod -> Int -> Int -> Maybe DebugSystemHooks -> GitLabServerConfig
+ GitLab.Types: GitLabServerConfig :: Text -> AuthMethod -> Int -> Int -> Maybe DebugSystemHooks -> Text -> GitLabServerConfig

Files

+ data/api/merge-requests/single-merge-request2.json view
@@ -0,0 +1,125 @@+{+  "id": 18468,+  "iid": 16018,+  "project_id": 1,+  "title": "users-guide: Fix weird notation in \"Summary of stolen syntax\"",+  "description": "Fixes #26869",+  "state": "closed",+  "created_at": "2026-05-11T18:43:41.85Z",+  "updated_at": "2026-05-18T14:22:27.43Z",+  "merged_by": null,+  "merge_user": null,+  "merged_at": null,+  "closed_by": {+    "id": 94751,+    "username": "meritamen",+    "public_email": "meritamen@sdf.org",+    "name": "Mrjtjmn",+    "state": "active",+    "locked": false,+    "avatar_url": "https://gitlab.haskell.org/uploads/-/system/user/avatar/94751/avatar.png",+    "web_url": "https://gitlab.haskell.org/meritamen"+  },+  "closed_at": "2026-05-18T13:16:33.016Z",+  "target_branch": "master",+  "source_branch": "master",+  "user_notes_count": 6,+  "upvotes": 1,+  "downvotes": 0,+  "author": {+    "id": 94751,+    "username": "meritamen",+    "public_email": "meritamen@sdf.org",+    "name": "Mrjtjmn",+    "state": "active",+    "locked": false,+    "avatar_url": "https://gitlab.haskell.org/uploads/-/system/user/avatar/94751/avatar.png",+    "web_url": "https://gitlab.haskell.org/meritamen"+  },+  "assignees": [+    {+      "id": 94751,+      "username": "meritamen",+      "public_email": "meritamen@sdf.org",+      "name": "Mrjtjmn",+      "state": "active",+      "locked": false,+      "avatar_url": "https://gitlab.haskell.org/uploads/-/system/user/avatar/94751/avatar.png",+      "web_url": "https://gitlab.haskell.org/meritamen"+    }+  ],+  "assignee": {+    "id": 94751,+    "username": "meritamen",+    "public_email": "meritamen@sdf.org",+    "name": "Mrjtjmn",+    "state": "active",+    "locked": false,+    "avatar_url": "https://gitlab.haskell.org/uploads/-/system/user/avatar/94751/avatar.png",+    "web_url": "https://gitlab.haskell.org/meritamen"+  },+  "reviewers": [],+  "source_project_id": null,+  "target_project_id": 1,+  "labels": [+    "documentation",+    "no-changelog"+  ],+  "draft": false,+  "imported": false,+  "imported_from": "none",+  "work_in_progress": false,+  "milestone": null,+  "merge_when_pipeline_succeeds": false,+  "merge_status": "can_be_merged",+  "detailed_merge_status": "not_open",+  "merge_after": null,+  "sha": "8cfc3a9241d9a178437ea10804384cd7125cd4e9",+  "merge_commit_sha": null,+  "squash_commit_sha": null,+  "discussion_locked": null,+  "should_remove_source_branch": null,+  "force_remove_source_branch": null,+  "prepared_at": "2026-05-11T18:43:55.731Z",+  "allow_collaboration": false,+  "allow_maintainer_to_push": false,+  "reference": "!16018",+  "references": {+    "short": "!16018",+    "relative": "!16018",+    "full": "ghc/ghc!16018"+  },+  "web_url": "https://gitlab.haskell.org/ghc/ghc/-/merge_requests/16018",+  "time_stats": {+    "time_estimate": 0,+    "total_time_spent": 0,+    "human_time_estimate": null,+    "human_total_time_spent": null+  },+  "squash": false,+  "squash_on_merge": false,+  "task_completion_status": {+    "count": 0,+    "completed_count": 0+  },+  "has_conflicts": false,+  "blocking_discussions_resolved": true,+  "approvals_before_merge": null,+  "subscribed": false,+  "changes_count": "2",+  "latest_build_started_at": null,+  "latest_build_finished_at": null,+  "first_deployed_to_production_at": null,+  "pipeline": null,+  "head_pipeline": null,+  "diff_refs": {+    "base_sha": "1e60023bfac04260fa6f22a9a9bfb0774fb3ee4c",+    "head_sha": "8cfc3a9241d9a178437ea10804384cd7125cd4e9",+    "start_sha": "1e60023bfac04260fa6f22a9a9bfb0774fb3ee4c"+  },+  "merge_error": null,+  "first_contribution": true,+  "user": {+    "can_merge": false+  }+}
gitlab-haskell.cabal view
@@ -1,7 +1,7 @@ cabal-version: 3.0 name:           gitlab-haskell category:       Git-version:        1.4.1.0+version:        1.5.0.0 synopsis:       A Haskell library for the GitLab web API description:             This library lifts the GitLab REST API into Haskell: <https://docs.gitlab.com/ee/api/>@@ -155,6 +155,7 @@   , data/api/merge-requests/time-estimate-merge-request.json   , data/api/merge-requests/list-merge-request-pipelines.json   , data/api/merge-requests/single-merge-request.json+  , data/api/merge-requests/single-merge-request2.json   , data/api/merge-requests/merge-to-default-merge-path.json   , data/api/merge-requests/single-merge-request-diff-version.json   , data/api/merge-requests/comments-on-merge-requests.json@@ -195,6 +196,9 @@                 , GitLab.SystemHooks.Rules   other-modules:                   GitLab.WebRequests.GitLabWebCalls+                  Paths_gitlab_haskell+  autogen-modules:+                  Paths_gitlab_haskell   hs-source-dirs:                  src   build-depends:@@ -212,6 +216,8 @@               , unix-compat               , mtl >= 2.2.2               , data-default+              , safe-exceptions+              , exceptions   default-language: Haskell2010   ghc-options: -Wall                -- the following two for the stan static analysis tool
src/GitLab.hs view
@@ -142,5 +142,14 @@ runGitLabDbg (GitLabT action) = do   liftIO $ hSetBuffering stdout LineBuffering   manager <- liftIO $ newManager (mkManagerSettings def Nothing)-  let cfg = GitLabServerConfig {url = "", authMethod = AuthMethodToken "", retries = 1, delayBetweenRetries = 1, debugSystemHooks = Nothing}+  let+    cfg =+      GitLabServerConfig+        { url = ""+        , authMethod = AuthMethodToken ""+        , retries = 1+        , delayBetweenRetries = 1+        , debugSystemHooks = Nothing+        , userAgent = ""+        }   runExceptT (runReaderT action (GitLabState cfg manager))
src/GitLab/API/Boards.hs view
@@ -45,9 +45,9 @@   ) where +import Control.Monad.Except import qualified Data.ByteString.Lazy as BSL import Data.Default-import Data.Either import Data.Maybe import Data.Text (Text) import qualified Data.Text as T@@ -63,8 +63,9 @@   GitLab [IssueBoard] projectIssueBoards project = do   result <- projectIssueBoards' (project_id project)-  -- return an empty list if the repository could not be found.-  return (fromRight [] result)+  case result of+    Left resp -> throwError (httpResponseError "projectIssueBoards" resp)+    Right xs -> return xs  -- | returns all issue boards for a project given its project ID. projectIssueBoards' ::@@ -111,7 +112,9 @@   GitLab (Maybe IssueBoard) createIssueBoard project boardName = do   result <- createIssueBoard' (project_id project) boardName-  return (fromRight Nothing result)+  case result of+    Left resp -> throwError (httpResponseError "createIssueBoard" resp)+    Right xs -> return xs  -- | Creates a project issue board. createIssueBoard' ::@@ -182,8 +185,9 @@   GitLab [BoardIssue] projectBoardLists project board = do   result <- projectBoardLists' (project_id project) (board_id board)-  -- return an empty list if the repository could not be found.-  return (fromRight [] result)+  case result of+    Left resp -> throwError (httpResponseError "projectBoardLists" resp)+    Right xs -> return xs  -- | Get a list of the board’s lists. Does not include open and closed lists. projectBoardLists' ::@@ -207,11 +211,9 @@   IssueBoard ->   -- | list ID   Int ->-  GitLab (Maybe BoardIssue)+  GitLab (Either (Response BSL.ByteString) (Maybe BoardIssue)) boardList project board listId = do-  result <- boardList' (project_id project) (board_id board) listId-  -- return an empty list if the repository could not be found.-  return (fromRight Nothing result)+  boardList' (project_id project) (board_id board) listId  -- | Get a single board list. Does not include open and closed lists. boardList' ::@@ -240,8 +242,9 @@   GitLab (Maybe BoardIssue) createBoardList project board attrs = do   result <- createBoardList' (project_id project) (board_id board) attrs-  -- return an empty list if the repository could not be found.-  return (fromRight Nothing result)+  case result of+    Left resp -> throwError (httpResponseError "createBoardList" resp)+    Right xs -> return xs  -- | Creates a new issue board list. createBoardList' ::
src/GitLab/API/Branches.hs view
@@ -27,7 +27,6 @@  import Control.Monad.Except import qualified Data.ByteString.Lazy as BSL-import Data.Either import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Encoding as T@@ -106,8 +105,9 @@   GitLab (Maybe Branch) createRepositoryBranch project branchName branchFrom = do   result <- createRepositoryBranch' (project_id project) branchName branchFrom-  -- return an empty list if the repository could not be found.-  return (fromRight Nothing result)+  case result of+    Left resp -> throwError (httpResponseError "createRepositoryBranch" resp)+    Right br -> return br  -- | Create a new branch in the repository. createRepositoryBranch' ::
src/GitLab/API/Commits.hs view
@@ -59,7 +59,6 @@  import Control.Monad.Except import qualified Data.ByteString.Lazy as BSL-import Data.Either import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Encoding as T@@ -75,7 +74,9 @@ repoCommits prj = do   -- return an empty list if the repository could not be found.   result <- gitlabGetMany (commitsAddr (project_id prj)) [("with_stats", Just "true")]-  return (fromRight [] result)+  case result of+    Left resp -> throwError (httpResponseError "repoCommits" resp)+    Right xs -> return xs   where     commitsAddr :: Int -> Text     commitsAddr projId =@@ -189,7 +190,9 @@   GitLab (Maybe Commit) singleCommit project theHash = do   result <- gitlabGetOne (commitsAddr (project_id project)) []-  return (fromRight Nothing result)+  case result of+    Left resp -> throwError (httpResponseError "singleCommit" resp)+    Right c -> return c   where     commitsAddr :: Int -> Text     commitsAddr projId =@@ -216,7 +219,7 @@       [ ("branch", Just (T.encodeUtf8 branchName))       ]   case result of-    Left _ -> return Nothing+    Left resp -> throwError (httpResponseError "cherryPickCommit" resp)     Right x -> return x   where     commitsAddr :: Text@@ -244,7 +247,7 @@       [ ("branch", Just (T.encodeUtf8 branchName))       ]   case result of-    Left _ -> return Nothing+    Left resp -> throwError (httpResponseError "revertCommit" resp)     Right x -> return x   where     commitsAddr :: Text
src/GitLab/API/Pipelines.hs view
@@ -37,6 +37,7 @@   ) where +import Control.Monad.Except import qualified Data.ByteString.Lazy as BSL import Data.Default import Data.Maybe@@ -60,8 +61,8 @@ pipelines p attrs = do   result <- pipelines' (project_id p) attrs   case result of+    Left resp -> throwError (httpResponseError "pipelines" resp)     Right ps -> return (Just ps)-    Left _err -> return Nothing -- perhaps throwError (GitLabError ..)  -- | returns the pipelines for a project given its project ID. pipelines' ::
src/GitLab/API/Users.hs view
@@ -649,6 +649,6 @@       params = [("username", Just (T.encodeUtf8 username))]   result <- gitlabGetMany pathUser params   case result of-    Left _err -> return Nothing+    Left resp -> throwError (httpResponseError "searchUser" resp)     Right [] -> return Nothing     Right (x : _) -> return (Just x)
src/GitLab/Types.hs view
@@ -112,6 +112,7 @@   ) where +import Control.Monad.Catch (MonadCatch, MonadThrow) import Control.Monad.Except import Control.Monad.IO.Class import qualified Control.Monad.IO.Class as MIO@@ -124,9 +125,11 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Time.Clock+import Data.Version (showVersion) import Network.HTTP.Conduit import Network.HTTP.Types.Header (ResponseHeaders) import Network.HTTP.Types.Status (Status)+import Paths_gitlab_haskell (version)  -- | The monad in which the GitLab operations can be run. -- Contains the 'GitLabState' to run the requests with.@@ -141,9 +144,15 @@ instance (MIO.MonadIO m) => MIO.MonadIO (GitLabT m) where   liftIO = GitLabT . MIO.liftIO +deriving instance (MonadThrow m) => MonadThrow (GitLabT m)++deriving instance (MonadCatch m) => MonadCatch (GitLabT m)+ data GitLabError   = -- | A general error, e.g. a JSON parsing failure.     GitLabError Text+  | -- | A repeated HTTP attempt has exhausted the maximum retries+    GitLabMaxRetriesError HttpException   | -- | A non-success HTTP response from the GitLab server, including response     -- headers.     GitLabHttpError HttpResponseError@@ -178,7 +187,9 @@     -- | delay between HTTP request retries in seconds     delayBetweenRetries :: Int,     -- | write system hook events to files in the system temporary directory.-    debugSystemHooks :: Maybe DebugSystemHooks+    debugSystemHooks :: Maybe DebugSystemHooks,+    -- | The user-agent string+    userAgent :: Text   }  data DebugSystemHooks@@ -192,7 +203,15 @@  -- | default settings are https://gitlab.com with 'AuthMethodNone', the 'url' and 'authMethod' values may need to be overwritten. instance Default GitLabServerConfig where-  def = GitLabServerConfig {url = "https://gitlab.com", authMethod = AuthMethodNone, retries = 5, delayBetweenRetries = 1, debugSystemHooks = Nothing}+  def =+      GitLabServerConfig+        { url = "https://gitlab.com"+        , authMethod = AuthMethodNone+        , retries = 5+        , delayBetweenRetries = 1+        , debugSystemHooks = Nothing+        , userAgent = T.pack ("gitlab-haskell/" <> showVersion version)+        }  -- | personal access token, see <https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html> data AuthMethod
src/GitLab/WebRequests/GitLabWebCalls.hs view
@@ -18,13 +18,14 @@  import Control.Concurrent (threadDelay) import qualified Control.Exception as Exception+import Control.Exception.Safe (try) import Control.Monad.Except import Control.Monad.IO.Class import qualified Control.Monad.Reader as MR import Data.Aeson import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BSL-import Data.Maybe (isJust)+import Data.Maybe import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Encoding as T@@ -209,12 +210,22 @@           { method = httpMethod,             requestHeaders =               authHeaders-                <> [ ("content-type", contentType)+                <> [ ("content-type", contentType) ]+                <> [ ("User-Agent", T.encodeUtf8 agentString)+                   | let agentString = userAgent cfg+                   , not $ T.null agentString                    ],             requestBody = RequestBodyBS (renderQuery False contentParams)           }-  liftIO $ tryGitLab 0 request (retries cfg) (delayBetweenRetries cfg) manager Nothing+  tryGitLab 0 request (retries cfg) (delayBetweenRetries cfg) manager Nothing +-- Tricky corner, when to return "Nothing"?+-- Currently if:++-- * explicit 204++-- * 2xx code with no response body+ gitlabHTTPOne ::   (FromJSON a) =>   -- | HTTP method (PUT, POST, DELETE, GET)@@ -238,7 +249,15 @@       urlParams       contentParams   if successStatus (responseStatus response)-    then return (Right (parseOne (responseBody response)))+    then+      let code = responseStatus response+       in if code == status204+            || code >= status200 && code < status300 && BSL.null (responseBody response) -- Deliberately nothing+            -- Success status with no body+            then pure $ Right Nothing+            else pure $ case eitherDecode (responseBody response) of+              Left _ -> Left response+              Right r -> Right r     else return (Left response)  gitlabHTTPMany ::@@ -268,27 +287,32 @@           (urlParams <> [("per_page", Just "100"), ("page", Just (T.encodeUtf8 (T.pack (show pageNum))))])           contentParams       if successStatus (responseStatus response)-        then do-          case eitherDecode (responseBody response) of-            Left decodeErr -> do-              liftIO $-                hPutStrLn stderr $-                  "[gitlab-haskell] JSON decode error for "-                    <> T.unpack urlPath-                    <> " (page "-                    <> show pageNum-                    <> "): "-                    <> decodeErr-              liftIO $-                hPutStrLn stderr $-                  "[gitlab-haskell] Raw response body: "-                    <> show (BSL.take 2000 (responseBody response))-              return (Right accum)-            Right moreResults -> do-              let accum' = accum <> moreResults-              if hasNextPage response-                then go (pageNum + 1) accum'-                else return (Right accum')+        then+          let code = responseStatus response+           in if code == status204+                || code >= status200 && code < status300 && BSL.null (responseBody response) -- Deliberately nothing+                -- Success status with no body+                then pure $ Right []+                else case eitherDecode (responseBody response) of+                  Left decodeErr -> do+                    liftIO $+                      hPutStrLn stderr $+                        "[gitlab-haskell] JSON decode error for "+                          <> T.unpack urlPath+                          <> " (page "+                          <> show pageNum+                          <> "): "+                          <> decodeErr+                    liftIO $+                      hPutStrLn stderr $+                        "[gitlab-haskell] Raw response body: "+                          <> show (BSL.take 2000 (responseBody response))+                    return (Left response)+                  Right moreResults -> do+                    let accum' = accum <> moreResults+                    if hasNextPage response+                      then go (pageNum + 1) accum'+                      else return (Right accum')         else return (Left response)  hasNextPage :: Response a -> Bool@@ -319,15 +343,13 @@   Manager ->   -- | the exception to report if maximum retries met   Maybe HttpException ->-  IO (Response BSL.ByteString)+  GitLab (Response BSL.ByteString) tryGitLab i request maxRetries delaySeconds manager lastException-  | i == maxRetries = error (show lastException)-  | otherwise =-      httpLbs request manager-        `Exception.catch` \ex -> threadDelay delaySeconds >> tryGitLab (i + 1) request maxRetries delaySeconds manager (Just ex)--parseOne :: (FromJSON a) => BSL.ByteString -> Maybe a-parseOne bs =-  case eitherDecode bs of-    Left _err -> Nothing-    Right x -> Just x+  | i == maxRetries + 1 = throwError (GitLabMaxRetriesError (fromJust lastException))+  | otherwise = do+      tried <- try $ httpLbs request manager+      case tried of+        Left ex -> do+          liftIO (threadDelay delaySeconds)+          tryGitLab (i + 1) request maxRetries delaySeconds manager (Just ex)+        Right theResp -> return theResp