module Git ( Branch (..), checkout, deleteBranch, fetch, fullBranchName, isCommonBranch, isRemoteBranch, listBranches, rebaseInteractive, merge, toBranches, ) where import Data.Char (isSpace) import Data.Text (Text) import Data.Text qualified as T import Data.Text.IO qualified as T import System.Exit import System.Process data Branch = BranchLocal Text | BranchCurrent Text | BranchRemote Text Text deriving (Eq) instance Show Branch where show (BranchLocal n) = T.unpack n show (BranchCurrent n) = T.unpack $ n <> "*" show (BranchRemote o n) = T.unpack $ o <> "/" <> n fetch :: IO Text fetch = readGit ["fetch", "--all", "--prune"] listBranches :: IO [Branch] listBranches = toBranches <$> readGit [ "branch" , "--list" , "--all" , "--sort=-committerdate" , "--no-column" , "--no-color" ] toBranches :: Text -> [Branch] toBranches input = toBranch <$> filter validBranch (T.lines input) where validBranch b = not $ isHead b || isDetachedHead b || isNoBranch b toBranch :: Text -> Branch toBranch line = mkBranch $ T.words $ T.dropWhile isSpace line where mkBranch ("*" : name : _) = BranchCurrent name mkBranch (name : _) = case T.stripPrefix "remotes/" name of Just rest -> parseRemoteBranch rest Nothing -> BranchLocal name mkBranch [] = error "empty branch name" parseRemoteBranch str = BranchRemote remote name where (remote, rest) = T.span ('/' /=) str name = T.drop 1 rest checkout :: Branch -> IO ExitCode checkout branch = spawnGit ["checkout", branchName branch] rebaseInteractive :: Branch -> IO ExitCode rebaseInteractive branch = do T.putStrLn $ "Rebase onto " <> fullBranchName branch spawnGit ["rebase", "--interactive", "--autostash", fullBranchName branch] merge :: Branch -> IO ExitCode merge branch = do T.putStrLn $ "Merge branch " <> fullBranchName branch spawnGit ["merge", fullBranchName branch] deleteBranch :: Branch -> IO ExitCode deleteBranch (BranchCurrent _) = error "Cannot delete current branch" deleteBranch (BranchLocal n) = spawnGit ["branch", "-D", n] deleteBranch (BranchRemote o n) = spawnGit ["push", o, "--delete", n] spawnGit :: [Text] -> IO ExitCode spawnGit args = waitForProcess =<< spawnProcess "git" (T.unpack <$> args) readGit :: [Text] -> IO Text readGit args = T.pack <$> readProcess "git" (T.unpack <$> args) [] isCommonBranch :: Branch -> Bool isCommonBranch b = branchName b `elem` commonBranchNames where commonBranchNames = [ "master" , "main" , "dev" , "devel" , "develop" , "development" , "staging" , "trunk" ] isRemoteBranch :: Branch -> Bool isRemoteBranch (BranchRemote _ _) = True isRemoteBranch _ = False --- Helper branchName :: Branch -> Text branchName (BranchCurrent n) = n branchName (BranchLocal n) = n branchName (BranchRemote _ n) = n fullBranchName :: Branch -> Text fullBranchName (BranchCurrent n) = n fullBranchName (BranchLocal n) = n fullBranchName (BranchRemote r n) = r <> "/" <> n isHead :: Text -> Bool isHead = T.isInfixOf "HEAD" isDetachedHead :: Text -> Bool isDetachedHead = T.isInfixOf "HEAD detached" -- While rebasing git will show "no branch" -- e.g. "* (no branch, rebasing branch-name)" isNoBranch :: Text -> Bool isNoBranch = T.isInfixOf "(no branch,"