{"action":"created","comment":{"url":"https://api.github.com/repos/commercialhaskell/rio/pulls/comments/235340824","pull_request_review_id":177181506,"id":235340824,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIzNTM0MDgyNA==","diff_hunk":"@@ -0,0 +1,345 @@\n+{-# LANGUAGE CPP                      #-}\n+{-# LANGUAGE ForeignFunctionInterface #-}\n+{-# LANGUAGE NoImplicitPrelude        #-}\n+{-# LANGUAGE OverloadedStrings        #-}\n+module RIO.File\n+  (\n+    writeFileBinaryDurable\n+  , writeFileBinaryDurableAtomic\n+  , withFileDurable\n+  , withFileDurableAtomic\n+  , ensureFileDurable\n+  )\n+  where\n+\n+import           RIO.Prelude.Reexports\n+\n+#ifdef WINDOWS\n+import           RIO.Prelude.IO\n+\n+#else\n+\n+import           RIO.Directory          (doesFileExist)\n+import           RIO.ByteString         (hPut)\n+import           Data.Bits              ((.|.))\n+import           Data.Typeable          (cast)\n+import           Foreign.C              (CInt (..), throwErrnoIfMinus1,\n+                                         throwErrnoIfMinus1Retry)\n+import           GHC.IO.Device          (IODeviceType (RegularFile))\n+import qualified GHC.IO.Device          as Device\n+import qualified GHC.IO.FD              as FD\n+import qualified GHC.IO.Handle.FD       as HandleFD\n+import           System.Directory       (copyFile)\n+import           System.FilePath        (takeDirectory, takeFileName, (</>))\n+import           System.Posix.Internals (CFilePath, c_close, c_safe_open,\n+                                         withFilePath)\n+import           System.Posix.Types     (CMode (..), Fd (..))\n+\n+#if MIN_VERSION_base(4,9,0)\n+import qualified GHC.IO.Handle.Types    as HandleFD (Handle (..), Handle__ (..))\n+#endif\n+\n+\n+-- TODO: Add a ticket/pull request to export this symbols from\n+-- System.Internal.Posix\n+--\n+-- NOTE: System.Posix.Internal doesn't re-export this constants so we have to\n+-- recreate-them here\n+foreign import ccall unsafe \"HsBase.h __hscore_o_rdonly\" o_RDONLY :: CInt\n+foreign import ccall unsafe \"HsBase.h __hscore_o_wronly\" o_WRONLY :: CInt\n+foreign import ccall unsafe \"HsBase.h __hscore_o_rdwr\"   o_RDWR   :: CInt\n+foreign import ccall unsafe \"HsBase.h __hscore_o_append\" o_APPEND :: CInt\n+foreign import ccall unsafe \"HsBase.h __hscore_o_creat\"  o_CREAT  :: CInt\n+foreign import ccall unsafe \"HsBase.h __hscore_o_noctty\" o_NOCTTY :: CInt\n+\n+-- After here, we have our own imports\n+foreign import ccall safe \"fcntl.h openat\"\n+  c_safe_openat :: CInt -> CFilePath -> CInt -> CMode -> IO CInt\n+\n+foreign import ccall safe \"fcntl.h renameat\"\n+  c_safe_renameat :: CInt -> CFilePath -> CInt -> CFilePath -> IO CInt\n+\n+foreign import ccall safe \"unistd.h fsync\"\n+  c_safe_fsync :: CInt -> IO CInt\n+\n+std_flags, output_flags, read_flags, write_flags, rw_flags,\n+    append_flags :: CInt\n+std_flags    = o_NOCTTY\n+output_flags = std_flags    .|. o_CREAT\n+read_flags   = std_flags    .|. o_RDONLY\n+write_flags  = output_flags .|. o_WRONLY\n+rw_flags     = output_flags .|. o_RDWR\n+append_flags = write_flags  .|. o_APPEND\n+\n+ioModeToFlags :: IOMode -> CInt\n+ioModeToFlags iomode =\n+  case iomode of\n+    ReadMode      -> read_flags\n+    WriteMode     -> write_flags\n+    ReadWriteMode -> rw_flags\n+    AppendMode    -> append_flags\n+\n+openDir :: MonadIO m => FilePath -> m Fd\n+openDir fp\n+  -- TODO: Investigate what is the situation with Windows FS in regards to non_blocking\n+  -- NOTE: File operations _do not support_ non_blocking on various kernels, more\n+  -- info can be found here: https://ghc.haskell.org/trac/ghc/ticket/15153\n+ =\n+  liftIO $\n+  withFilePath fp $ \\cFp ->\n+    Fd <$>\n+    (throwErrnoIfMinus1Retry \"openDir\" $\n+     c_safe_open cFp (ioModeToFlags ReadMode) 0o660)\n+\n+-- NOTE: Not Async exception safe\n+openFileFromDir :: (MonadIO m) => Fd -> FilePath -> IOMode -> m Handle\n+openFileFromDir (Fd dirFd) fp iomode =\n+  liftIO $\n+  withFilePath fp $ \\f -> do\n+    fileFd <-\n+      throwErrnoIfMinus1Retry \"openFileFromDir\" $\n+      c_safe_openat dirFd f (ioModeToFlags iomode) 0o666\n+    (fD, fd_type) <-\n+      FD.mkFD\n+        fileFd\n+        iomode\n+        Nothing {- no stat -}\n+        False {- not a socket -}\n+        False {- non_blocking --}\n+         -- TODO: We want to investigate if we can do non-blocking here\n+       `catchAny` \\e -> do\n+        void (c_close fileFd)\n+        throwIO e\n+    -- we want to truncate() if this is an open in WriteMode, but only if the\n+    -- target is a RegularFile. ftruncate() fails on special files like\n+    -- /dev/null.\n+    when (iomode == WriteMode && fd_type == RegularFile) $ Device.setSize fD 0\n+    HandleFD.mkHandleFromFD fD fd_type fp iomode False Nothing `onException`\n+      (liftIO $ Device.close fD)\n+\n+-- | Opens a file using the openat C low-level API. This approach allows us to\n+-- get a file descriptor for the directory that contains the file, which we can\n+-- use later on to fsync the directory with.\n+--\n+-- @since 0.1.6\n+openFileDurable :: MonadIO m => FilePath -> IOMode -> m (Fd, Handle)\n+openFileDurable absFp iomode =  do\n+  let dir = takeDirectory absFp\n+      fp = takeFileName absFp\n+\n+  dirFd <- openDir dir\n+  fileHandle <- openFileFromDir dirFd fp iomode\n+  return (dirFd, fileHandle)\n+\n+-- | This sub-routine does the following tasks:\n+--\n+-- * It calls fsync and then closes the given handler (mapping to a temporal/backup filepath)\n+-- * It calls fsync and then closes the containing directory of the file\n+--\n+-- These steps guarante that the file changes are durable.\n+--\n+-- @since 0.1.6\n+closeFileDurable :: MonadIO m => Fd -> Handle -> m ()\n+closeFileDurable (Fd dirFd) h =\n+  liftIO $\n+  finally\n+    (do fileFd <- handleToFd h\n+        fsyncDescriptor \"File\" (FD.fdFD fileFd) `finally` hClose h\n+        fsyncDescriptor \"Directory\" dirFd)\n+    closeDirectory\n+  where\n+    fsyncDescriptor name fd =\n+      void $\n+      throwErrnoIfMinus1 (\"closeFileDurable - fsync(\" <> name <> \")\") $\n+      c_safe_fsync fd\n+    closeDirectory =\n+      void $\n+      throwErrnoIfMinus1Retry \"closeFileDurable - close(Directory)\" $\n+      c_close dirFd\n+\n+toTmpFilePath :: FilePath -> FilePath\n+toTmpFilePath filePath =\n+    dirPath </> tmpFilename\n+  where\n+    dirPath = takeDirectory filePath\n+    filename = takeFileName filePath\n+    tmpFilename = \".\" <> filename <> \".tmp\"\n+\n+\n+\n+handleToFd :: Handle -> IO FD.FD\n+#if MIN_VERSION_base(4,9,0)\n+handleToFd h =\n+  case h of\n+    HandleFD.FileHandle _ mv -> do\n+      HandleFD.Handle__{HandleFD.haDevice = dev} <- readMVar mv\n+      case cast dev of\n+        Just fd -> return fd\n+        Nothing -> error \"not a file handle\"\n+    HandleFD.DuplexHandle {} -> error \"not a file handle\"\n+# else\n+handleToFd = HandleFD.handleToFd\n+#endif\n+\n+-- | This sub-routine does the following tasks:\n+--\n+-- * It calls fsync and then closes the given handler (mapping to a temporal/backup filepath)\n+-- * It renames the file to the original path (using renameat)\n+-- * It calls fsync and then closes the containing directory of the file\n+--\n+-- These steps guarante that the file are durable, and that the backup mechanism\n+-- for catastrophic failure is discarded after no error is thrown.\n+--\n+-- @since 0.1.6\n+closeFileDurableAtomic ::\n+     MonadIO m => FilePath -> Fd -> Handle -> m ()\n+closeFileDurableAtomic filePath (Fd dirFd) fileHandler =\n+  liftIO $\n+  finally\n+    (withFilePath tmpFilePath $ \\tmpFp ->\n+       withFilePath filePath $ \\fp -> do\n+         fileFd <- handleToFd fileHandler\n+         fsyncDescriptor \"File\" (FD.fdFD fileFd)\n+           `finally` hClose fileHandler\n+         renameFile tmpFp fp\n+         fsyncDescriptor \"Directory\" dirFd)\n+    closeDirectory\n+  where\n+    tmpFilePath = toTmpFilePath filePath\n+    fsyncDescriptor name fd =\n+      void $\n+      throwErrnoIfMinus1 (\"closeFileDurableAtomic - fsync(\" <> name <> \")\") $\n+      c_safe_fsync fd\n+    renameFile tmpFp origFp =\n+      void $\n+      throwErrnoIfMinus1Retry \"closeFileDurableAtomic - renameFile\" $\n+      c_safe_renameat dirFd tmpFp dirFd origFp\n+    closeDirectory =\n+      void $\n+      throwErrnoIfMinus1Retry \"closeFileDurableAtomic - close(Directory)\" $\n+      c_close dirFd\n+\n+#endif\n+\n+-- | After a file is closed, it opens it again and executes fsync internally on\n+-- both the file and the directory that contains it.\n+--\n+-- [The effectiveness of calling this function is\n+-- debatable](https://stackoverflow.com/questions/37288453/calling-fsync2-after-close2/50158433#50158433),\n+-- as this relies on internal implementation details at the Kernel level that\n+-- might change. We argue that, despite this fact, calling this function may\n+-- bring benefits in terms of durability\n+--\n+-- @since 0.1.6\n+ensureFileDurable :: MonadUnliftIO m => FilePath -> m ()\n+ensureFileDurable absFp =\n+#if WINDOWS\n+  absFp `seq` return ()\n+#else\n+  bracket (openFileDurable absFp ReadMode)\n+          (uncurry closeFileDurable)\n+          (const $ return ())\n+#endif\n+\n+\n+-- | Similar to 'writeFileBinary', but it also ensures that changes executed to\n+-- the file are guaranteed to be durable. It internally uses fsync and makes\n+-- sure it synchronizes the file on disk.\n+--\n+-- @since 0.1.6\n+writeFileBinaryDurable :: MonadUnliftIO m => FilePath -> ByteString -> m ()\n+writeFileBinaryDurable absFp bytes =\n+#if WINDOWS\n+  writeFileBinary absFp bytes\n+#else\n+  withFileDurable absFp WriteMode (liftIO . (`hPut` bytes))\n+#endif\n+\n+-- | Similar to 'writeFileBinary', but it also guarantes that changes executed\n+-- to the file are durable, also, in case of failure, the modified file is never\n+-- going to get corrupted. It internally uses fsync and makes sure it\n+-- synchronizes the file on disk.\n+--\n+-- @since 0.1.6\n+writeFileBinaryDurableAtomic :: MonadUnliftIO m => FilePath -> ByteString -> m ()\n+writeFileBinaryDurableAtomic fp bytes =\n+#if WINDOWS\n+  writeFileBinary fp bytes\n+#else\n+  withFileDurableAtomic fp WriteMode (liftIO . (`hPut` bytes))\n+#endif\n+\n+-- | Opens a file with the following guarantees:\n+--\n+-- * Successfully closes the file in case of an asynchronous exception\n+--\n+-- * It reliably saves the file in the correct directory; including edge case\n+--   situations like a different device being mounted to the current directory,\n+--   or the current directory being renamed to some other name while the file is\n+--   being used.\n+--\n+-- * It ensures durability by executing fsync before close\n+--\n+-- @since 0.1.6\n+withFileDurable ::","path":"rio/src/RIO/File.hs","position":null,"original_position":284,"commit_id":"814d7e6e37575f492419f63f0a25b96554e8562d","original_commit_id":"1b2c757847a58dc67bbc91576994583c583d4269","user":{"login":"snoyberg","id":49415,"node_id":"MDQ6VXNlcjQ5NDE1","avatar_url":"https://avatars3.githubusercontent.com/u/49415?v=4","gravatar_id":"","url":"https://api.github.com/users/snoyberg","html_url":"https://github.com/snoyberg","followers_url":"https://api.github.com/users/snoyberg/followers","following_url":"https://api.github.com/users/snoyberg/following{/other_user}","gists_url":"https://api.github.com/users/snoyberg/gists{/gist_id}","starred_url":"https://api.github.com/users/snoyberg/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/snoyberg/subscriptions","organizations_url":"https://api.github.com/users/snoyberg/orgs","repos_url":"https://api.github.com/users/snoyberg/repos","events_url":"https://api.github.com/users/snoyberg/events{/privacy}","received_events_url":"https://api.github.com/users/snoyberg/received_events","type":"User","site_admin":false},"body":"Ugh, I messed up, it _should_ be `withBinaryFileDurable` for consistency, sorry.","created_at":"2018-11-21T10:56:47Z","updated_at":"2018-11-21T10:56:47Z","html_url":"https://github.com/commercialhaskell/rio/pull/138#discussion_r235340824","pull_request_url":"https://api.github.com/repos/commercialhaskell/rio/pulls/138","author_association":"COLLABORATOR","_links":{"self":{"href":"https://api.github.com/repos/commercialhaskell/rio/pulls/comments/235340824"},"html":{"href":"https://github.com/commercialhaskell/rio/pull/138#discussion_r235340824"},"pull_request":{"href":"https://api.github.com/repos/commercialhaskell/rio/pulls/138"}},"in_reply_to_id":234516312},"pull_request":{"url":"https://api.github.com/repos/commercialhaskell/rio/pulls/138","id":229092516,"node_id":"MDExOlB1bGxSZXF1ZXN0MjI5MDkyNTE2","html_url":"https://github.com/commercialhaskell/rio/pull/138","diff_url":"https://github.com/commercialhaskell/rio/pull/138.diff","patch_url":"https://github.com/commercialhaskell/rio/pull/138.patch","issue_url":"https://api.github.com/repos/commercialhaskell/rio/issues/138","number":138,"state":"open","locked":false,"title":"Add Durable Atomic File Writes to RIO","user":{"login":"roman","id":7335,"node_id":"MDQ6VXNlcjczMzU=","avatar_url":"https://avatars3.githubusercontent.com/u/7335?v=4","gravatar_id":"","url":"https://api.github.com/users/roman","html_url":"https://github.com/roman","followers_url":"https://api.github.com/users/roman/followers","following_url":"https://api.github.com/users/roman/following{/other_user}","gists_url":"https://api.github.com/users/roman/gists{/gist_id}","starred_url":"https://api.github.com/users/roman/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/roman/subscriptions","organizations_url":"https://api.github.com/users/roman/orgs","repos_url":"https://api.github.com/users/roman/repos","events_url":"https://api.github.com/users/roman/events{/privacy}","received_events_url":"https://api.github.com/users/roman/received_events","type":"User","site_admin":false},"body":"This pull request offers the following API:\r\n\r\n* `writeFileBinaryDurable`\r\n* `writeFileBinaryDurableAtomic`\r\n* `withFileDurable`\r\n* `withFileDurableAtomic`\r\n* `ensureFileDurable`\r\n\r\nThese APIs offer better guarantees in regards to file writing, as they use C APIs:\r\n\r\n* `openat`\r\n* `fsync`\r\n* `renameat`","created_at":"2018-11-07T16:39:27Z","updated_at":"2018-11-21T10:56:47Z","closed_at":null,"merged_at":null,"merge_commit_sha":"9e8a0573e1b192deeef304c726bf3d601eeff7d3","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/commercialhaskell/rio/pulls/138/commits","review_comments_url":"https://api.github.com/repos/commercialhaskell/rio/pulls/138/comments","review_comment_url":"https://api.github.com/repos/commercialhaskell/rio/pulls/comments{/number}","comments_url":"https://api.github.com/repos/commercialhaskell/rio/issues/138/comments","statuses_url":"https://api.github.com/repos/commercialhaskell/rio/statuses/814d7e6e37575f492419f63f0a25b96554e8562d","head":{"label":"roman:87-durable-atomic-writes","ref":"87-durable-atomic-writes","sha":"814d7e6e37575f492419f63f0a25b96554e8562d","user":{"login":"roman","id":7335,"node_id":"MDQ6VXNlcjczMzU=","avatar_url":"https://avatars3.githubusercontent.com/u/7335?v=4","gravatar_id":"","url":"https://api.github.com/users/roman","html_url":"https://github.com/roman","followers_url":"https://api.github.com/users/roman/followers","following_url":"https://api.github.com/users/roman/following{/other_user}","gists_url":"https://api.github.com/users/roman/gists{/gist_id}","starred_url":"https://api.github.com/users/roman/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/roman/subscriptions","organizations_url":"https://api.github.com/users/roman/orgs","repos_url":"https://api.github.com/users/roman/repos","events_url":"https://api.github.com/users/roman/events{/privacy}","received_events_url":"https://api.github.com/users/roman/received_events","type":"User","site_admin":false},"repo":{"id":120209676,"node_id":"MDEwOlJlcG9zaXRvcnkxMjAyMDk2NzY=","name":"rio","full_name":"roman/rio","private":false,"owner":{"login":"roman","id":7335,"node_id":"MDQ6VXNlcjczMzU=","avatar_url":"https://avatars3.githubusercontent.com/u/7335?v=4","gravatar_id":"","url":"https://api.github.com/users/roman","html_url":"https://github.com/roman","followers_url":"https://api.github.com/users/roman/followers","following_url":"https://api.github.com/users/roman/following{/other_user}","gists_url":"https://api.github.com/users/roman/gists{/gist_id}","starred_url":"https://api.github.com/users/roman/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/roman/subscriptions","organizations_url":"https://api.github.com/users/roman/orgs","repos_url":"https://api.github.com/users/roman/repos","events_url":"https://api.github.com/users/roman/events{/privacy}","received_events_url":"https://api.github.com/users/roman/received_events","type":"User","site_admin":false},"html_url":"https://github.com/roman/rio","description":"A standard library for Haskell","fork":true,"url":"https://api.github.com/repos/roman/rio","forks_url":"https://api.github.com/repos/roman/rio/forks","keys_url":"https://api.github.com/repos/roman/rio/keys{/key_id}","collaborators_url":"https://api.github.com/repos/roman/rio/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/roman/rio/teams","hooks_url":"https://api.github.com/repos/roman/rio/hooks","issue_events_url":"https://api.github.com/repos/roman/rio/issues/events{/number}","events_url":"https://api.github.com/repos/roman/rio/events","assignees_url":"https://api.github.com/repos/roman/rio/assignees{/user}","branches_url":"https://api.github.com/repos/roman/rio/branches{/branch}","tags_url":"https://api.github.com/repos/roman/rio/tags","blobs_url":"https://api.github.com/repos/roman/rio/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/roman/rio/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/roman/rio/git/refs{/sha}","trees_url":"https://api.github.com/repos/roman/rio/git/trees{/sha}","statuses_url":"https://api.github.com/repos/roman/rio/statuses/{sha}","languages_url":"https://api.github.com/repos/roman/rio/languages","stargazers_url":"https://api.github.com/repos/roman/rio/stargazers","contributors_url":"https://api.github.com/repos/roman/rio/contributors","subscribers_url":"https://api.github.com/repos/roman/rio/subscribers","subscription_url":"https://api.github.com/repos/roman/rio/subscription","commits_url":"https://api.github.com/repos/roman/rio/commits{/sha}","git_commits_url":"https://api.github.com/repos/roman/rio/git/commits{/sha}","comments_url":"https://api.github.com/repos/roman/rio/comments{/number}","issue_comment_url":"https://api.github.com/repos/roman/rio/issues/comments{/number}","contents_url":"https://api.github.com/repos/roman/rio/contents/{+path}","compare_url":"https://api.github.com/repos/roman/rio/compare/{base}...{head}","merges_url":"https://api.github.com/repos/roman/rio/merges","archive_url":"https://api.github.com/repos/roman/rio/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/roman/rio/downloads","issues_url":"https://api.github.com/repos/roman/rio/issues{/number}","pulls_url":"https://api.github.com/repos/roman/rio/pulls{/number}","milestones_url":"https://api.github.com/repos/roman/rio/milestones{/number}","notifications_url":"https://api.github.com/repos/roman/rio/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/roman/rio/labels{/name}","releases_url":"https://api.github.com/repos/roman/rio/releases{/id}","deployments_url":"https://api.github.com/repos/roman/rio/deployments","created_at":"2018-02-04T18:07:41Z","updated_at":"2018-06-06T15:12:31Z","pushed_at":"2018-11-19T19:54:11Z","git_url":"git://github.com/roman/rio.git","ssh_url":"git@github.com:roman/rio.git","clone_url":"https://github.com/roman/rio.git","svn_url":"https://github.com/roman/rio","homepage":"","size":280,"stargazers_count":0,"watchers_count":0,"language":"Haskell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":0,"license":null,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"commercialhaskell:master","ref":"master","sha":"ce8055b4ac80ef266e4b4da402971a34ca3e91f2","user":{"login":"commercialhaskell","id":10633049,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEwNjMzMDQ5","avatar_url":"https://avatars1.githubusercontent.com/u/10633049?v=4","gravatar_id":"","url":"https://api.github.com/users/commercialhaskell","html_url":"https://github.com/commercialhaskell","followers_url":"https://api.github.com/users/commercialhaskell/followers","following_url":"https://api.github.com/users/commercialhaskell/following{/other_user}","gists_url":"https://api.github.com/users/commercialhaskell/gists{/gist_id}","starred_url":"https://api.github.com/users/commercialhaskell/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/commercialhaskell/subscriptions","organizations_url":"https://api.github.com/users/commercialhaskell/orgs","repos_url":"https://api.github.com/users/commercialhaskell/repos","events_url":"https://api.github.com/users/commercialhaskell/events{/privacy}","received_events_url":"https://api.github.com/users/commercialhaskell/received_events","type":"Organization","site_admin":false},"repo":{"id":115809346,"node_id":"MDEwOlJlcG9zaXRvcnkxMTU4MDkzNDY=","name":"rio","full_name":"commercialhaskell/rio","private":false,"owner":{"login":"commercialhaskell","id":10633049,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEwNjMzMDQ5","avatar_url":"https://avatars1.githubusercontent.com/u/10633049?v=4","gravatar_id":"","url":"https://api.github.com/users/commercialhaskell","html_url":"https://github.com/commercialhaskell","followers_url":"https://api.github.com/users/commercialhaskell/followers","following_url":"https://api.github.com/users/commercialhaskell/following{/other_user}","gists_url":"https://api.github.com/users/commercialhaskell/gists{/gist_id}","starred_url":"https://api.github.com/users/commercialhaskell/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/commercialhaskell/subscriptions","organizations_url":"https://api.github.com/users/commercialhaskell/orgs","repos_url":"https://api.github.com/users/commercialhaskell/repos","events_url":"https://api.github.com/users/commercialhaskell/events{/privacy}","received_events_url":"https://api.github.com/users/commercialhaskell/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/commercialhaskell/rio","description":"A standard library for Haskell","fork":false,"url":"https://api.github.com/repos/commercialhaskell/rio","forks_url":"https://api.github.com/repos/commercialhaskell/rio/forks","keys_url":"https://api.github.com/repos/commercialhaskell/rio/keys{/key_id}","collaborators_url":"https://api.github.com/repos/commercialhaskell/rio/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/commercialhaskell/rio/teams","hooks_url":"https://api.github.com/repos/commercialhaskell/rio/hooks","issue_events_url":"https://api.github.com/repos/commercialhaskell/rio/issues/events{/number}","events_url":"https://api.github.com/repos/commercialhaskell/rio/events","assignees_url":"https://api.github.com/repos/commercialhaskell/rio/assignees{/user}","branches_url":"https://api.github.com/repos/commercialhaskell/rio/branches{/branch}","tags_url":"https://api.github.com/repos/commercialhaskell/rio/tags","blobs_url":"https://api.github.com/repos/commercialhaskell/rio/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/commercialhaskell/rio/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/commercialhaskell/rio/git/refs{/sha}","trees_url":"https://api.github.com/repos/commercialhaskell/rio/git/trees{/sha}","statuses_url":"https://api.github.com/repos/commercialhaskell/rio/statuses/{sha}","languages_url":"https://api.github.com/repos/commercialhaskell/rio/languages","stargazers_url":"https://api.github.com/repos/commercialhaskell/rio/stargazers","contributors_url":"https://api.github.com/repos/commercialhaskell/rio/contributors","subscribers_url":"https://api.github.com/repos/commercialhaskell/rio/subscribers","subscription_url":"https://api.github.com/repos/commercialhaskell/rio/subscription","commits_url":"https://api.github.com/repos/commercialhaskell/rio/commits{/sha}","git_commits_url":"https://api.github.com/repos/commercialhaskell/rio/git/commits{/sha}","comments_url":"https://api.github.com/repos/commercialhaskell/rio/comments{/number}","issue_comment_url":"https://api.github.com/repos/commercialhaskell/rio/issues/comments{/number}","contents_url":"https://api.github.com/repos/commercialhaskell/rio/contents/{+path}","compare_url":"https://api.github.com/repos/commercialhaskell/rio/compare/{base}...{head}","merges_url":"https://api.github.com/repos/commercialhaskell/rio/merges","archive_url":"https://api.github.com/repos/commercialhaskell/rio/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/commercialhaskell/rio/downloads","issues_url":"https://api.github.com/repos/commercialhaskell/rio/issues{/number}","pulls_url":"https://api.github.com/repos/commercialhaskell/rio/pulls{/number}","milestones_url":"https://api.github.com/repos/commercialhaskell/rio/milestones{/number}","notifications_url":"https://api.github.com/repos/commercialhaskell/rio/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/commercialhaskell/rio/labels{/name}","releases_url":"https://api.github.com/repos/commercialhaskell/rio/releases{/id}","deployments_url":"https://api.github.com/repos/commercialhaskell/rio/deployments","created_at":"2017-12-30T16:36:57Z","updated_at":"2018-11-20T19:20:33Z","pushed_at":"2018-11-19T19:54:12Z","git_url":"git://github.com/commercialhaskell/rio.git","ssh_url":"git@github.com:commercialhaskell/rio.git","clone_url":"https://github.com/commercialhaskell/rio.git","svn_url":"https://github.com/commercialhaskell/rio","homepage":"","size":262,"stargazers_count":349,"watchers_count":349,"language":"Haskell","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":25,"mirror_url":null,"archived":false,"open_issues_count":20,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":25,"open_issues":20,"watchers":349,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/commercialhaskell/rio/pulls/138"},"html":{"href":"https://github.com/commercialhaskell/rio/pull/138"},"issue":{"href":"https://api.github.com/repos/commercialhaskell/rio/issues/138"},"comments":{"href":"https://api.github.com/repos/commercialhaskell/rio/issues/138/comments"},"review_comments":{"href":"https://api.github.com/repos/commercialhaskell/rio/pulls/138/comments"},"review_comment":{"href":"https://api.github.com/repos/commercialhaskell/rio/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/commercialhaskell/rio/pulls/138/commits"},"statuses":{"href":"https://api.github.com/repos/commercialhaskell/rio/statuses/814d7e6e37575f492419f63f0a25b96554e8562d"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"snoyberg"}}