packages feed

gitlab-haskell 0.1.2 → 0.1.3

raw patch · 9 files changed

+609/−180 lines, 9 filesdep +directorydep +filepathdep +mossdep ~aesondep ~http-conduitPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: directory, filepath, moss, temporary, time

Dependency ranges changed: aeson, http-conduit

API changes (from Hackage documentation)

- GitLab.API.Groups: groupsWithName :: (MonadUnliftIO m, MonadIO m) => Text -> GitLab m [Group]
- GitLab.Types: [due_date] :: Milestone -> Maybe Text
- GitLab.Types: instance GHC.Classes.Eq GitLab.Types.Issue
- GitLab.Types: instance GHC.Classes.Eq GitLab.Types.Links
- GitLab.Types: instance GHC.Classes.Eq GitLab.Types.Milestone
- GitLab.Types: instance GHC.Classes.Eq GitLab.Types.Namespace
- GitLab.Types: instance GHC.Classes.Eq GitLab.Types.Owner
- GitLab.Types: instance GHC.Classes.Eq GitLab.Types.Permissions
- GitLab.Types: instance GHC.Classes.Eq GitLab.Types.Project
- GitLab.Types: instance GHC.Classes.Eq GitLab.Types.ProjectStats
- GitLab.Types: instance GHC.Classes.Eq GitLab.Types.TimeStats
- GitLab.Types: instance GHC.Classes.Eq GitLab.Types.User
+ GitLab.API.Groups: addUsersToGroup' :: (MonadIO m, MonadUnliftIO m) => Text -> AccessLevel -> [Text] -> GitLab m [Either Status Member]
+ GitLab.API.Groups: groupsWithNameOrPath :: (MonadUnliftIO m, MonadIO m) => Text -> GitLab m [Group]
+ GitLab.API.RepositoryFiles: repositoryFileBlob :: MonadIO m => Int -> Text -> GitLab m String
+ GitLab.API.Todos: todos :: MonadIO m => GitLab m [Todo]
+ GitLab.Types: TAApprovalRequired :: TodoAction
+ GitLab.Types: TAAssigned :: TodoAction
+ GitLab.Types: TABuildFailed :: TodoAction
+ GitLab.Types: TADirectlyAddressed :: TodoAction
+ GitLab.Types: TAMarked :: TodoAction
+ GitLab.Types: TAMentioned :: TodoAction
+ GitLab.Types: TAUnmergeable :: TodoAction
+ GitLab.Types: TSDone :: TodoState
+ GitLab.Types: TSPending :: TodoState
+ GitLab.Types: TTCommit :: CommitTodo -> TodoTarget
+ GitLab.Types: TTIssue :: Issue -> TodoTarget
+ GitLab.Types: TTMergeRequest :: MergeRequest -> TodoTarget
+ GitLab.Types: Todo :: Int -> TodoProject -> User -> TodoAction -> TodoTarget -> URL -> Text -> TodoState -> UTCTime -> Todo
+ GitLab.Types: [milestone_due_date] :: Milestone -> Maybe UTCTime
+ GitLab.Types: [milestone_group_id] :: Milestone -> Maybe Int
+ GitLab.Types: [milestone_web_url] :: Milestone -> URL
+ GitLab.Types: [todo_action_name] :: Todo -> TodoAction
+ GitLab.Types: [todo_author] :: Todo -> User
+ GitLab.Types: [todo_body] :: Todo -> Text
+ GitLab.Types: [todo_created_at] :: Todo -> UTCTime
+ GitLab.Types: [todo_id] :: Todo -> Int
+ GitLab.Types: [todo_project] :: Todo -> TodoProject
+ GitLab.Types: [todo_state] :: Todo -> TodoState
+ GitLab.Types: [todo_target] :: Todo -> TodoTarget
+ GitLab.Types: [todo_target_url] :: Todo -> URL
+ GitLab.Types: data Todo
+ GitLab.Types: data TodoAction
+ GitLab.Types: data TodoState
+ GitLab.Types: data TodoTarget
+ GitLab.Types: instance Data.Aeson.Types.FromJSON.FromJSON GitLab.Types.CommitTodo
+ GitLab.Types: instance Data.Aeson.Types.FromJSON.FromJSON GitLab.Types.MilestoneState
+ GitLab.Types: instance Data.Aeson.Types.FromJSON.FromJSON GitLab.Types.Todo
+ GitLab.Types: instance Data.Aeson.Types.FromJSON.FromJSON GitLab.Types.TodoAction
+ GitLab.Types: instance Data.Aeson.Types.FromJSON.FromJSON GitLab.Types.TodoProject
+ GitLab.Types: instance Data.Aeson.Types.FromJSON.FromJSON GitLab.Types.TodoState
+ GitLab.Types: instance GHC.Generics.Generic GitLab.Types.CommitTodo
+ GitLab.Types: instance GHC.Generics.Generic GitLab.Types.TodoProject
+ GitLab.Types: instance GHC.Show.Show GitLab.Types.CommitTodo
+ GitLab.Types: instance GHC.Show.Show GitLab.Types.MilestoneState
+ GitLab.Types: instance GHC.Show.Show GitLab.Types.Todo
+ GitLab.Types: instance GHC.Show.Show GitLab.Types.TodoAction
+ GitLab.Types: instance GHC.Show.Show GitLab.Types.TodoProject
+ GitLab.Types: instance GHC.Show.Show GitLab.Types.TodoState
+ GitLab.Types: instance GHC.Show.Show GitLab.Types.TodoTarget
+ GitLab.Types: type URL = Text
+ GitLab.WebRequests.GitLabWebCalls: gitlabReqText :: MonadIO m => Text -> GitLab m String
- GitLab.API.Groups: addAllUsersToGroup :: MonadIO m => Text -> AccessLevel -> GitLab m [Either Status Member]
+ GitLab.API.Groups: addAllUsersToGroup :: (MonadIO m, MonadUnliftIO m) => Text -> AccessLevel -> GitLab m [Either Status Member]
- GitLab.API.Groups: addUserToGroup :: MonadIO m => Text -> AccessLevel -> User -> GitLab m (Either Status Member)
+ GitLab.API.Groups: addUserToGroup :: (MonadIO m, MonadUnliftIO m) => Text -> AccessLevel -> User -> GitLab m (Either Status Member)
- GitLab.API.Groups: addUserToGroup' :: MonadIO m => Text -> AccessLevel -> Int -> GitLab m (Either Status Member)
+ GitLab.API.Groups: addUserToGroup' :: (MonadIO m, MonadUnliftIO m) => Text -> AccessLevel -> Int -> GitLab m (Either Status Member)
- GitLab.API.Groups: addUsersToGroup :: MonadIO m => Text -> AccessLevel -> [Text] -> GitLab m [Either Status Member]
+ GitLab.API.Groups: addUsersToGroup :: (MonadIO m, MonadUnliftIO m) => Text -> AccessLevel -> [User] -> GitLab m [Either Status Member]
- GitLab.Types: Milestone :: Int -> Text -> Text -> Maybe Text -> Int -> Maybe Text -> Text -> Int -> Text -> Milestone
+ GitLab.Types: Milestone :: Maybe Int -> Maybe Int -> Text -> MilestoneState -> Maybe UTCTime -> Int -> Maybe UTCTime -> Text -> Int -> UTCTime -> URL -> Milestone
- GitLab.Types: [milestone_created_at] :: Milestone -> Maybe Text
+ GitLab.Types: [milestone_created_at] :: Milestone -> Maybe UTCTime
- GitLab.Types: [milestone_project_id] :: Milestone -> Int
+ GitLab.Types: [milestone_project_id] :: Milestone -> Maybe Int
- GitLab.Types: [milestone_state] :: Milestone -> Text
+ GitLab.Types: [milestone_state] :: Milestone -> MilestoneState
- GitLab.Types: [milestone_updated_at] :: Milestone -> Text
+ GitLab.Types: [milestone_updated_at] :: Milestone -> UTCTime

Files

app/Main.hs view
@@ -4,6 +4,7 @@  import Control.Monad import Control.Monad.IO.Class+import Control.Monad.IO.Unlift import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.IO as T@@ -11,6 +12,12 @@ import Network.HTTP.Types.Status import Data.Semigroup ((<>)) import Options.Applicative+import qualified Data.ByteString.Char8 as C+import Stanford.Moss+import Data.Maybe+import System.FilePath.Posix+import System.IO.Temp+import System.Directory  main :: IO () main = processOptions =<< execParser opts@@ -24,12 +31,19 @@   Options   { gitlabUrl :: String   , gitlabToken :: String-  , addUsersFilename :: String+  , usersFilename :: String   , group :: String   , project :: String   , addReportersToGroup :: Bool   , addGroupToProject :: Bool   , isRegistered :: Bool+  , plagiarism :: Bool+  , mossUserId :: String+  , mossLang :: String+  , plagiarismProject :: String+  , plagiarismFilePaths :: String+  , parentUser :: String+  , projectLinks :: Bool   }   deriving (Show) @@ -45,10 +59,10 @@     <> value ""     <> help "GitLab access token" )   <*> strOption-  ( long "filename"-    <> metavar "filename"+  ( long "userFilename"+    <> metavar "userFilename"     <> value ""-    <> help "name of a CSV file" )+    <> help "name of a CSV file with user IDs" )   <*> strOption   ( long "group"     <> metavar "group"@@ -68,83 +82,192 @@   <*> switch   ( long "registered"     <> help "prints 'yes' or 'no' depending on whether a user exists on the GitLab server" )+  <*> switch+  ( long "plagiarism"+    <> help "use MOSS to detect plagiarism" )+  <*> strOption+  ( long "mossUser"+    <> metavar "mossUser"+    <> value ""+    <> help "MOSS user ID" )+  <*> strOption+  ( long "mossLang"+    <> metavar "mossLang"+    <> value ""+    <> help "languge to pass to MOSS" )+  <*> strOption+  ( long "plagiarismProject"+    <> metavar "plagiarismProject"+    <> value ""+    <> help "project to detect plagiarism in" )+  <*> strOption+  ( long "plagiarismFiles"+    <> metavar "plagiarismFiles"+    <> value ""+    <> help "comma separated list of plagiarism files" )+  <*> strOption+  ( long "parent"+    <> metavar "parent"+    <> value ""+    <> help "parent username of skeleton code" )+  <*> switch+  ( long "projectLinks"+    <> help "get the URL links for searched for projects" )  processOptions :: Options -> IO ()-processOptions (Options _url _tok "" _grp _proj True _addGroup _isRegistered) =-  return ()-processOptions (Options _url _tok _filename "" _proj True _addGroup _isRegistered) =-  putStrLn "you must specify a group name too"-processOptions (Options _gUrl _gToken _fname _groupName _ True True _isRegistered) =-  putStrLn "you can only choose one of adding users to group or adding group to projects"---- add reporters to a group-processOptions (Options gUrl gToken fname groupName _ True False _isRegistered) = do-  text <- T.readFile fname-  let usernames = T.splitOn "," text-  runGitLab-    (defaultGitLabServer-      { url = T.pack gUrl-      , token = T.pack gToken-      })-    (addUsersToGroupDbg (T.pack groupName) Reporter usernames)-    --- add group as a member to all projects with a given name, as Reporter-processOptions (Options gUrl gToken _ groupName projectName False True _isRegistered) = do-  when (null groupName || null projectName)-    (error "for --share-project you must specify a group name and a project name")-  runGitLab-    (defaultGitLabServer-      { url = T.pack gUrl-      , token = T.pack gToken-      })-    (do groups <- groupsWithName (T.pack groupName)-        void $ liftIO $ when (null groups)-          (error ("group not found: " ++ groupName))-        void $ liftIO $ when (length groups > 1)-          (error ("multiple groups found for: " ++ groupName))-        -- should only be 1 group with this head-        let grp = head groups-        projects <- projectsWithName (T.pack projectName)-        mapM_-          (\prj -> do-             result <- shareProjectWithGroup (group_id grp) (project_id prj) Reporter-             case result of-               Left st -> liftIO (putStrLn ("unable to share project " ++ show (project_id prj) ++ " with group " ++ show (group_id grp) ++ ". Reason: " ++ show st))-               Right _details -> liftIO (putStrLn ("Added group " ++ show (group_id grp) ++ " to project " ++ show (project_id prj) ++ " as a Reporter"))-          )-          projects-    )---- ask if users are registered for a given GitLab server-processOptions (Options _gUrl _gToken "" __groupName _ False False True) =-  putStrLn "--registered needs a filename with --filename"-processOptions (Options gUrl gToken fname _groupName _ False False True) = do-  text <- T.readFile fname-  let usernames = T.splitOn "," text-  runGitLab-    (defaultGitLabServer-      { url = T.pack gUrl-      , token = T.pack gToken-      })-    (mapM_-      (\usrName ->-          do res <- searchUser usrName-             -- empty list of returned users-             if null res-             then liftIO $ putStrLn (T.unpack usrName ++ ": no")-             else liftIO $ putStrLn (T.unpack usrName ++ ": yes")-      )-      usernames-    )+processOptions opts = go+  where+    go+      | addReportersToGroup opts && usersFilename opts == "" = return ()+      | addReportersToGroup opts && group opts == "" =+        putStrLn "you must specify a group name too"+      | addReportersToGroup opts && addGroupToProject opts =+        putStrLn+          "you can only choose one of adding users to group or adding group to projects"+         -- add reporters to a group+      | addReportersToGroup opts && usersFilename opts /= "" = do+        text <- T.readFile (usersFilename opts)+        let usernames = T.splitOn "," text+        runGitLab+          (defaultGitLabServer+           {url = T.pack (gitlabUrl opts), token = T.pack (gitlabToken opts)})+          (addUsersToGroupDbg (T.pack (group opts)) Reporter usernames)+      | not (addReportersToGroup opts) &&+          addGroupToProject opts && null (group opts) && null (project opts) =+        error+          "for --share-project you must specify a group name and a project name"+       -- add group as a member to all projects with a given name, as Reporter+      | not (addReportersToGroup opts) && addGroupToProject opts =+        runGitLab+          (defaultGitLabServer+           {url = T.pack (gitlabUrl opts), token = T.pack (gitlabToken opts)})+          (do groups <- groupsWithNameOrPath (T.pack (group opts))+              void $+                liftIO $+                when (null groups) (error ("group not found: " ++ group opts))+              void $+                liftIO $+                when+                  (length groups > 1)+                  (error ("multiple groups found for: " ++ group opts))+               -- should only be 1 group with this head+              let grp = head groups+              projects <- projectsWithName (T.pack (project opts))+              mapM_+                (\prj -> do+                   result <-+                     shareProjectWithGroup+                       (group_id grp)+                       (project_id prj)+                       Reporter+                   case result of+                     Left st ->+                       liftIO+                         (putStrLn+                            ("unable to share project " +++                             show (project_id prj) +++                             " with group " +++                             show (group_id grp) ++ ". Reason: " ++ show st))+                     Right _details ->+                       liftIO+                         (putStrLn+                            ("Added group " +++                             show (group_id grp) +++                             " to project " +++                             show (project_id prj) ++ " as a Reporter")))+                projects)+       -- ask if users are registered for a given GitLab server+      | not (addReportersToGroup opts) &&+          not (addGroupToProject opts) && isRegistered opts =+        putStrLn "--registered needs a filename with --filename"+      | not (addReportersToGroup opts) &&+          not (addGroupToProject opts) &&+          isRegistered opts && usersFilename opts == "" =+        putStrLn "--registered needs a filename with --filename"+      | not (addReportersToGroup opts) &&+          not (addGroupToProject opts) && isRegistered opts = do+        text <- T.readFile (usersFilename opts)+        let usernames = T.splitOn "," text+        runGitLab+          (defaultGitLabServer+           {url = T.pack (gitlabUrl opts), token = T.pack (gitlabToken opts)})+          (mapM_+             (\usrName -> do+                res <- searchUser usrName+                      -- empty list of returned users+                if null res+                  then liftIO $ putStrLn (T.unpack usrName ++ ": no")+                  else liftIO $ putStrLn (T.unpack usrName ++ ": yes"))+             usernames)+      | plagiarism opts &&+          ((mossUserId opts == "") ||+           (mossLang opts == "") ||+           (plagiarismFilePaths opts == "") ||+           (plagiarismProject opts == "") ||+           (parentUser opts == "") || (usersFilename opts == "")) =+        putStrLn+          "--plagiarism needs a MOSS user, MOSS language, filepath list and a parent user"+      | plagiarism opts = do+        usersData <- readFile (usersFilename opts)+        let usernames = T.splitOn "," (T.pack usersData)+        let files = T.splitOn "," (T.pack (plagiarismFilePaths opts))+        runGitLab+          (defaultGitLabServer+           {url = T.pack (gitlabUrl opts), token = T.pack (gitlabToken opts)})+          (do reports <-+                runMoss+                  files+                  usernames+                  (T.pack (parentUser opts))+                  (T.pack (plagiarismProject opts))+                  (T.pack (mossUserId opts))+                  (T.pack (mossLang opts))+              liftIO (putStrLn "MOSS reports:")+              liftIO $ mapM_ T.putStr reports)+      | projectLinks opts && null (project opts) =+        error "--projectLinks must also be used with --project"+      | projectLinks opts = do+        usersData <- readFile (usersFilename opts)+        let usernames = T.splitOn "," (T.pack usersData)+        runGitLab+          (defaultGitLabServer+           {url = T.pack (gitlabUrl opts), token = T.pack (gitlabToken opts)})+          (mapM_+            (\username -> do+                projects <-+                  projectsWithNameAndUser username (T.pack (project opts))+                case projects of+                  Nothing -> return ()+                  Just proj -> do+                    isPassing <- projectCISuccess proj+                    numCommits <- projectCommits proj+                    liftIO $+                      putStrLn+                      (T.unpack (project_web_url proj) <> " (" <>+                        (if isPassing+                         then "passing, "+                         else "failing, ")+                        <> show (length numCommits) <> " commits)"))+                usernames)+      | otherwise = error "combination of flags not recognised" -processOptions Options{} =-  error "combination of flags not recognised"+{- add users -}    addUsersToGroupDbg ::-  (MonadIO m) => Text -> AccessLevel -> [Text] -> GitLab m ()+  (MonadIO m, MonadUnliftIO m) => Text -> AccessLevel -> [Text] -> GitLab m () addUsersToGroupDbg groupName access usernames = do+  (users :: [User]) <-+    catMaybes <$>+    mapM (\username ->+            do maybeUser <- searchUser username+               case maybeUser of+                 Nothing -> do+                   liftIO $ putStrLn $ T.unpack username <> " not found on server."+                   return Nothing+                 u -> return u+         ) usernames   (results :: [Either Status Member]) <--    addUsersToGroup groupName access usernames+    addUsersToGroup groupName access users   liftIO $ mapM_     (\(result :: Either Status Member) ->        case result of@@ -154,3 +277,121 @@              ("Added to " ++               show groupName ++ ": " ++ show (member_username member)))     results++{- plagiarism detection -}++runMoss :: (MonadIO m, MonadUnliftIO m) => [Text] -> [Text] -> Text -> Text -> Text -> Text -> GitLab m [Text]+runMoss filepaths usernames skeletonUser projectName mossUserKey lang =+  mapM+    (\fpath -> do+       liftIO $ putStrLn ("Processing file " ++ T.unpack fpath ++ "\n")+       liftIO $+         putStrLn+           ("getting handed out code " +++            T.unpack skeletonUser ++ "/" ++ T.unpack projectName ++ "\n")+       skeletonCode <-+         fromJust <$> getCode fpath "master" skeletonUser projectName+       userSubmissions <-+         mapM+           (\usr -> do+              liftIO+                (putStrLn+                   ("getting " ++ T.unpack fpath ++ " from " +++                    T.unpack usr ++ "/" ++ T.unpack projectName))+              maybe_userCode <- getCode fpath "master" usr projectName+              case maybe_userCode of+                Nothing -> return Nothing+                Just userCode -> return (Just (usr, userCode)))+           usernames+       liftIO (putStrLn "sending job to moss")+       result <- moss fpath skeletonCode (catMaybes userSubmissions) mossUserKey lang+       liftIO $ putStrLn ("File " ++ T.unpack fpath ++ " processed.\n")+       return result+    )+    filepaths++moss :: (MonadIO m, MonadUnliftIO m) => Text -> Text -> [(Text,Text)] -> Text -> Text -> GitLab m Text+moss skeletonFilename skeletonData studentCode mossUserKey lang = do+  let language =+        case T.unpack lang of+          "C" -> C+          "CPP" -> CPP+          "Java" -> Java+          "CSharp" -> CSharp+          "Python" -> Python+          "VisualBasic" -> VisualBasic+          "Javascript" -> Javascript+          "FORTRAN" -> FORTRAN+          "ML" -> ML+          "Haskell" -> Haskell+          "Lisp" -> Lisp+          "Scheme" -> Scheme+          "Pascal" -> Pascal+          "Modula2" -> Modula2+          "Ada" -> Ada+          "Perl" -> Perl+          "TCL" -> TCL+          "Matlab" -> Matlab+          "VHDL" -> VHDL+          "Verilog" -> Verilog+          _ -> error "unrecognised MOSS language name"+  let ext =+        case T.unpack lang of+          "C" -> ".c"+          "CPP" -> ".cpp"+          "Java" -> ".java"+          "CSharp" -> ".cs"+          "Python" -> ".py"+          "VisualBasic" -> ".vb"+          "Javascript" -> ".js"+          "FORTRAN" -> ".f"+          "ML" -> ".ml"+          "Haskell" -> ".hs"+          "Lisp" -> ".lisp"+          "Scheme" -> ".scm"+          "Pascal" -> ".pas"+          "Modula2" -> ".mod"+          "Ada" -> ".ada"+          "Perl" -> ".pl"+          "TCL" -> ".tcl"+          "Matlab" -> ".m"+          "VHDL" -> ".vhdl"+          "Verilog" -> ".v"+          _ -> error "unrecognised MOSS language name"        +  let cfg :: MossCfg+      cfg = defaultMossCfg {+        mossLanguage = language+        , mossUser = C.pack (T.unpack mossUserKey)+        }+  tmpSkelFile <- liftIO $ writeSystemTempFile (takeFileName (T.unpack skeletonFilename)) (T.unpack skeletonData)+  userFiles <- mapM (\(userId,code) -> do+                        userFile <- liftIO $ writeSystemTempFile (T.unpack userId <> ext) (T.unpack code)+                        return (userFile,userId)+                    )+               studentCode+  liftIO $ withMoss cfg $ do+    addBaseFile "Skeleton" tmpSkelFile+    mapM_ (\(userFile,userId) ->+           addFile (T.unpack userId) userFile+          ) userFiles+    res <- fromJust <$> query (C.pack ("moss-" <> takeFileName (T.unpack skeletonFilename)))++    -- remove temporary files+    liftIO $ removeFile tmpSkelFile+    liftIO $ mapM_ (\(fileName,_) -> removeFile fileName) userFiles+    +    return (T.pack (C.unpack res))++getCode :: (MonadIO m, MonadUnliftIO m) => Text -> Text -> Text -> Text -> GitLab m (Maybe Text)+getCode filepath branch username projectName = do+    maybe_proj <- projectsWithNameAndUser username projectName+    case maybe_proj of+      Nothing -> return Nothing+      Just proj -> do+        maybe_file <- repositoryFiles proj filepath branch+        case maybe_file of+          Nothing -> error "repo file not found"+          Just repoFile -> do+            let blobHash = blob_id repoFile+            contents <- repositoryFileBlob (project_id proj) blobHash+            return (Just (T.pack contents))
gitlab-haskell.cabal view
@@ -1,14 +1,14 @@ cabal-version: 2.4 name:           gitlab-haskell category:       Git-version:        0.1.2+version:        0.1.3 synopsis:       A Haskell library for the GitLab web API description:   This Haskell library queries and updates the database of a GitLab instance using the GitLab web API: <https://docs.gitlab.com/ee/api/>   .   Unsurprisingly, GitLab hosts this Haskell library: <https://gitlab.com/robstewart57/gitlab-haskell>- -                ++ homepage:       https://gitlab.com/robstewart57/gitlab-haskell bug-reports:    https://gitlab.com/robstewart57/gitlab-haskell/issues author:         Rob Stewart@@ -35,6 +35,12 @@               , text               , http-types               , optparse-applicative+              , bytestring+              , moss+              , filepath+              , unliftio-core+              , temporary+              , directory   default-language: Haskell2010  library@@ -53,19 +59,21 @@                 , GitLab.API.Repositories                 , GitLab.API.MergeRequests                 , GitLab.API.RepositoryFiles+                , GitLab.API.Todos                 , GitLab.WebRequests.GitLabWebCalls   hs-source-dirs:                  src   build-depends:                 base >=4.7 && <5-              , http-conduit >= 2.3.4+              , http-conduit               , connection-              , aeson+              , aeson >= 1.4.4.0               , bytestring               , text               , http-types               , transformers               , unliftio               , unliftio-core+              , time   default-language: Haskell2010   ghc-options: -Wall
src/GitLab.hs view
@@ -1,7 +1,7 @@  {-| Module      : GitLab-Description : Contains the 'runGitLab' function to run GitLab actions +Description : Contains the 'runGitLab' function to run GitLab actions Copyright   : (c) Rob Stewart, Heriot-Watt University, 2019 License     : BSD3 Maintainer  : robstewart57@gmail.com@@ -22,6 +22,7 @@   , module GitLab.API.MergeRequests   , module GitLab.API.Repositories   , module GitLab.API.RepositoryFiles+  , module GitLab.API.Todos   ) where  import Control.Monad.IO.Class@@ -42,6 +43,7 @@ import GitLab.API.Projects import GitLab.API.Repositories import GitLab.API.RepositoryFiles+import GitLab.API.Todos import GitLab.API.Users import GitLab.Types 
src/GitLab/API/Groups.hs view
@@ -16,37 +16,43 @@ import Data.Maybe import Data.Text (Text) import qualified Data.Text as T+import qualified Data.Text.Encoding as T import Network.HTTP.Types.Status+import Network.HTTP.Types.URI+import qualified Data.ByteString.Char8 as C  import GitLab.API.Members import GitLab.API.Users import GitLab.Types import GitLab.WebRequests.GitLabWebCalls --- | gets groups with the given group name.+-- | gets groups with the given group name or path. ----- > projectsWithName "group1"-groupsWithName :: (MonadUnliftIO m, MonadIO m)+-- > projectsWithNameOrPath "group1"+groupsWithNameOrPath :: (MonadUnliftIO m, MonadIO m)   => Text -- ^ group name being searched for.   -> GitLab m [Group]-groupsWithName groupName =-  filter (\group -> groupName == group_path group) <$>+groupsWithNameOrPath groupName =+  filter (\group ->+            groupName == group_name group+            || groupName == group_path group+         ) <$>   gitlabWithAttrs "/groups" ("&search=" <> groupName)  -- | adds all registered users to a group. addAllUsersToGroup ::-     (MonadIO m)+     (MonadIO m, MonadUnliftIO m)   => Text -- ^ group name   -> AccessLevel -- ^ level of access granted   -> GitLab m [Either Status Member] addAllUsersToGroup groupName access = do   allRegisteredUsers <- allUsers   let allUserIds = map user_username allRegisteredUsers-  addUsersToGroup groupName access allUserIds+  addUsersToGroup' groupName access allUserIds  -- | adds a user to a group. addUserToGroup ::-     (MonadIO m)+     (MonadIO m, MonadUnliftIO m)   => Text -- ^ group name   -> AccessLevel -- ^ level of access granted   -> User -- ^ the user@@ -54,29 +60,47 @@ addUserToGroup groupName access usr =   addUserToGroup' groupName access (user_id usr) +-- | adds a user with a given user ID to a group.+addUserToGroup' ::+     (MonadIO m, MonadUnliftIO m)+  => Text -- ^ group name+  -> AccessLevel -- ^ level of access granted+  -> Int -- ^ user ID+  -> GitLab m (Either Status Member)+addUserToGroup' groupName access userId = do+  groups <- groupsWithNameOrPath groupName+  case groups of+    [] -> return (Left (mkStatus 404 (C.pack "cannot find group")))+    [grp] ->+      gitlabPost addr dataBody+      where+        dataBody :: Text+        dataBody =+          "user_id=" <> T.pack (show userId) <> "&access_level=" <>+          T.pack (show access)+        addr = "/groups/"+               <> T.decodeUtf8 (urlEncode False (T.encodeUtf8 (T.pack (show (group_id grp)))))+               <> "/members"+    (_:_) ->+      return (Left (mkStatus 404 (C.pack "too many groups found")))+ -- | adds a list of users to a group. addUsersToGroup ::-     (MonadIO m)+     (MonadIO m, MonadUnliftIO m)   => Text -- ^ group name   -> AccessLevel -- ^ level of access granted-  -> [Text] -- ^ list of usernames to be added to the group+  -> [User] -- ^ list of usernames to be added to the group   -> GitLab m [Either Status Member]-addUsersToGroup groupName access usernames = do-  users <- catMaybes <$> mapM searchUser usernames-  mapM (addUserToGroup' groupName access . user_id) users+addUsersToGroup groupName access =+  mapM (addUserToGroup groupName access) --- | adds a user with a given user ID to a group.-addUserToGroup' ::-     (MonadIO m)+-- | adds a list of users to a group.+addUsersToGroup' ::+     (MonadIO m, MonadUnliftIO m)   => Text -- ^ group name   -> AccessLevel -- ^ level of access granted-  -> Int -- ^ user ID-  -> GitLab m (Either Status Member)-addUserToGroup' groupName access userId = gitlabPost addr dataBody-  where-    dataBody :: Text-    dataBody =-      "user_id=" <> T.pack (show userId) <> "&access_level=" <>-      T.pack (show access)-    addr = "/groups/" <> groupName <> "/members"-+  -> [Text] -- ^ list of usernames to be added to the group+  -> GitLab m [Either Status Member]+addUsersToGroup' groupName access usernames = do+  users <- catMaybes <$> mapM searchUser usernames+  mapM (addUserToGroup' groupName access . user_id) users
src/GitLab/API/Projects.hs view
@@ -206,8 +206,8 @@  -- | gets all diffs in a project for a given commit SHA. projectDiffs :: (MonadIO m) => Project -> Text -> GitLab m [Diff]-projectDiffs proj commitSha =-  projectDiffs' (project_id proj) commitSha+projectDiffs proj =+  projectDiffs' (project_id proj)  -- | gets all diffs in a project for a given project ID, for a given -- commit SHA.
src/GitLab/API/RepositoryFiles.hs view
@@ -13,6 +13,8 @@ import Control.Monad.IO.Unlift import Data.Text (Text) import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Network.HTTP.Types.URI  import GitLab.Types import GitLab.WebRequests.GitLabWebCalls@@ -41,4 +43,21 @@       <> "/repository"       <> "/files"       <> "/"-      <> filePath+      <> T.decodeUtf8 (urlEncode False (T.encodeUtf8 filePath))+      -- <> filePath++-- | Get raw data for a given file blob hash.+repositoryFileBlob :: (MonadIO m)+  => Int -- ^ project ID+  -> Text -- ^ blob SHA+  -> GitLab m String+repositoryFileBlob projectId blobSha =+  gitlabReqText addr+  where+    addr =+      "/projects/"+      <> T.pack (show projectId)+      <> "/repository"+      <> "/blobs/"+      <> blobSha+      <> "/raw"
+ src/GitLab/API/Todos.hs view
@@ -0,0 +1,11 @@+{-# LANGUAGE OverloadedStrings #-}++module GitLab.API.Todos where++import Control.Monad.IO.Class+import GitLab.Types+import GitLab.WebRequests.GitLabWebCalls++-- | returns all pending todos for the user, as defined by the access token.+todos :: (MonadIO m) => GitLab m [Todo]+todos = gitlab "/todos"
src/GitLab/Types.hs view
@@ -1,5 +1,7 @@-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric       #-}+{-# LANGUAGE LambdaCase          #-}+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE ScopedTypeVariables #-}  {-| Module      : GitLab.Types@@ -38,18 +40,25 @@   , Branch(..)   , RepositoryFile(..)   , MergeRequest(..)+  , Todo(..)+  , TodoAction(..)+  , TodoTarget(..)+  , TodoState(..)+  , URL   ) where +import Control.Monad.Trans.Reader import Data.Aeson+import Data.Aeson.Types import Data.Text (Text)+import Data.Time.Clock import GHC.Generics import Network.HTTP.Conduit-import Control.Monad.Trans.Reader  -- | type synonym for all GitLab actions. type GitLab m a = ReaderT GitLabState m a --- | state used by GitLab actions, used internally. +-- | state used by GitLab actions, used internally. data GitLabState =   GitLabState   { serverCfg :: GitLabServerConfig@@ -97,7 +106,7 @@   , kind :: Text   , full_path :: Text   , parent_id :: Maybe Text-  } deriving (Generic, Show, Eq)+  } deriving (Generic, Show)  -- | links. data Links =@@ -109,7 +118,7 @@   , link_labels :: Text   , link_events :: Text   , members :: Text-  } deriving (Generic, Show, Eq)+  } deriving (Generic, Show)  -- | owners. data Owner =@@ -120,14 +129,14 @@   , state :: Text   , owner_avatar_url :: Maybe Text   , owner_web_url :: Text-  } deriving (Generic, Show, Eq)+  } deriving (Generic, Show)  -- | permissions. data Permissions =   Permissions   { project_access :: Maybe Object   , group_access :: Maybe Object-  } deriving (Generic, Show, Eq)+  } deriving (Generic, Show)  -- | projects. data Project =@@ -177,7 +186,7 @@   , merge_method:: Maybe Text   , permissions :: Maybe Permissions   , project_stats :: Maybe ProjectStats-  } deriving (Generic, Show, Eq)+  } deriving (Generic, Show)  data ProjectStats =   ProjectStats@@ -188,7 +197,7 @@   , lfs_objects_size :: Maybe Int   , job_artifacts_size :: Maybe Int   , packages_size :: Maybe Int-  } deriving (Generic, Show, Eq)+  } deriving (Generic, Show)  -- | registered users. data User =@@ -199,21 +208,33 @@   , user_state :: Text   , user_avatar_uri :: Maybe Text   , user_web_url :: Maybe Text-  } deriving (Generic, Show, Eq)+  } deriving (Generic, Show) --- | project milestones.+-- | milestones.+data MilestoneState = MSActive+                    | MSClosed+                    deriving (Show)+instance FromJSON MilestoneState where+  parseJSON (String "active") = return MSActive+  parseJSON (String "closed") = return MSClosed+  parseJSON x = unexpected x+ data Milestone =   Milestone-  { milestone_project_id :: Int+  { milestone_project_id :: Maybe Int+  , milestone_group_id :: Maybe Int   , milestone_description :: Text-  , milestone_state :: Text-  , due_date :: Maybe Text+  , milestone_state :: MilestoneState+  , milestone_due_date :: Maybe UTCTime   , milestone_iid :: Int-  , milestone_created_at :: Maybe Text+  , milestone_created_at :: Maybe UTCTime   , milestone_title :: Text   , milestone_id :: Int-  , milestone_updated_at :: Text-  } deriving (Generic, Show, Eq)+  , milestone_updated_at :: UTCTime+  , milestone_web_url :: URL+  } deriving (Generic, Show)+instance FromJSON Milestone where+  parseJSON = genericParseJSON (defaultOptions { fieldLabelModifier = drop 10 })  -- | time stats. data TimeStats =@@ -222,7 +243,7 @@   , total_time_spent :: Int   , humane_time_estimate :: Maybe Int   , human_total_time_spent :: Maybe Int-  } deriving (Generic, Show, Eq)+  } deriving (Generic, Show)  -- | project issues. data Issue =@@ -251,7 +272,7 @@   , weight :: Maybe Text -- Int?   , discussion_locked :: Maybe Bool   , time_stats :: TimeStats-  } deriving (Generic, Show, Eq)+  } deriving (Generic, Show)  -- | project pipelines data Pipeline =@@ -283,6 +304,15 @@   , commit_status :: Maybe Text   } deriving (Generic, Show) +-- | summary of a code commit for TODOs.+data CommitTodo =+  CommitTodo+  { todo_commit_id :: Text+  , todo_commit_short_id :: Text+  , todo_commit_created_at :: Text+  , todo_parent_ids :: Maybe [Text]+  } deriving (Generic, Show)+ -- | commit stats. data CommitStats =   Stats@@ -365,7 +395,6 @@   , group_parent_id :: Maybe Int   } deriving (Generic, Show) -   -- | response to sharing a project with a group. data GroupShare =   GroupShare@@ -446,61 +475,135 @@   , merge_request_approvals_before_merge :: Maybe Bool -- ?   } deriving (Generic, Show) ++data TodoAction = TAAssigned+                | TAMentioned+                | TABuildFailed+                | TAMarked+                | TAApprovalRequired+                | TAUnmergeable+                | TADirectlyAddressed+                deriving (Show)+instance FromJSON TodoAction where+  parseJSON (String "assigned") = return TAAssigned+  parseJSON (String "mentioned") = return TAMentioned+  parseJSON (String "build_failed") = return TABuildFailed+  parseJSON (String "marked") = return TAMarked+  parseJSON (String "approval_required") = return TAApprovalRequired+  parseJSON (String "unmergeable") = return TAUnmergeable+  parseJSON (String "directly_addressed") = return TADirectlyAddressed+  parseJSON x = unexpected x++data TodoTarget = TTIssue Issue+                | TTMergeRequest MergeRequest+                | TTCommit CommitTodo+                deriving (Show)++type URL = Text++data TodoState = TSPending+               | TSDone+               deriving (Show)+instance FromJSON TodoState where+  parseJSON (String "pending") = return TSPending+  parseJSON (String "done") = return TSDone+  parseJSON x = unexpected x++data TodoProject = TP { tp_id :: Int+                      , tp_description :: Text+                      , tp_name :: Text+                      , tp_name_with_namespace :: Text+                      , tp_path :: Text+                      , tp_path_with_namespace :: Text+                      , tp_created_at :: UTCTime }+                 deriving (Generic, Show)+instance FromJSON TodoProject where+  parseJSON = genericParseJSON (defaultOptions { fieldLabelModifier = drop 3 })++data Todo = Todo { todo_id :: Int+                 , todo_project :: TodoProject+                 , todo_author :: User+                 , todo_action_name :: TodoAction+                 , todo_target :: TodoTarget+                 , todo_target_url :: URL+                 , todo_body :: Text+                 , todo_state :: TodoState+                 , todo_created_at :: UTCTime }+          deriving (Show)+instance FromJSON Todo where+  parseJSON = withObject "Todo" $ \v -> Todo+    <$> v .: "id"+    <*> v .: "project"+    <*> v .: "author"+    <*> v .: "action_name"+    <*> (v .: "target_type" >>= \case+      "MergeRequest" -> TTMergeRequest <$> v .: "target"+      "Issue" -> TTIssue <$> v .: "target"+      "Commit" -> TTCommit <$> v .: "target"+      (_ :: Text) -> fail "" )+    <*> v .: "target_url"+    <*> v .: "body"+    <*> v .: "state"+    <*> v .: "created_at"+ ----------------------------- -- JSON GitLab parsers below -----------------------------  bodyNoPrefix :: String -> String-bodyNoPrefix "project_id" = "id"-bodyNoPrefix "namespace_id" = "id"-bodyNoPrefix "owner_id" = "id"-bodyNoPrefix "user_id" = "id"-bodyNoPrefix "repository_id" = "id"-bodyNoPrefix "project_name" = "name"-bodyNoPrefix "repository_name" = "name"-bodyNoPrefix "project_path" = "path"-bodyNoPrefix "namespace_name" = "name"-bodyNoPrefix "user_name" = "name"-bodyNoPrefix "namespace_path" = "path"-bodyNoPrefix "owner_name" = "name"-bodyNoPrefix "owner_avatar_url" = "avatar_url"-bodyNoPrefix "user_avatar_url" = "avatar_url"-bodyNoPrefix "owner_web_url" = "web_url"+bodyNoPrefix "commit_created_at" = "created_at"+bodyNoPrefix "commit_id" = "id"+bodyNoPrefix "commit_status" = "status"+bodyNoPrefix "todo_commit_id" = "id"+bodyNoPrefix "todo_commit_short_id" = "short_id"+bodyNoPrefix "todo_commit_created_at" = "created_at"+bodyNoPrefix "todo_parent_ids" = "parent_ids"+bodyNoPrefix "issue_author" = "author"+bodyNoPrefix "issue_created_at" = "created_at"+bodyNoPrefix "issue_description" = "description"+bodyNoPrefix "issue_due_date" = "due_date"+bodyNoPrefix "issue_id" = "id"+bodyNoPrefix "issue_labels" = "labels"+bodyNoPrefix "issue_project_id" = "project_id"+bodyNoPrefix "issue_state" = "state"+bodyNoPrefix "issue_title" = "title"+bodyNoPrefix "issue_web_url" = "web_url" bodyNoPrefix "job_web_url" = "web_url"-bodyNoPrefix "project_avatar_url" = "avatar_url"-bodyNoPrefix "project_web_url" = "web_url"+bodyNoPrefix "link_events" = "events"+bodyNoPrefix "link_labels" = "labels"+bodyNoPrefix "member_avatar_url" = "avatar_url" bodyNoPrefix "member_id" = "id" bodyNoPrefix "member_name" = "name"-bodyNoPrefix "member_avatar_url" = "avatar_url"-bodyNoPrefix "member_web_url" = "we_url"-bodyNoPrefix "user_web_url" = "we_url"+bodyNoPrefix "member_state" = "state" bodyNoPrefix "member_username" = "username"+bodyNoPrefix "member_web_url" = "we_url"+bodyNoPrefix "namespace_id" = "id"+bodyNoPrefix "namespace_name" = "name"+bodyNoPrefix "namespace_path" = "path"+bodyNoPrefix "owner_avatar_url" = "avatar_url"+bodyNoPrefix "owner_id" = "id"+bodyNoPrefix "owner_name" = "name" bodyNoPrefix "owner_username" = "username"-bodyNoPrefix "user_username" = "username"+bodyNoPrefix "owner_web_url" = "web_url" bodyNoPrefix "pipeline_id" = "id"-bodyNoPrefix "pipeline_web_url" = "web_url"-bodyNoPrefix "commit_id" = "id"-bodyNoPrefix "member_state" = "state"-bodyNoPrefix "user_state" = "state"-bodyNoPrefix "issue_state" = "state"-bodyNoPrefix "issue_description" = "description"-bodyNoPrefix "issue_author" = "author"-bodyNoPrefix "issue_project_id" = "project_id"-bodyNoPrefix "issue_id" = "id"-bodyNoPrefix "issue_title" = "title"-bodyNoPrefix "issue_due_date" = "due_date"-bodyNoPrefix "issue_web_url" = "web_url"-bodyNoPrefix "commit_status" = "status" bodyNoPrefix "pipeline_status" = "status"-bodyNoPrefix "issue_labels" = "labels"-bodyNoPrefix "link_labels" = "labels"-bodyNoPrefix "issue_created_at" = "created_at"-bodyNoPrefix "commit_created_at" = "created_at"+bodyNoPrefix "pipeline_web_url" = "web_url"+bodyNoPrefix "project_avatar_url" = "avatar_url" bodyNoPrefix "project_created_at" = "created_at"-bodyNoPrefix "milestone_created_at" = "created_at"-bodyNoPrefix "link_events" = "events"-bodyNoPrefix "repository_type" = "type"+bodyNoPrefix "project_id" = "id"+bodyNoPrefix "project_name" = "name"+bodyNoPrefix "project_path" = "path"+bodyNoPrefix "project_web_url" = "web_url"+bodyNoPrefix "repository_id" = "id"+bodyNoPrefix "repository_name" = "name" bodyNoPrefix "repository_path" = "path"+bodyNoPrefix "repository_type" = "type"+bodyNoPrefix "user_avatar_url" = "avatar_url"+bodyNoPrefix "user_id" = "id"+bodyNoPrefix "user_name" = "name"+bodyNoPrefix "user_state" = "state"+bodyNoPrefix "user_username" = "username"+bodyNoPrefix "user_web_url" = "we_url"  bodyNoPrefix "event_title" = "title" bodyNoPrefix "event_project_id" = "project_id"@@ -576,8 +679,6 @@ bodyNoPrefix "group_full_path" = "full_path" bodyNoPrefix "group_file_template_project_id" = "file_template_project_id" bodyNoPrefix "group_parent_id" = "parent_id"-- -- TODO field names for Issues data type bodyNoPrefix s = s @@ -586,22 +687,22 @@               (defaultOptions                { fieldLabelModifier = bodyNoPrefix }) -instance FromJSON Milestone where+instance FromJSON Issue where   parseJSON = genericParseJSON               (defaultOptions                { fieldLabelModifier = bodyNoPrefix }) -instance FromJSON Issue where+instance FromJSON User where   parseJSON = genericParseJSON               (defaultOptions                { fieldLabelModifier = bodyNoPrefix }) -instance FromJSON User where+instance FromJSON Commit where   parseJSON = genericParseJSON               (defaultOptions                { fieldLabelModifier = bodyNoPrefix }) -instance FromJSON Commit where+instance FromJSON CommitTodo where   parseJSON = genericParseJSON               (defaultOptions                { fieldLabelModifier = bodyNoPrefix })
src/GitLab/WebRequests/GitLabWebCalls.hs view
@@ -9,6 +9,7 @@   , gitlabOne   , gitlabWithAttrsOne   , gitlabPost+  , gitlabReqText   ) where  import Network.HTTP.Conduit@@ -22,6 +23,8 @@ import GitLab.Types import Control.Monad.Trans.Reader import Control.Monad.IO.Class+import Data.ByteString.Lazy.Char8 as C+import Network.HTTP.Types.URI  gitlabPost ::   (MonadIO m, FromJSON b) =>@@ -89,7 +92,7 @@             <> "?per_page=100"             <> "&page="             <> T.pack (show i)-            <> attrs+            <> T.decodeUtf8 (urlEncode False (T.encodeUtf8 attrs))       let request' = parseRequest_ (T.unpack url')           request = request'             { requestHeaders =@@ -104,8 +107,7 @@       else go (i+1) accum'  gitlabReqOne :: (MonadIO m, FromJSON a) => Text -> Text -> GitLab m (Maybe a)-gitlabReqOne urlPath attrs = do-  go+gitlabReqOne urlPath attrs = go   where     go = do       cfg <- serverCfg <$> ask@@ -125,6 +127,27 @@             }       res <- liftIO $ tryGitLab 0 request (retries cfg) manager Nothing       return (parseBSOne (responseBody res))++gitlabReqText :: (MonadIO m) => Text -> GitLab m String+gitlabReqText urlPath = go+  where+    go = do+      cfg <- serverCfg <$> ask+      manager <- httpManager <$> ask+      let url' =+               url cfg+            <> "/api/v4"+            <> urlPath+            <> "?per_page=100"+            <> "&page=1"+      let request' = parseRequest_ (T.unpack url')+          request = request'+            { requestHeaders =+              [("PRIVATE-TOKEN", T.encodeUtf8 (token cfg))]+            , responseTimeout = responseTimeoutMicro (timeout cfg)+            }+      res <- liftIO $ tryGitLab 0 request (retries cfg) manager Nothing+      return (C.unpack (responseBody res))  gitlab :: (MonadIO m, FromJSON a) => Text -> GitLab m [a] gitlab addr = gitlabReq addr ""