life-sync-1.1.1.0: src/Life/Github.hs
{- |
Copyright: (c) 2017-2020 Kowainik
SPDX-License-Identifier: MPL-2.0
Maintainer: Kowainik <xrom.xkov@gmail.com>
Utilities to work with GitHub repositories using @hub@.
-}
module Life.Github
(
-- * Repository utils
checkRemoteSync
, cloneRepo
, createNewBranch
, doesBranchExist
, insideRepo
, withSynced
-- * Repository manipulation commands
, copyLife
, addToRepo
, createRepository
, pullUpdateFromRepo
, removeFromRepo
, updateDotfilesRepo
, updateFromRepo
, getUserLogin
) where
import Colourista (errorMessage, infoMessage, warningMessage)
import Control.Exception (catch, throwIO)
import Path (Abs, Dir, File, Path, Rel, toFilePath, (</>))
import Path.IO (copyDirRecur, copyFile, getHomeDir, withCurrentDir)
import Shellmet (($|))
import System.IO.Error (IOError, isDoesNotExistError)
import Life.Configuration (LifeConfiguration (..), lifeConfigMinus, parseRepoLife)
import Life.Core (Branch (..), CommitMsg (..), CopyDirection (..), Owner (..), Repo (..), master)
import Life.Message (chooseYesNo)
import Life.Path (lifePath, relativeToHome, repoName)
import qualified Data.Text as Text
----------------------------------------------------------------------------
-- VSC commands
----------------------------------------------------------------------------
askToPushka :: CommitMsg -> IO ()
askToPushka commitMsg = do
"git" ["add", "."]
infoMessage "The following changes are going to be pushed:"
"git" ["diff", "--name-status", "HEAD"]
continue <- chooseYesNo "Would you like to proceed?"
if continue
then pushka master commitMsg
else errorMessage "Abort pushing" >> exitFailure
-- | Make a commit and push it.
pushka :: Branch -> CommitMsg -> IO ()
pushka (Branch branch) (CommitMsg commitMsg) = do
"git" ["add", "."]
"git" ["commit", "-m", commitMsg]
"git" ["push", "-u", "origin", branch]
-- | Creates repository on GitHub inside given folder.
createRepository :: Maybe Owner -> Repo -> IO ()
createRepository mo (Repo repo) = do
owner <- getOwnerLogin mo
let description = ":computer: Configuration files"
"git" ["init"]
"hub" ["create", "-d", description, owner <> "/" <> repo]
pushka master $ CommitMsg "Create the project"
-- | Get user login from the local global git config.
getUserLogin :: IO Text
getUserLogin = do
login <- "git" $| ["config", "user.login"]
if login == ""
then errorMessage "user.login is not specified" >> exitFailure
else pure $ toText login
-- | Consider owner from global git config if Owner is not given
getOwnerLogin :: Maybe Owner -> IO Text
getOwnerLogin = maybe getUserLogin (pure . unOwner)
----------------------------------------------------------------------------
-- dotfiles workflow
----------------------------------------------------------------------------
-- | Executes action with 'repoName' set as pwd.
insideRepo :: IO a -> IO a
insideRepo action = do
repoPath <- relativeToHome repoName
withCurrentDir repoPath action
-- | Commits all changes inside 'repoName' and pushes to remote.
pushRepo :: CommitMsg -> IO ()
pushRepo = insideRepo . askToPushka
-- | Clones @dotfiles@ repository assuming it doesn't exist.
cloneRepo :: Maybe Owner -> IO ()
cloneRepo mo = do
owner <- getOwnerLogin mo
homeDir <- getHomeDir
withCurrentDir homeDir $ do
infoMessage "Using SSH to clone repo..."
"git" ["clone", "git@github.com:" <> owner <> "/dotfiles.git"]
-- | Create new branch with given branch name
createNewBranch :: Branch -> IO ()
createNewBranch (Branch branch) =
"git" ["checkout", "-b", branch]
-- | Returns true if local @dotfiles@ repository is synchronized with remote repo.
checkRemoteSync :: Branch -> IO Bool
checkRemoteSync (Branch branch) = do
"git" ["fetch", "origin", branch]
localHash <- "git" $| ["rev-parse", branch]
remoteHash <- "git" $| ["rev-parse", "origin/" <> branch]
pure $ localHash == remoteHash
-- | Check if a branch exists in remote repo
doesBranchExist :: Branch -> IO Bool
doesBranchExist (Branch branch) = do
r <- "git" $| ["ls-remote", "--heads", "origin", branch]
pure $ not (Text.null r)
withSynced :: Branch -> IO a -> IO a
withSynced branch@(Branch branchname) action = insideRepo $ do
infoMessage "Checking if repo is synchronized..."
isSynced <- checkRemoteSync branch
if isSynced then do
infoMessage "Repo is up-to-date"
action
else do
warningMessage "Local version of repository is out of date"
shouldSync <- chooseYesNo "Do you want to sync repo with remote?"
if shouldSync then do
"git" ["rebase", "origin/" <> branchname]
action
else do
errorMessage "Aborting current command because repository is not synchronized with remote"
exitFailure
----------------------------------------------------------------------------
-- File manipulation
----------------------------------------------------------------------------
pullUpdateFromRepo :: LifeConfiguration -> IO ()
pullUpdateFromRepo life = do
insideRepo $ "git" ["pull", "-r"]
updateFromRepo life
updateFromRepo :: LifeConfiguration -> IO ()
updateFromRepo excludeLife = insideRepo $ do
infoMessage "Copying files from repo to local machine..."
repoLife <- parseRepoLife
let lifeToLive = lifeConfigMinus repoLife excludeLife
copyLife FromRepoToHome lifeToLive
updateDotfilesRepo :: CommitMsg -> LifeConfiguration -> IO ()
updateDotfilesRepo commitMsg life = do
copyLife FromHomeToRepo life
pushRepo commitMsg
copyLife :: CopyDirection -> LifeConfiguration -> IO ()
copyLife direction LifeConfiguration{..} = do
copyFiles direction (toList lifeConfigurationFiles)
copyDirs direction (toList lifeConfigurationDirectories)
-- | Copy files to repository and push changes to remote repository.
copyFiles :: CopyDirection -> [Path Rel File] -> IO ()
copyFiles = copyPathList copyFile
-- | Copy dirs to repository.
copyDirs :: CopyDirection -> [Path Rel Dir] -> IO ()
copyDirs = copyPathList copyDirRecur
copyPathList
:: (Path Abs t -> Path Abs t -> IO ())
-- ^ Copying action
-> CopyDirection
-- ^ Describes in which direction files should be copied
-> [Path Rel t]
-- ^ List of paths to copy
-> IO ()
copyPathList copyAction direction pathList = do
homeDir <- getHomeDir
let repoDir = homeDir </> repoName
for_ pathList $ \entryPath -> do
let homePath = homeDir </> entryPath
let repoPath = repoDir </> entryPath
case direction of
FromHomeToRepo -> copyAction homePath repoPath
FromRepoToHome -> copyAction repoPath homePath
-- | Update .life file
updateLifeFile :: IO ()
updateLifeFile = do
lifeFile <- relativeToHome lifePath
repoLifeFile <- relativeToHome (repoName </> lifePath)
copyFile lifeFile repoLifeFile
-- | Adds file or directory to the repository and commits
addToRepo :: (Path Abs t -> Path Abs t -> IO ()) -> Path Rel t -> IO ()
addToRepo copyFun path = do
-- copy file
sourcePath <- relativeToHome path
destinationPath <- relativeToHome (repoName </> path)
copyFun sourcePath destinationPath
updateLifeFile
let commitMsg = CommitMsg $ "Add: " <> toText (toFilePath path)
pushRepo commitMsg
-- | Removes file or directory from the repository and commits
removeFromRepo :: (Path Abs t -> IO ()) -> Path Rel t -> IO ()
removeFromRepo removeFun path = do
absPath <- relativeToHome (repoName </> path)
catch (removeFun absPath) handleNotExist
updateLifeFile
let commitMsg = CommitMsg $ "Remove: " <> pathTextName
pushRepo commitMsg
where
pathTextName :: Text
pathTextName = toText $ toFilePath path
handleNotExist :: IOError -> IO ()
handleNotExist e = if isDoesNotExistError e
then errorMessage ("File/directory " <> pathTextName <> " is not found") >> exitFailure
else throwIO e