Compare commits

...

24 Commits

Author SHA1 Message Date
2ed99f5493 Prettify 2020-02-02 15:42:03 +01:00
2ba3d01b9c Update README 2020-02-02 15:39:30 +01:00
97041d7012 Add listing gists 2020-02-02 15:34:20 +01:00
f1186c3b11 Add gist functionality 2020-02-02 14:43:22 +01:00
ee11c131ef Cleanup 2020-02-01 16:43:46 +01:00
cfd1fc531b Speed up getForks by using search API 2020-02-01 16:23:47 +01:00
fe9578c9d6 Always show help message 2020-02-01 14:58:54 +01:00
160928e228 Update freeze file 2020-02-01 00:39:04 +01:00
bd07ee8022 Update README 2020-02-01 00:38:44 +01:00
1bc5ae70d9 Add descriptions to commands 2020-02-01 00:38:33 +01:00
f8dd4b9f95 Fix repo parser 2020-02-01 00:26:40 +01:00
20a9fcd210 Fix parser 2020-01-31 23:41:47 +01:00
b031456619 Fix build and warnings 2020-01-31 23:25:14 +01:00
68b5be3edf Comments 2020-01-31 23:22:37 +01:00
649efea81c Freeze cabal and index state 2020-01-31 23:19:39 +01:00
dc7604024f Nice table layout 2020-01-31 23:18:45 +01:00
d3cd8bf333 Force compiler ghc-8.6.5 2020-01-31 19:35:17 +01:00
595758d653 Nicer list-forks output 2020-01-31 19:34:05 +01:00
fadc0f84bf Fix error handling 2020-01-31 18:23:12 +01:00
6ab8d721b4 Smaller cleanup 2020-01-31 17:46:06 +01:00
7c7cb4cc60 Refactor 2020-01-31 17:38:29 +01:00
2359090203 Add list-forks command 2020-01-31 15:55:26 +01:00
013fa1ae66 Also output info to stderr 2020-01-31 15:54:01 +01:00
543c17ee12 Add freeze file 2020-01-30 23:14:09 +01:00
7 changed files with 814 additions and 120 deletions

View File

@@ -1,3 +1,40 @@
# ghup # ghup
Simple Github helper for myself. Simple Github helper for myself.
## Installation
If you don't have haskell GHC and cabal installed,
follow [ghcup](https://www.haskell.org/ghcup/) first.
Then issue:
```sh
$ cabal v2-install ghup
```
## Usage
First you need to set the github OAuth (no other method currently supported)
for API access:
```
ghup config --oauth "<your-github-token>"
```
Then follow the help page:
```
Usage: ghup COMMAND
Available options:
-h,--help Show this help text
Available commands:
fork Fork a repository
config Set ghup config (such as OAuth)
delete Delete a forked repository
list-forks List my forks
gistc Create gist
gistl List gists
```

View File

@@ -1,15 +1,34 @@
module Main where module Main where
import Control.Monad import Control.Error.Util
import Control.Monad.Except
import Control.Monad.Reader
import Data.ByteString ( ByteString ) import Data.ByteString ( ByteString )
import qualified Data.ByteString.UTF8 as UTF8
import Data.Dates ( getCurrentDateTime
, parseDate
, DateTime(..)
)
import Data.Functor ( (<&>) ) import Data.Functor ( (<&>) )
import Data.Maybe
import Data.Semigroup ( (<>) ) import Data.Semigroup ( (<>) )
import qualified Data.Text as T
import qualified Data.Text.Encoding as E
import Data.Time.Calendar
import Data.Time.Clock
import Data.Time.Format
import Data.Time.Format.ISO8601
import Data.Traversable
import GHup import GHup
import GitHub.Auth import GitHub.Auth
import GitHub.Data.Gists
import GitHub.Data.Repos
import GitHub.Data.URL
import HPath import HPath
import Options.Applicative import Options.Applicative
import System.Console.Pretty import System.Console.Pretty
import System.Exit import System.Exit
import Text.Layout.Table
@@ -21,6 +40,9 @@ data Command
= Fork ForkOptions = Fork ForkOptions
| Config ConfigOptions | Config ConfigOptions
| Del DelOptions | Del DelOptions
| ListForks ListForkOptions
| CreateGist CreateGistOptions
| ListGist ListGistOptions
data ForkOptions = ForkOptions data ForkOptions = ForkOptions
{ {
@@ -29,21 +51,85 @@ data ForkOptions = ForkOptions
, repoBasePath :: Maybe ByteString , repoBasePath :: Maybe ByteString
} }
data ListForkOptions = ListForkOptions
{
lSince :: Maybe ByteString
}
data ConfigOptions = ConfigOptions { data ConfigOptions = ConfigOptions {
oAuth :: ByteString oAuth :: ByteString
, bPath :: Maybe ByteString , bPath :: Maybe ByteString
} }
data DelOptions = DelOptions { data DelOptions = DelOptions {
del :: ByteString del :: ByteString
} }
data CreateGistOptions = CreateGistOptions {
input :: Input
, description :: Maybe ByteString
, private :: Bool
}
data Input
= FileInput [ByteString]
| StdInput
data ListGistOptions = ListGistOptions
{
lgSince :: Maybe ByteString
, lgDesc :: Bool
}
fileInput :: Parser Input
fileInput =
FileInput
<$> (some
(strOption
(long "file" <> short 'f' <> metavar "FILENAME" <> help
"Input one or more files"
)
)
)
stdInput :: Parser Input
stdInput = flag' StdInput (long "stdin" <> help "Read from stdin")
inputP :: Parser Input
inputP = fileInput <|> stdInput
opts :: Parser Command opts :: Parser Command
opts = subparser opts = subparser
( command "fork" (Fork <$> (info (forkOpts <**> helper) idm)) ( command
<> command "config" (Config <$> (info (configOpts <**> helper) idm)) "fork"
<> command "delete" (Del <$> (info (delOpts <**> helper) idm)) (Fork <$> (info (forkOpts <**> helper) (progDesc "Fork a repository")))
<> command
"config"
( Config
<$> (info (configOpts <**> helper)
(progDesc "Set ghup config (such as OAuth)")
)
)
<> command
"delete"
( Del
<$> (info (delOpts <**> helper)
(progDesc "Delete a forked repository")
)
)
<> command
"list-forks"
( ListForks
<$> (info (lForkOpts <**> helper) (progDesc "List my forks"))
)
<> command
"gistc"
(CreateGist <$> (info (cGistOpts <**> helper) (progDesc "Create gist"))
)
<> command
"gistl"
(ListGist <$> (info (lGistOpts <**> helper) (progDesc "List gists")))
) )
configOpts :: Parser ConfigOptions configOpts :: Parser ConfigOptions
@@ -84,20 +170,165 @@ delOpts :: Parser DelOptions
delOpts = DelOptions <$> strOption delOpts = DelOptions <$> strOption
(short 'r' <> long "repo" <> metavar "REPO" <> help "The REPO fork to delete") (short 'r' <> long "repo" <> metavar "REPO" <> help "The REPO fork to delete")
lForkOpts :: Parser ListForkOptions
lForkOpts = ListForkOptions <$> optional
(strOption
(short 's' <> long "since" <> metavar "SINCE" <> help
"The repository to fork"
)
)
cGistOpts :: Parser CreateGistOptions
cGistOpts =
CreateGistOptions
<$> inputP
<*> optional
(strOption
(short 'd' <> long "description" <> metavar "DESCRIPTION" <> help
"The description of the gist (optional)"
)
)
<*> switch
(short 'p' <> long "private" <> help
"Whether gist should be private (default: public)"
)
lGistOpts :: Parser ListGistOptions
lGistOpts =
ListGistOptions
<$> optional
(strOption
(short 's' <> long "since" <> metavar "SINCE" <> help
"The repository to fork"
)
)
<*> switch
(short 'd' <> long "descriptions" <> help
"Whether to show descriptions (default: False)"
)
main :: IO () main :: IO ()
main = do main = do
e <- execParser (info (opts <**> helper) idm) >>= \case -- wrapper to run effects with settings
Fork (ForkOptions {..}) -> do let
case repoBasePath of run e = do
Just rbp -> case parseAbs rbp of settings <-
Just p -> prepareRepoForPR' repo (Just p) newBranch exceptT
Nothing -> fail "Repo path must be absolute" (\_ ->
Nothing -> prepareRepoForPR' repo Nothing newBranch die
Config (ConfigOptions {..}) -> do . color Red
p <- maybe (pure Nothing) (fmap Just . parseAbs) bPath $ "Could not get settings, make sure to run 'ghup config' first"
writeSettings (Settings (OAuth oAuth) p) <&> Right )
Del (DelOptions {..}) -> deleteFork' del pure
$ getSettings
(flip runReaderT) settings . runExceptT . withExceptT show $ e
e <-
customExecParser (prefs showHelpOnError) (info (opts <**> helper) idm)
>>= \case
-- fork
Fork (ForkOptions {..}) -> run $ do
case repoBasePath of
Just rbp -> case parseAbs rbp of
Just p -> prepareRepoForPR' repo (Just p) newBranch
Nothing ->
liftIO $ die (color Red $ "Repo path must be absolute")
Nothing -> prepareRepoForPR' repo Nothing newBranch
-- config
Config (ConfigOptions {..}) -> do
p <- maybe (pure Nothing) (fmap Just . parseAbs) bPath
writeSettings (Settings (OAuth oAuth) p) <&> Right
-- delete
Del (DelOptions {..} ) -> run $ deleteFork' del
-- list-forks
ListForks (ListForkOptions {..}) -> run $ do
mtime <- parseSince lSince
forks <- withExceptT show $ getForks mtime
let
formatted =
gridString
[column expand left def def, column expand left def def]
$ fmap
(\Repo {..} ->
[ (T.unpack . getUrl $ repoHtmlUrl)
, formatShow (iso8601Format :: Format Day)
(utctDay $ fromJust repoUpdatedAt)
]
)
forks
liftIO $ putStrLn $ formatted
pure ()
-- gistc
CreateGist (CreateGistOptions {..}) -> run $ do
let desc = maybe T.empty E.decodeUtf8 description
public = not private
gist <- case input of
StdInput -> postGistStdin desc public
FileInput files -> do
files' <- for files $ \file -> do
let absPath = parseAbs file
let relPath = parseRel file
case (absPath, relPath) of
(Just a, _) -> pure $ AnyPath $ a
(_, Just a) -> pure $ AnyPath $ a
_ -> throwError (uError "Could not parse path")
postGistFiles files' desc public
liftIO $ putStrLn $ T.unpack $ getUrl $ gistHtmlUrl gist
-- gistl
ListGist (ListGistOptions {..}) -> run $ do
mtime <- parseSince lgSince
gists <- listGists mtime
let
formatted =
gridString
( [column expand left def def]
<> (if lgDesc then [column expand left def def] else [])
<> [column expand left def def]
)
$ fmap
(\Gist {..} ->
[(T.unpack . getUrl $ gistHtmlUrl)]
<> (if lgDesc
then
[ T.unpack $ fromMaybe (T.pack "(No desc)")
gistDescription
]
else []
)
<> [ formatShow (iso8601Format :: Format Day)
(utctDay gistUpdatedAt)
]
)
gists
liftIO $ putStrLn $ formatted
-- print error, if any
case e of case e of
Right () -> _info "success!" Right () -> pure ()
Left t -> die (color Red $ t) Left t -> die (color Red $ t)
where
parseSince lSince = do
liftIO $ case lSince of
Just t' -> do
dt <- getCurrentDateTime
let mt =
either (const Nothing) Just . parseDate dt . UTF8.toString $ t'
pure $ mt >>= \t ->
(parseTimeM
True
defaultTimeLocale
"%Y-%-m-%-d"
(show (year t) <> "-" <> show (month t) <> "-" <> show (day t)) :: Maybe
UTCTime
)
Nothing -> pure Nothing

View File

@@ -1 +1,5 @@
packages: ./ghup.cabal packages: ./ghup.cabal
with-compiler: ghc-8.6.5
index-state: 2020-01-31T21:11:24Z

174
cabal.project.freeze Normal file
View File

@@ -0,0 +1,174 @@
constraints: any.Cabal ==3.0.0.0,
Cabal -bundled-binary-generic,
any.IfElse ==0.85,
any.StateVar ==1.2,
any.abstract-deque ==0.3,
abstract-deque -usecas,
any.adjunctions ==4.4,
any.aeson ==1.4.6.0,
aeson -bytestring-builder -cffi -developer -fast,
any.ansi-terminal ==0.10.2,
ansi-terminal -example,
any.ansi-wl-pprint ==0.6.9,
ansi-wl-pprint -example,
any.array ==0.5.3.0,
any.asn1-encoding ==0.9.6,
any.asn1-parse ==0.9.5,
any.asn1-types ==0.3.3,
any.async ==2.2.2,
async -bench,
any.atomic-primops ==0.8.3,
atomic-primops -debug,
any.attoparsec ==0.13.2.3,
attoparsec -developer,
any.attoparsec-iso8601 ==1.0.1.0,
attoparsec-iso8601 -developer -fast,
any.base ==4.12.0.0,
any.base-compat ==0.11.1,
any.base-orphans ==0.8.2,
any.base-unicode-symbols ==0.2.4.2,
base-unicode-symbols +base-4-8 -old-base,
any.base16-bytestring ==0.1.1.6,
any.basement ==0.0.11,
any.bifunctors ==5.5.7,
bifunctors +semigroups +tagged,
any.binary ==0.8.6.0,
any.binary-instances ==1,
any.binary-orphans ==1.0.1,
any.blaze-builder ==0.4.1.0,
any.bytestring ==0.10.8.2,
any.bytestring-conversion ==0.3.1,
any.cabal-doctest ==1.0.8,
any.case-insensitive ==1.2.1.0,
any.cereal ==0.5.8.1,
cereal -bytestring-builder,
any.colour ==2.3.5,
any.comonad ==5.0.6,
comonad +containers +distributive +test-doctests,
any.connection ==0.3.1,
any.containers ==0.6.0.1,
any.contravariant ==1.5.2,
contravariant +semigroups +statevar +tagged,
any.cookie ==0.4.5,
any.cryptohash-sha1 ==0.11.100.1,
any.cryptonite ==0.26,
cryptonite -check_alignment +integer-gmp -old_toolchain_inliner +support_aesni +support_deepseq -support_pclmuldq +support_rdrand -support_sse,
any.data-default-class ==0.1.2.0,
any.data-default-instances-base ==0.1.0.1,
any.dates ==0.2.3.0,
any.deepseq ==1.4.4.0,
any.deepseq-generics ==0.2.0.0,
any.directory ==1.3.6.0,
any.distributive ==0.6.1,
distributive +semigroups +tagged,
any.dlist ==0.8.0.7,
any.double-conversion ==2.0.2.0,
double-conversion -developer,
any.errors ==2.3.0,
any.exceptions ==0.10.4,
exceptions +transformers-0-4,
any.filepath ==1.4.2.1,
any.free ==5.1.3,
any.ghc-boot-th ==8.6.5,
any.ghc-prim ==0.5.3,
any.github ==0.24,
github -openssl,
any.hashable ==1.3.0.0,
hashable -examples +integer-gmp +sse2 -sse41,
any.heaps ==0.3.6.1,
any.hourglass ==0.2.12,
any.hpath ==0.11.0,
any.hpath-directory ==0.13.1,
any.hpath-filepath ==0.10.4,
any.hpath-io ==0.13.1,
any.hpath-posix ==0.13.0,
any.hsc2hs ==0.68.6,
hsc2hs -in-ghc-tree,
any.http-api-data ==0.4.1.1,
http-api-data -use-text-show,
any.http-client ==0.6.4,
http-client +network-uri,
any.http-client-tls ==0.3.5.3,
any.http-link-header ==1.0.3.1,
any.http-types ==0.12.3,
any.integer-gmp ==1.0.2.0,
any.integer-logarithms ==1.0.3,
integer-logarithms -check-bounds +integer-gmp,
any.invariant ==0.5.3,
any.iso8601-time ==0.1.5,
iso8601-time +new-time,
any.kan-extensions ==5.2,
any.keys ==3.12.3,
any.lockfree-queue ==0.2.3.1,
any.memory ==0.15.0,
memory +support_basement +support_bytestring +support_deepseq +support_foundation,
any.mime-types ==0.1.0.9,
any.monad-control ==1.0.2.3,
any.mtl ==2.2.2,
any.network ==3.1.1.1,
any.network-uri ==2.6.2.0,
any.optparse-applicative ==0.15.1.0,
any.parsec ==3.1.13.0,
any.pem ==0.2.4,
any.pointed ==5.0.1,
pointed +comonad +containers +kan-extensions +semigroupoids +semigroups +stm +tagged +transformers +unordered-containers,
any.pretty ==1.1.3.6,
any.pretty-terminal ==0.1.0.0,
any.primitive ==0.7.0.0,
any.process ==1.6.7.0,
any.profunctors ==5.5.1,
any.random ==1.1,
any.rts ==1.0,
any.safe ==0.3.18,
any.safe-exceptions ==0.1.7.0,
any.scientific ==0.3.6.2,
scientific -bytestring-builder -integer-simple,
any.semigroupoids ==5.3.4,
semigroupoids +comonad +containers +contravariant +distributive +doctests +tagged +unordered-containers,
any.semigroups ==0.19.1,
semigroups +binary +bytestring -bytestring-builder +containers +deepseq +hashable +tagged +template-haskell +text +transformers +unordered-containers,
any.socks ==0.6.1,
any.stm ==2.5.0.0,
any.streaming-commons ==0.2.1.2,
streaming-commons -use-bytestring-builder,
any.streamly ==0.7.0,
streamly -benchmark -debug -dev -examples -examples-sdl -has-llvm -inspection -no-charts -no-fusion -streamk,
any.streamly-bytestring ==0.1.0.1,
any.syb ==0.7.1,
any.table-layout ==0.8.0.5,
any.tagged ==0.8.6,
tagged +deepseq +transformers,
any.template-haskell ==2.14.0.0,
any.text ==1.2.3.1,
any.text-binary ==0.2.1.1,
any.th-abstraction ==0.3.1.0,
any.time ==1.9.3,
any.time-compat ==1.9.2.2,
time-compat -old-locale,
any.tls ==1.5.3,
tls +compat -hans +network,
any.transformers ==0.5.6.2,
any.transformers-base ==0.4.5.2,
transformers-base +orphaninstances,
any.transformers-compat ==0.6.5,
transformers-compat -five +five-three -four +generic-deriving +mtl -three -two,
any.unix ==2.7.2.2,
any.unix-bytestring ==0.3.7.3,
any.unordered-containers ==0.2.10.0,
unordered-containers -debug,
any.utf8-string ==1.0.1.1,
any.uuid-types ==1.0.3,
any.vector ==0.12.0.3,
vector +boundschecks -internalchecks -unsafechecks -wall,
any.vector-binary-instances ==0.2.5.1,
any.vector-instances ==3.4,
vector-instances +hashable,
any.void ==0.7.3,
void -safe,
any.word8 ==0.1.3,
any.x509 ==1.7.5,
any.x509-store ==1.6.7,
any.x509-system ==1.6.6,
any.x509-validation ==1.6.11,
any.zlib ==0.6.2.1,
zlib -non-blocking-ffi -pkg-config

View File

@@ -18,21 +18,25 @@ library
exposed-modules: GHup exposed-modules: GHup
-- other-modules: -- other-modules:
-- other-extensions: -- other-extensions:
build-depends: attoparsec ^>= 0.13 build-depends: aeson ^>= 1.4
, base ^>= 4.12 , attoparsec ^>= 0.13
, bytestring ^>= 0.10 , base ^>= 4.12
, github ^>= 0.24 , bytestring ^>= 0.10
, hpath ^>= 0.11 , github ^>= 0.24
, hpath-io ^>= 0.13.1 , hpath ^>= 0.11
, http-client ^>= 0.6.4 , hpath-io ^>= 0.13.1
, mtl ^>= 2.2 , http-client ^>= 0.6.4
, pretty-terminal ^>= 0.1 , mtl ^>= 2.2
, safe-exceptions ^>= 0.1 , pretty-terminal ^>= 0.1
, streamly ^>= 0.7 , safe-exceptions ^>= 0.1
, text ^>= 1.2 , streamly ^>= 0.7
, unix ^>= 2.7 , text ^>= 1.2
, utf8-string ^>= 1.0 , time ^>= 1.9
, word8 ^>= 0.1 , unix ^>= 2.7
, unordered-containers ^>= 0.2
, utf8-string ^>= 1.0
, vector ^>= 0.12
, word8 ^>= 0.1
hs-source-dirs: lib hs-source-dirs: lib
ghc-options: -Wall ghc-options: -Wall
default-language: Haskell2010 default-language: Haskell2010
@@ -44,12 +48,20 @@ executable ghup
-- other-extensions: -- other-extensions:
build-depends: base ^>= 4.12 build-depends: base ^>= 4.12
, bytestring ^>= 0.10 , bytestring ^>= 0.10
, dates ^>= 0.2
, errors ^>= 2.3
, ghup , ghup
, github ^>= 0.24 , github ^>= 0.24
, hpath ^>= 0.11 , hpath ^>= 0.11
, mtl ^>= 2.2
, optparse-applicative ^>= 0.15 , optparse-applicative ^>= 0.15
, pretty-terminal ^>= 0.1 , pretty-terminal ^>= 0.1
, table-layout ^>= 0.8
, text ^>= 1.2
, time ^>= 1.9
, utf8-string ^>= 1.0
hs-source-dirs: app hs-source-dirs: app
ghc-options: -Wall
default-language: Haskell2010 default-language: Haskell2010
default-extensions: LambdaCase, MultiWayIf, RecordWildCards, TupleSections default-extensions: LambdaCase, MultiWayIf, RecordWildCards, TupleSections

View File

@@ -1,13 +1,19 @@
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE FlexibleInstances #-}
{-# OPTIONS_GHC -Wno-orphans #-} {-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module GHup module GHup
( (
-- * Types -- * Types
ForkResult(..) AnyPath(..)
, ForkResult(..)
, CloneMethod(..) , CloneMethod(..)
, ProcessError(..) , ProcessError(..)
, Settings(..) , Settings(..)
@@ -23,6 +29,11 @@ module GHup
, createBranch , createBranch
, deleteFork' , deleteFork'
, deleteFork , deleteFork
, getForks
, postGistStdin
, postGistFiles
, postGist
, listGists
-- * Parsers -- * Parsers
, parseURL , parseURL
, ghURLParser , ghURLParser
@@ -30,29 +41,51 @@ module GHup
, _info , _info
, _warn , _warn
, _err , _err
, uError
) )
where where
import Control.Applicative import Control.Applicative
import Control.Exception.Safe import Control.Exception.Safe
import Control.Monad.Except import Control.Monad.Except hiding ( fail )
import Control.Monad.Fail
import Control.Monad.Reader hiding ( fail )
import Data.Aeson
import Data.Aeson.TH
import Data.Attoparsec.ByteString import Data.Attoparsec.ByteString
import Data.ByteString ( ByteString ) import Data.ByteString ( ByteString )
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.UTF8 as UTF8 import qualified Data.ByteString.UTF8 as UTF8
import qualified Data.ByteString.Lazy.UTF8 as LUTF8 import qualified Data.ByteString.Lazy.UTF8 as LUTF8
import Data.Functor ( (<&>) ) import Data.Functor ( (<&>) )
import qualified Data.HashMap.Strict as H
import Data.HashMap.Strict ( HashMap )
import Data.List
import Data.Proxy import Data.Proxy
import qualified Data.Text as T
import qualified Data.Text.Encoding as E import qualified Data.Text.Encoding as E
import qualified Data.Text.IO as T
import Data.Traversable
import Data.Time.Clock
import Data.Time.Format.ISO8601
import qualified Data.Vector as V
import Data.Word8 import Data.Word8
import GHC.Exts ( toList )
import GitHub.Auth import GitHub.Auth
import GitHub.Data.Gists
import GitHub.Data.Name import GitHub.Data.Name
import GitHub.Data.URL import GitHub.Data.URL
import GitHub.Data.Request
import GitHub.Endpoints.Repos import GitHub.Endpoints.Repos
import GitHub.Endpoints.Search
import GitHub.Endpoints.Users
import GitHub.Request import GitHub.Request
import HPath import HPath
import HPath.IO import HPath.IO
import Prelude hiding ( readFile import Prelude hiding ( readFile
, writeFile , writeFile
, fail
) )
import System.Console.Pretty import System.Console.Pretty
import System.IO ( hPutStrLn import System.IO ( hPutStrLn
@@ -98,8 +131,8 @@ data UrlParseResult = UrlParseResult {
data Settings = Settings { data Settings = Settings {
auth :: Auth _auth :: Auth
, basePath :: Maybe (Path Abs) , _basePath :: Maybe (Path Abs)
} deriving (Eq, Read, Show) } deriving (Eq, Read, Show)
@@ -115,6 +148,8 @@ instance Read (Path Abs) where
_ -> [] _ -> []
data AnyPath = forall a . AnyPath (Path a)
@@ -124,7 +159,7 @@ instance Read (Path Abs) where
writeSettings :: Settings -> IO () writeSettings :: (MonadThrow m, MonadIO m) => Settings -> m ()
writeSettings settings = do writeSettings settings = do
sf <- getSettingsFile sf <- getSettingsFile
let fileperms = let fileperms =
@@ -132,86 +167,95 @@ writeSettings settings = do
`unionFileModes` ownerReadMode `unionFileModes` ownerReadMode
`unionFileModes` groupWriteMode `unionFileModes` groupWriteMode
`unionFileModes` groupReadMode `unionFileModes` groupReadMode
writeFile sf (Just fileperms) (u8 . show $ settings) liftIO $ writeFile sf (Just fileperms) (u8 . show $ settings)
_info ("Written config to file " <> (UTF8.toString $ toFilePath sf)) _info ("Written config to file " <> (UTF8.toString $ toFilePath sf))
getSettingsFile :: IO (Path Abs) getSettingsFile :: (MonadThrow m, MonadIO m) => m (Path Abs)
getSettingsFile = do getSettingsFile = do
let app_dir = [rel|ghup|] :: Path Rel let app_dir = [rel|ghup|] :: Path Rel
getEnv (u8 "XDG_CONFIG_HOME") >>= \case (liftIO $ getEnv (u8 "XDG_CONFIG_HOME")) >>= \case
Just config -> do Just config -> do
pc <- parseAbs config pc <- parseAbs config
pure $ pc </> app_dir pure $ pc </> app_dir
Nothing -> do Nothing -> do
let config_dir = [rel|.config|] :: Path Rel let config_dir = [rel|.config|] :: Path Rel
home <- getHomeDirectory >>= parseAbs home <- liftIO (getHomeDirectory >>= parseAbs)
pure $ home </> config_dir </> app_dir pure $ home </> config_dir </> app_dir
getSettings :: IO (Either String Settings) getSettings :: (MonadThrow m, MonadIO m) => ExceptT String m Settings
getSettings = runExceptT (fromEnv <|> fromFile) getSettings = (fromEnv <|> fromFile)
where where
fromEnv :: ExceptT String IO Settings fromEnv :: MonadIO m => ExceptT String m Settings
fromEnv = do fromEnv = do
(lift $ getEnv (u8 "GITHUB_TOKEN")) >>= \case (liftIO $ getEnv (u8 "GITHUB_TOKEN")) >>= \case
Just t -> pure $ Settings (OAuth t) Nothing Just t -> pure $ Settings (OAuth t) Nothing
Nothing -> throwError "Not found" Nothing -> throwError "Not found"
fromFile :: ExceptT String IO Settings fromFile :: (MonadThrow m, MonadIO m) => ExceptT String m Settings
fromFile = do fromFile = do
sf <- lift $ getSettingsFile sf <- getSettingsFile
out <- out <-
ExceptT ExceptT
$ ( flip catchIOError (\e -> pure $ Left $ show e) $ liftIO
$ fmap Right $ (flip catchIOError (\e -> pure $ Left $ show e) $ fmap Right $ readFile
$ readFile sf sf
) )
liftEither $ readEither (LUTF8.toString out) liftEither $ readEither (LUTF8.toString out)
---------------------------- -------------------------------------
--[ Github / Git actions ]-- --[ Combined Github / Git actions ]--
---------------------------- -------------------------------------
-- | Same as 'prepareRepoForPR', but gets the auth from the config file -- | Same as 'prepareRepoForPR', but gets the auth from the config file
-- and parses the owner/repo from the given repo url string. -- and parses the owner/repo from the given repo url string.
prepareRepoForPR' :: ByteString -- ^ string that contains repo url prepareRepoForPR' :: ( MonadIO m
, MonadReader Settings m
, MonadFail m
, MonadThrow m
)
=> ByteString -- ^ string that contains repo url
-> Maybe (Path b) -- ^ base path where the repo should be cloned -> Maybe (Path b) -- ^ base path where the repo should be cloned
-> Maybe ByteString -- ^ PR branch name to switch to -> Maybe ByteString -- ^ PR branch name to switch to
-> IO (Either String ()) -> ExceptT Error m ()
prepareRepoForPR' repoString mRepobase branch = runExceptT $ do prepareRepoForPR' repoString mRepobase branch = do
UrlParseResult {..} <- liftEither $ parseURL repoString UrlParseResult {..} <- (liftEither $ parseURL repoString) ?* uError
Settings {..} <- ExceptT getSettings
repobase <- case mRepobase of repobase <- case mRepobase of
Just r -> fmap Just $ lift $ toAbs r Just r -> fmap Just $ liftIO $ toAbs r
Nothing -> pure basePath Nothing -> basePath
ExceptT $ prepareRepoForPR auth owner repo repobase branch prepareRepoForPR owner repo repobase branch
-- | Fork the repository to my account, clone it, add original upstream -- | Fork the repository to my account, clone it, add original upstream
-- as remote, optionally switch to the given branch. -- as remote, optionally switch to the given branch.
prepareRepoForPR :: AuthMethod am prepareRepoForPR :: ( MonadIO m
=> am , MonadReader Settings m
-> Name Owner , MonadFail m
, MonadThrow m
)
=> Name Owner
-> Name Repo -> Name Repo
-> Maybe (Path b) -- ^ base path where the repo should be cloned -> Maybe (Path b) -- ^ base path where the repo should be cloned
-> Maybe ByteString -- ^ PR branch name to switch to -> Maybe ByteString -- ^ PR branch name to switch to
-> IO (Either String ()) -> ExceptT Error m ()
prepareRepoForPR am owner repo repobase branch = runExceptT $ do prepareRepoForPR owner repo repobase branch = do
repodest <- case repobase of repodest <- case repobase of
Just rb -> Just rb ->
((rb </>) <$> (parseRel $ E.encodeUtf8 $ untagName repo)) >>= lift . toAbs ((rb </>) <$> (parseRel $ E.encodeUtf8 $ untagName repo))
Nothing -> (parseRel $ E.encodeUtf8 $ untagName repo) >>= lift . toAbs >>= liftIO
ForkResult {..} <- withExceptT show $ ExceptT $ forkRepository am owner repo . toAbs
withExceptT show $ ExceptT $ cloneRepository CloneSSH downstream repodest Nothing -> (parseRel $ E.encodeUtf8 $ untagName repo) >>= liftIO . toAbs
withExceptT show $ ExceptT $ setUpstream upstream repodest ForkResult {..} <- (forkRepository owner repo) ?* (uError . show)
(ExceptT $ cloneRepository CloneSSH downstream repodest) ?* (uError . show)
(ExceptT $ setUpstream upstream repodest) ?* (uError . show)
case branch of case branch of
Just b -> withExceptT show $ ExceptT $ createBranch b repodest Just b -> (ExceptT $ createBranch b repodest) ?* (uError . show)
Nothing -> pure () Nothing -> pure ()
lift $ _info lift $ _info
( "To change to the repo dir, run:\n\tcd " ( "To change to the repo dir, run:\n\tcd "
@@ -219,21 +263,17 @@ prepareRepoForPR am owner repo repobase branch = runExceptT $ do
) )
forkRepository :: AuthMethod am
=> am -------------------
-> Name Owner --[ Git actions ]--
-> Name Repo -------------------
-> IO (Either Error ForkResult)
forkRepository am owner repo = runExceptT $ do
upstream <- ExceptT $ github' (repositoryR owner repo)
downstream <- ExceptT $ github am (forkExistingRepoR owner repo Nothing)
pure $ ForkResult { .. }
cloneRepository :: CloneMethod cloneRepository :: (MonadIO m, MonadFail m)
=> CloneMethod
-> Repo -> Repo
-> Path b -- ^ full path where the repo should be cloned to -> Path b -- ^ full path where the repo should be cloned to
-> IO (Either ProcessError ()) -> m (Either ProcessError ())
cloneRepository CloneSSH (Repo { repoSshUrl = (Just url) }) dest = cloneRepository CloneSSH (Repo { repoSshUrl = (Just url) }) dest =
_clone (E.encodeUtf8 $ getUrl url) (toFilePath dest) _clone (E.encodeUtf8 $ getUrl url) (toFilePath dest)
cloneRepository CloneHTTP (Repo { repoCloneUrl = (Just url) }) dest = cloneRepository CloneHTTP (Repo { repoCloneUrl = (Just url) }) dest =
@@ -242,9 +282,10 @@ cloneRepository _ _ _ = fail "No clone url!"
setUpstream :: Repo -- ^ upstream setUpstream :: (MonadIO m, MonadFail m)
=> Repo -- ^ upstream
-> Path b -- ^ full path to repo -> Path b -- ^ full path to repo
-> IO (Either ProcessError ()) -> m (Either ProcessError ())
setUpstream (Repo { repoCloneUrl = (Just url) }) repodir = _runGit setUpstream (Repo { repoCloneUrl = (Just url) }) repodir = _runGit
[ u8 "-C" [ u8 "-C"
, toFilePath repodir , toFilePath repodir
@@ -256,34 +297,119 @@ setUpstream (Repo { repoCloneUrl = (Just url) }) repodir = _runGit
setUpstream _ _ = fail "No clone url!" setUpstream _ _ = fail "No clone url!"
createBranch :: ByteString -- ^ branch name createBranch :: MonadIO m
=> ByteString -- ^ branch name
-> Path b -- ^ full path to repo -> Path b -- ^ full path to repo
-> IO (Either ProcessError ()) -> m (Either ProcessError ())
createBranch branch repodir = createBranch branch repodir =
_runGit [u8 "-C", toFilePath repodir, u8 "checkout", u8 "-b", branch] _runGit [u8 "-C", toFilePath repodir, u8 "checkout", u8 "-b", branch]
----------------------
--[ Github actions ]--
----------------------
forkRepository :: (MonadIO m, MonadReader Settings m)
=> Name Owner
-> Name Repo
-> ExceptT Error m ForkResult
forkRepository owner repo = do
upstream <- github_ (repositoryR owner repo)
downstream <- githubAuth (forkExistingRepoR owner repo Nothing)
pure $ ForkResult { .. }
-- | Same as deleteFork, but gets the auth from the config file -- | Same as deleteFork, but gets the auth from the config file
-- and parses the owner/repo from the given repo url string. -- and parses the owner/repo from the given repo url string.
deleteFork' :: ByteString -> IO (Either String ()) deleteFork' :: (MonadIO m, MonadReader Settings m)
deleteFork' repoString = runExceptT $ do => ByteString
UrlParseResult {..} <- liftEither $ parseURL repoString -> ExceptT Error m ()
Settings {..} <- ExceptT getSettings deleteFork' repoString = do
ExceptT $ deleteFork auth owner repo UrlParseResult {..} <- (liftEither $ parseURL repoString) ?* uError
deleteFork owner repo
deleteFork :: AuthMethod am deleteFork :: (MonadIO m, MonadReader Settings m)
=> am => Name Owner
-> Name Owner
-> Name Repo -> Name Repo
-> IO (Either String ()) -> ExceptT Error m ()
deleteFork am owner repo = runExceptT $ do deleteFork owner repo = do
(withExceptT show $ ExceptT $ github' (repositoryR owner repo)) >>= \case github_ (repositoryR owner repo) >>= \case
(Repo { repoFork = Just True }) -> pure () (Repo { repoFork = Just True }) -> pure ()
_ -> throwError "Not a fork" _ -> throwError (uError "Not a fork")
withExceptT show $ ExceptT $ github am (deleteRepoR owner repo) githubAuth (deleteRepoR owner repo)
getForks :: (MonadIO m, MonadReader Settings m)
=> Maybe UTCTime
-> ExceptT Error m [Repo]
getForks mtime = do
user <- githubAuth userInfoCurrentR
let userName = untagName $ userLogin user
repos <- github_
(searchReposR $ mconcat [T.pack "user:", userName, T.pack " fork:only"])
pure $ sortBy (\x y -> compare (repoUpdatedAt y) (repoUpdatedAt x)) $ filter
(\case
Repo { repoFork = Just True, repoUpdatedAt = Just t } ->
maybe True (t >=) mtime
_ -> False
)
(toList $ searchResultResults repos)
data GistContent = GistContent {
content :: T.Text
} deriving (Show, Eq)
data GistRequest = GistRequest {
description :: T.Text
, public :: Bool
, files :: HashMap T.Text GistContent
} deriving (Show, Eq)
postGistStdin :: (MonadIO m, MonadReader Settings m, MonadThrow m)
=> T.Text -- ^ description
-> Bool -- ^ whether to be public
-> ExceptT Error m Gist
postGistStdin description public = do
content <- liftIO T.getContents
let files = H.fromList [(T.pack "stdout", GistContent content)]
postGist GistRequest { .. }
postGistFiles :: (MonadIO m, MonadReader Settings m, MonadThrow m)
=> [AnyPath] -- ^ files
-> T.Text -- ^ description
-> Bool -- ^ whether to be public
-> ExceptT Error m Gist
postGistFiles files' description public = do
files <- liftIO $ fmap H.fromList $ for files' $ \(AnyPath file) -> do
contents <- (E.decodeUtf8 . L.toStrict) <$> readFile file
filename <- (E.decodeUtf8 . toFilePath) <$> basename file
pure (filename, GistContent contents)
postGist GistRequest { .. }
postGist :: (MonadIO m, MonadReader Settings m)
=> GistRequest
-> ExceptT Error m Gist
postGist greq = githubAuth (command Post [T.pack "gists"] (encode greq))
listGists :: (MonadIO m, MonadReader Settings m)
=> Maybe UTCTime
-> ExceptT Error m [Gist]
listGists mtime = do
let queryString = case mtime of
Just time -> [(u8 "since", Just $ UTF8.fromString $ iso8601Show time)]
Nothing -> []
V.toList <$> githubAuth (pagedQuery [T.pack "gists"] queryString FetchAll)
@@ -308,14 +434,20 @@ ghURLParser =
<|> str "git@github.com:" <|> str "git@github.com:"
<|> empty' <|> empty'
) )
*> takeWhile1 (\w -> (w /= _slash) && isAlphaNum w) *> takeWhile1 (/= _slash)
<* word8 _slash <* word8 _slash
) )
<*> (takeWhile1 isAlphaNum <* ((str ".git" <|> empty') <* endOfInput)) <*> parseRepoName
where where
str = string . u8 str = string . u8
empty' = str "" empty' = str ""
parseRepoName :: Parser ByteString
parseRepoName = do
c <- fmap B.singleton anyWord8
r <- many1' ((str ".git" <* endOfInput) <|> fmap B.singleton anyWord8)
if last r == u8 ".git"
then pure $ mconcat (c : (init r))
else pure (mconcat (c : r)) <* endOfInput
@@ -327,7 +459,7 @@ ghURLParser =
u8 :: String -> ByteString u8 :: String -> ByteString
u8 = UTF8.fromString u8 = UTF8.fromString
_clone :: ByteString -> ByteString -> IO (Either ProcessError ()) _clone :: MonadIO m => ByteString -> ByteString -> m (Either ProcessError ())
_clone url dest = _runGit [u8 "clone", url, dest] _clone url dest = _runGit [u8 "clone", url, dest]
_toGitError :: Maybe ProcessStatus -> Either ProcessError () _toGitError :: Maybe ProcessStatus -> Either ProcessError ()
@@ -338,8 +470,8 @@ _toGitError ps = case ps of
Just (SPPB.Stopped _ ) -> Left $ ProcessInterrupted Just (SPPB.Stopped _ ) -> Left $ ProcessInterrupted
Nothing -> Left $ NoSuchPid Nothing -> Left $ NoSuchPid
_runGit :: [ByteString] -> IO (Either ProcessError ()) _runGit :: MonadIO m => [ByteString] -> m (Either ProcessError ())
_runGit args = do _runGit args = liftIO $ do
pid <- executeFile ([rel|git|] :: Path Rel) args pid <- executeFile ([rel|git|] :: Path Rel) args
SPPB.getProcessStatus True True pid <&> _toGitError SPPB.getProcessStatus True True pid <&> _toGitError
@@ -354,14 +486,52 @@ getHomeDirectory = do
pure $ u8 h -- this is a guess pure $ u8 h -- this is a guess
_info :: String -> IO () _info :: MonadIO m => String -> m ()
_info = putStrLn . color Green _info = liftIO . _stderr . color Green
_warn :: String -> IO () _warn :: MonadIO m => String -> m ()
_warn = _stderr . color Yellow _warn = liftIO . _stderr . color Yellow
_err :: String -> IO () _err :: MonadIO m => String -> m ()
_err = _stderr . color Red _err = liftIO . _stderr . color Red
_stderr :: String -> IO () _stderr :: MonadIO m => String -> m ()
_stderr = hPutStrLn stderr _stderr = liftIO . hPutStrLn stderr
auth :: MonadReader Settings m => m Auth
auth = asks _auth
basePath :: MonadReader Settings m => m (Maybe (Path Abs))
basePath = asks _basePath
githubAuth :: ( MonadReader Settings m
, MonadIO m
, ParseResponse mt req
, res ~ Either Error req
)
=> (GenRequest mt rw req)
-> ExceptT Error m req
githubAuth req = do
a <- auth
ExceptT $ liftIO $ github a req
github_ :: (MonadIO m, ParseResponse mt req, res ~ Either Error req, ro ~ 'RO)
=> (GenRequest mt ro req)
-> ExceptT Error m req
github_ req = do
ExceptT $ liftIO $ github' req
-- | Flipped 'withExceptT'.
(?*) :: Functor m => ExceptT e m a -> (e -> e') -> ExceptT e' m a
(?*) = flip withExceptT
uError :: String -> Error
uError = UserError . T.pack
$(deriveJSON defaultOptions 'GistContent)
$(deriveJSON defaultOptions 'GistRequest)

66
update-index-state.sh Executable file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -eu
status_message() {
printf "\\033[0;32m%s\\033[0m\\n" "$1"
}
error_message() {
printf "\\033[0;31m%s\\033[0m\\n" "$1"
}
SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )"
CACHE_LOCATION="${HOME}/.cabal/packages/hackage.haskell.org/01-index.cache"
if [ ! -f "${CACHE_LOCATION}" ] ; then
error_message "${CACHE_LOCATION} does not exist, did you run 'cabal update'?"
exit 1
fi
if [ ! -f "${SCRIPTPATH}/cabal.project" ] ; then
error_message "Could not find ${SCRIPTPATH}/cabal.project, skipping index state update."
exit 3
fi
cabal v2-update
arch=$(getconf LONG_BIT)
case "${arch}" in
32)
byte_size=4
magic_word="CABA1002"
;;
64)
byte_size=8
magic_word="00000000CABA1002"
;;
*)
error_message "Unknown architecture (long bit): ${arch}"
exit 2
;;
esac
# This is the logic to parse the binary format of 01-index.cache.
# The first word is a magic 'caba1002', the second one is the timestamp in unix epoch.
# Better than copying the cabal-install source code.
if [ "$(xxd -u -p -l${byte_size} -s 0 "${CACHE_LOCATION}")" != "${magic_word}" ] ; then
error_message "Magic word does not match!"
exit 4
fi
cache_timestamp=$(echo "ibase=16;obase=A;$(xxd -u -p -l${byte_size} -s ${byte_size} "${CACHE_LOCATION}")" | bc)
# If we got junk from the binary file, this should fail.
cache_date=$(date --utc --date "@${cache_timestamp}" "+%FT%TZ")
status_message "Updating index state in ${SCRIPTPATH}/cabal.project"
if grep -q "^index-state: .*" "${SCRIPTPATH}/cabal.project" ; then
awk '/index-state:/ {gsub(/.*/, "index-state: '${cache_date}'")}; { print }' "${SCRIPTPATH}/cabal.project" > "${SCRIPTPATH}/cabal.project.tmp"
mv "${SCRIPTPATH}/cabal.project.tmp" "${SCRIPTPATH}/cabal.project"
else
printf "index-state: %s\n" "${cache_date}" >> "${SCRIPTPATH}/cabal.project"
fi