git-brunch 1.7.2.0 → 1.8.0
raw patch · 6 files changed
+109/−13 lines, 6 filesdep +containersdep +directory
Dependencies added: containers, directory
Files
- README.md +17/−0
- app/Git.hs +43/−0
- app/GitBrunch.hs +36/−11
- app/Theme.hs +4/−0
- git-brunch.cabal +6/−2
- test/Spec.hs +3/−0
README.md view
@@ -11,6 +11,7 @@ - Search for a branch - Delete a branch - Fetch / Update+- Worktree support ## Usage @@ -42,6 +43,22 @@ ```sh yay -S git-brunch pamac install git-brunch+```++### Homebrew (macOS)++You can install `git-brunch` via a Homebrew tap. This uses a prebuilt macOS binary.++```sh+brew tap andys8/tap+brew install git-brunch+```++#### macOS Security Note+Since the binary is not signed with an Apple Developer certificate, macOS might block it initially. After installation, you can run this command to allow the binary to execute:++```sh+xattr -d com.apple.quarantine $(which git-brunch) ``` ### FreeBSD
app/Git.hs view
@@ -7,27 +7,35 @@ isCommonBranch, isRemoteBranch, listBranches,+ getWorktreePath, rebaseInteractive, merge, toBranches, ) where import Data.Char (isSpace)+import Data.Map (Map)+import Data.Map qualified as Map+import Data.Maybe (fromMaybe) import Data.Text (Text) import Data.Text qualified as T import Data.Text.IO qualified as T+import System.Directory (setCurrentDirectory)+import System.Environment (lookupEnv) import System.Exit import System.Process data Branch = BranchLocal Text | BranchCurrent Text+ | BranchWorktree Text | BranchRemote Text Text deriving (Eq) instance Show Branch where show (BranchLocal n) = T.unpack n show (BranchCurrent n) = T.unpack $ n <> "*"+ show (BranchWorktree n) = T.unpack $ n <> "+" show (BranchRemote o n) = T.unpack $ o <> "/" <> n fetch :: IO Text@@ -54,6 +62,7 @@ toBranch line = mkBranch $ T.words $ T.dropWhile isSpace line where mkBranch ("*" : name : _) = BranchCurrent name+ mkBranch ("+" : name : _) = BranchWorktree name mkBranch (name : _) = case T.stripPrefix "remotes/" name of Just rest -> parseRemoteBranch rest Nothing -> BranchLocal name@@ -64,8 +73,20 @@ name = T.drop 1 rest checkout :: Branch -> IO ExitCode+checkout (BranchWorktree n) = do+ mPath <- getWorktreePath n+ case mPath of+ Just path -> spawnShell path+ Nothing -> spawnGit ["checkout", n] checkout branch = spawnGit ["checkout", branchName branch] +spawnShell :: FilePath -> IO ExitCode+spawnShell path = do+ setCurrentDirectory path+ userShell <- fromMaybe "/bin/sh" <$> lookupEnv "SHELL"+ T.putStrLn $ "Worktree: " <> T.pack path+ waitForProcess =<< spawnProcess userShell []+ rebaseInteractive :: Branch -> IO ExitCode rebaseInteractive branch = do T.putStrLn $ "Rebase onto " <> fullBranchName branch@@ -78,6 +99,7 @@ deleteBranch :: Branch -> IO ExitCode deleteBranch (BranchCurrent _) = error "Cannot delete current branch"+deleteBranch (BranchWorktree _) = error "Cannot delete branch checked out in another worktree" deleteBranch (BranchLocal n) = spawnGit ["branch", "-D", n] deleteBranch (BranchRemote o n) = spawnGit ["push", o, "--delete", n] @@ -87,6 +109,25 @@ readGit :: [Text] -> IO Text readGit args = T.pack <$> readProcess "git" (T.unpack <$> args) [] +getWorktreePath :: Text -> IO (Maybe FilePath)+getWorktreePath branch = do+ output <- readGit ["worktree", "list", "--porcelain"]+ let worktrees = parseWorktrees (T.lines output)+ pure $ Map.lookup branch worktrees++parseWorktrees :: [Text] -> Map Text FilePath+parseWorktrees lines' = go lines' Map.empty Nothing+ where+ go [] m _ = m+ go (l : ls) m currentPath+ | "worktree " `T.isPrefixOf` l = go ls m (Just $ T.unpack $ T.drop 9 l)+ | "branch " `T.isPrefixOf` l =+ let branch = fromMaybe (T.drop 13 l) (T.stripPrefix "refs/heads/" (T.drop 7 l))+ in case currentPath of+ Just p -> go ls (Map.insert branch p m) Nothing+ Nothing -> go ls m Nothing+ | otherwise = go ls m currentPath+ isCommonBranch :: Branch -> Bool isCommonBranch b = branchName b `elem` commonBranchNames where@@ -109,11 +150,13 @@ branchName :: Branch -> Text branchName (BranchCurrent n) = n+branchName (BranchWorktree n) = n branchName (BranchLocal n) = n branchName (BranchRemote _ n) = n fullBranchName :: Branch -> Text fullBranchName (BranchCurrent n) = n+fullBranchName (BranchWorktree n) = n fullBranchName (BranchLocal n) = n fullBranchName (BranchRemote r n) = r <> "/" <> n
app/GitBrunch.hs view
@@ -75,14 +75,17 @@ main = do branches <- Git.listBranches `catch` gitFailed state <- M.defaultMain app $ syncBranchLists emptyState{_branches = branches}- let execGit = gitFunction (_gitCommand state)- exitCode <- maybe noBranchErr execGit (selectedBranch state)- when (exitCode /= ExitSuccess)- $ die ("Failed to " ++ show (_gitCommand state) ++ ".")+ case selectedBranch state of+ Nothing -> pure ()+ Just branch -> do+ let cmd = _gitCommand state+ let execGit = gitFunction cmd+ exitCode <- execGit branch+ when (exitCode /= ExitSuccess)+ $ die ("Failed to " ++ show cmd ++ ".") where gitFailed :: SomeException -> IO a gitFailed _ = exitFailure- noBranchErr = die "No branch selected." gitFunction = \case GitCheckout -> Git.checkout GitRebase -> Git.rebaseInteractive@@ -181,6 +184,7 @@ where maxPadding = if isListFocussed then padRight Max else id highlight (BranchCurrent _) = withAttr attrBranchCurrent+ highlight (BranchWorktree _) = withAttr attrBranchWorktree highlight b | Git.isCommonBranch b = withAttr attrBranchCommon highlight _ = id @@ -221,7 +225,10 @@ confirmDelete :: Maybe Branch -> EventM Name State () confirmDelete (Just (BranchCurrent _)) = pure ()- confirmDelete (Just _) = dialogL ?= createDialog GitDeleteBranch+ confirmDelete (Just (BranchWorktree _)) = pure ()+ confirmDelete (Just _) = do+ gitCommandL .= GitDeleteBranch+ dialogL ?= createDialog GitDeleteBranch confirmDelete Nothing = pure () fetch = do@@ -270,10 +277,29 @@ dialogL .= Nothing gitCommandL .= GitCheckout - confirmDialog cmd = do- dialogL .= Nothing- gitCommandL .= cmd- halt+ confirmDialog cmd =+ if cmd == GitDeleteBranch+ then do+ dialogL .= Nothing+ gitCommandL .= GitCheckout+ state <- get+ case selectedBranch state of+ Nothing -> pure ()+ Just branch -> M.suspendAndResume $ do+ exitCode <- Git.deleteBranch branch+ if exitCode == ExitSuccess+ then do+ branches <- Git.listBranches+ pure $ syncBranchLists state{_branches = branches}+ else do+ T.putStrLn $ "Failed to delete branch " <> Git.fullBranchName branch+ T.putStrLn "Press Enter to continue..."+ void getLine+ pure state+ else do+ dialogL .= Nothing+ gitCommandL .= cmd+ halt in case vimifiedKey e of EvKey KEnter [] -> do@@ -325,7 +351,6 @@ updateBranches branches = syncBranchLists . (branchesL .~ branches)- . (filterL .~ emptyFilter) syncBranchLists :: State -> State syncBranchLists state =
app/Theme.hs view
@@ -27,6 +27,7 @@ , (attrTitle, withStyle (fg brightWhite) bold) , (attrTitleFocus, withStyle (fg yellow) bold) , (attrBranchCurrent, fg brightRed)+ , (attrBranchWorktree, fg brightCyan) , (attrBranchCommon, fg brightBlue) ] @@ -47,6 +48,9 @@ attrBranchCurrent :: AttrName attrBranchCurrent = attrName "current-branch"++attrBranchWorktree :: AttrName+attrBranchWorktree = attrName "worktree-branch" attrBranchCommon :: AttrName attrBranchCommon = attrName "common-branch"
git-brunch.cabal view
@@ -1,11 +1,11 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.35.1.+-- This file has been generated from package.yaml by hpack version 0.38.1. -- -- see: https://github.com/sol/hpack name: git-brunch-version: 1.7.2.0+version: 1.8.0 synopsis: git checkout command-line tool description: Please see the README on GitHub at <https://github.com/andys8/git-brunch> category: Git@@ -45,6 +45,8 @@ build-depends: base >=4.7 && <5 , brick+ , containers+ , directory , extra , hspec , microlens@@ -80,6 +82,8 @@ build-depends: base >=4.7 && <5 , brick+ , containers+ , directory , extra , hspec , microlens
test/Spec.hs view
@@ -14,6 +14,9 @@ it "detects current branch by asterik" $ do toBranches "* master" `shouldBe` [BranchCurrent "master"] + it "detects worktree branch by plus" $ do+ toBranches "+ master" `shouldBe` [BranchWorktree "master"]+ it "returns a local branch" $ do toBranches "master" `shouldBe` [BranchLocal "master"]