hit-on 0.0.0 → 0.1.0.0
raw patch · 9 files changed
+710/−137 lines, 9 filesdep +directorydep +hspecdep +processdep ~basedep ~githubdep ~relude
Dependencies added: directory, hspec, process
Dependency ranges changed: base, github, relude
Files
- CHANGELOG.md +51/−2
- README.md +85/−7
- hit-on.cabal +50/−49
- src/Hit/Cli.hs +96/−36
- src/Hit/ColorTerminal.hs +55/−14
- src/Hit/Core.hs +28/−0
- src/Hit/Git.hs +136/−29
- src/Hit/Git/Status.hs +177/−0
- test/Main.hs +32/−0
CHANGELOG.md view
@@ -3,8 +3,57 @@ `hit-on` uses [PVP Versioning][1]. The changelog is available [on GitHub][2]. -0.0.0-=====+### 0.1.0.0 — Aug 3, 2019++* [#85](https://github.com/kowainik/hit-on/issues/85):+ Make `hit commit` command take the commit name from the corresponding issue+ name, if it is currently applicable (the branch name has the info about issue+ number)+ (by [@vrom911](https://github.com/vrom911)).+* [#63](https://github.com/kowainik/hit-on/issues/63),+ [#79](https://github.com/kowainik/hit-on/issues/79),+ [#86](https://github.com/kowainik/hit-on/issues/86):+ Implement `hit status` command with pretty output+ (by [@chshersh](https://github.com/chshersh)).+* [#72](https://github.com/kowainik/hit-on/issues/72):+ Allow `hit new` command to receive branch names as long as issue numbers+ (by [@vrom911](https://github.com/vrom911)).+* [#89](https://github.com/kowainik/hit-on/issues/89):+ Add `-p|push` and `-f|force` flags to `hit commit` command+ (by [@vrom911](https://github.com/vrom911)).+* [#81](https://github.com/kowainik/hit-on/issues/81):+ Implement `hit diff` command with pretty diff+ (by [@chshersh](https://github.com/chshersh)).+* [#67](https://github.com/kowainik/hit-on/issues/67):+ Implement `hit stash` and `hit unstash` commands+ (by [@chshersh](https://github.com/chshersh)).+* [#77](https://github.com/kowainik/hit-on/issues/77):+ Implement `hit clear` command+ (by [@chshersh](https://github.com/chshersh)).+* [#82](https://github.com/kowainik/hit-on/issues/82):+ Add `--force` flag to `hit fix` command+ (by [@vrom911](https://github.com/vrom911)).+* [#80](https://github.com/kowainik/hit-on/issues/80):+ Add `hit uncommit` command+ (by [@vrom911](https://github.com/vrom911)).+* [#69](https://github.com/kowainik/hit-on/issues/69):+ Write autocompletion instructions+ (by [@chshersh](https://github.com/chshersh)).+* [#49](https://github.com/kowainik/hit-on/issues/49):+ Configure OSX releases on Travis CI+ (by [@chshersh](https://github.com/chshersh)).+* [#96](https://github.com/kowainik/hit-on/issues/96):+ Add table of all commands to README+ (by [@vrom911](https://github.com/vrom911)).+* [#35](https://github.com/kowainik/hit-on/issues/35):+ Write unit tests for URL parsing functions+ (by [@kahlil29](https://github.com/kahlil29)).+* Bump up to GHC 8.6.5+ (by [@chshersh](https://github.com/chshersh)).+* Bump up to `relude-0.5.0`+ (by [@chshersh](https://github.com/chshersh)).++### 0.0.0 * Initially created.
README.md view
@@ -1,11 +1,11 @@ # Hit On -[](https://hackage.haskell.org/package/hit-on)-[](LICENSE)+[](https://travis-ci.org/kowainik/hit-on)+[](https://hackage.haskell.org/package/hit-on) [](http://stackage.org/lts/package/hit-on) [](http://stackage.org/nightly/package/hit-on)-[](https://travis-ci.org/kowainik/hit-on)+[](LICENSE) **Kowainik Git Workflow Helper Tool.** @@ -17,15 +17,28 @@ Here is an example of how you can see the list of issues and the issue description with `hit`: -+ +Or how you can see pretty short stats about your changes:+++ ## Getting started ### Prerequisites To start using `hit` make sure that you have the following tools installed on your machine: -* [ `git`][git] — `hit` is a wrapper around `git`++ [ `git`][git] — `hit` is a wrapper around `git`++ **Optional:** `diff-highlight` — for pretty output of the `hit diff` command+ + Linux installation instructions+ ```shell+ cd /usr/share/doc/git/contrib/diff-highlight/+ sudo make+ sudo chmod +x diff-highlight+ sudo ln -s diff-highlight /usr/local/bin/diff-highlight+ ```+ + [macOS installation instructions](https://www.viget.com/articles/dress-up-your-git-diffs-with-word-level-highlights/) ### Installation @@ -89,11 +102,16 @@ Follow the steps below to configure `hit` : -1. Specify your GitHub login in the global `.gitconfig`+1. Enable autocompletion by calling the following command:+ ```shell+ source <(hit --bash-completion-script `which hit`)+ ```+ Add it your personal config file (like `~/.bashrc`) to enable automatically.+2. Specify your GitHub login in the global `.gitconfig` ```shell git config --global user.login <your_login> ```-2. **This step is only required if you want to use `hit` with private repositories**.+3. **This step is only required if you want to use `hit` with private repositories**. 1. [Create OAuth token on GitHub.](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) The following scopes for the token should be specified:  @@ -103,6 +121,29 @@ export GITHUB_TOKEN=<paste_generated_token_here> ``` +## Commands++ | Command | Description |+ |----------|----------------------------------------------------------------------------|+ | hop | Switch to branch and sync it |+ | fresh | Rebase current branch on remote one |+ | new | Create new branch from the current one |+ | stash | Stash all local changes |+ | unstash | Unstash previously stashed changes |+ | commit | Commit all local changes and prepend issue number |+ | uncommit | Reset to the previous commit saving the changes |+ | fix | Fix requested changes to the last commit |+ | amend | Amend changes to the last commit and force push |+ | issue | Show the information about the issue |+ | push | Push the current branch |+ | sync | Sync local branch with its remote |+ | resolve | Switch to master, sync and delete the branch |+ | clear | Remove all local changes permanently |+ | current | Show info about current branch and issue (if applicable) |+ | status | Show current branch and beautiful stats with COMMIT_HASH (by default HEAD) |+ | diff | Display beautiful diff with COMMIT_HASH (by default HEAD) |+ | clone | Clone the repo. Use 'reponame' or 'username/reponame' formats |+ ## Usage The best way to demonstrate the power of the `hit` tool on a day-to-day basis with our workflow is to go through the entire workflow step by step, solving an ordinary problem of the typical [`git`][git] user.@@ -174,6 +215,28 @@ It uses the issue title to generate a short description. +### hit status++Before commiting your changes, you may want to inspect short stats about your+work. With `git` you usually call the following command:++```shell+git status+```++However, the same `hit` command produces better output:++```shell+hit status+```++++### hit diff++If you want to see detailed diff of your changes, use `hit diff` command. If you+have `diff-hightlight` installed then `hit diff` outputs much nicer diffs.+ ### hit commit After finishing your work on that issue, you need to commit your changes. With `git` you would do the following:@@ -191,6 +254,15 @@ hit commit "Implement my feature" ``` +or even simplier:++```shell+hit commit+```++And the commit name would be the title of the corresponding issue at GitHub (if+you are currently in the branch named as described above).+ Note that you don't need to keep in mind the current issue number. However, if you want to refresh the context about the issue, use the `hit current` command. ### hit push@@ -208,6 +280,12 @@ ```shell hit push ```++> __Note:__ `hit push` command can be combined with the `hit commit` command+> using `-p|push` flag in the latter command.+> ```shell+> hit commit --push+> ``` ### hit sync
hit-on.cabal view
@@ -1,47 +1,26 @@-cabal-version: 2.0+cabal-version: 2.4 name: hit-on-version: 0.0.0+version: 0.1.0.0 synopsis: Haskell Git Helper Tool description: Haskell Git Helper Tool homepage: https://github.com/kowainik/hit-on bug-reports: https://github.com/kowainik/hit-on/issues license: MPL-2.0 license-file: LICENSE-author: Kowainik-maintainer: xrom.xkov@gmail.com+author: Veronika Romashkina, Dmitrii Kovanikov+maintainer: Kowainik <xrom.xkov@gmail.com> copyright: 2019 Kowainik category: Git, CLI Tool build-type: Simple extra-doc-files: README.md , CHANGELOG.md-tested-with: GHC == 8.6.3+tested-with: GHC == 8.6.5 source-repository head type: git location: https://github.com/kowainik/hit-on.git -library- hs-source-dirs: src- exposed-modules: Hit- Hit.Cli- Hit.ColorTerminal- Hit.Git- Hit.Issue-- autogen-modules: Paths_hit_on- other-modules: Paths_hit_on- Prelude-- build-depends: base-noprelude ^>= 4.12.0.0- , ansi-terminal >= 0.8- , github ^>= 0.20- , gitrev ^>= 1.3- , optparse-applicative ^>= 0.14- , relude ^>= 0.4- , shellmet >= 0.0.0- , text- , vector ^>= 0.12-+common common-options ghc-options: -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates@@ -67,7 +46,35 @@ TypeApplications ViewPatterns +library+ import: common-options+ hs-source-dirs: src+ exposed-modules: Hit+ Hit.Cli+ Hit.ColorTerminal+ Hit.Core+ Hit.Git+ Hit.Git.Status+ Hit.Issue++ autogen-modules: Paths_hit_on+ other-modules: Paths_hit_on+ Prelude++ build-depends: base-noprelude ^>= 4.12.0.0+ , ansi-terminal >= 0.8+ , directory ^>= 1.3+ , github ^>= 0.20+ , gitrev ^>= 1.3+ , optparse-applicative ^>= 0.14+ , process ^>= 1.6+ , relude ^>= 0.5+ , shellmet >= 0.0.0+ , text+ , vector ^>= 0.12+ executable hit+ import: common-options hs-source-dirs: app main-is: Main.hs @@ -78,26 +85,20 @@ -threaded -rtsopts -with-rtsopts=-N- -Wincomplete-uni-patterns- -Wincomplete-record-updates- -Wcompat- -Widentities- -Wredundant-constraints- -fhide-source-paths- -Wmissing-export-lists- -Wpartial-fields - default-language: Haskell2010- default-extensions: ConstraintKinds- DeriveGeneric- GeneralizedNewtypeDeriving- InstanceSigs- KindSignatures- LambdaCase- OverloadedStrings- RecordWildCards- ScopedTypeVariables- StandaloneDeriving- TupleSections- TypeApplications- ViewPatterns++test-suite hit-on-test+ import: common-options+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Main.hs++ build-depends: base+ , text+ , hspec+ , github+ , hit-on++ ghc-options: -threaded+ -rtsopts+ -with-rtsopts=-N
src/Hit/Cli.hs view
@@ -9,13 +9,15 @@ import Data.Version (showVersion) import Development.GitRev (gitCommitDate, gitDirty, gitHash)-import Options.Applicative (Parser, ParserInfo, argument, auto, command, execParser, fullDesc, help,- helper, info, infoOption, long, metavar, progDesc, short, strArgument,- subparser, switch)+import Options.Applicative (CommandFields, Mod, Parser, ParserInfo, argument, auto, command,+ execParser, flag, fullDesc, help, helper, info, infoOption, long,+ metavar, progDesc, short, strArgument, subparser, switch) import Hit.ColorTerminal (arrow, blueCode, boldCode, redCode, resetCode)-import Hit.Git (getUsername, runAmend, runClone, runCommit, runCurrent, runFix, runFresh, runHop,- runNew, runPush, runResolve, runSync)+import Hit.Core (CommitOptions (..), PushBool (..))+import Hit.Git (getUsername, runAmend, runClear, runClone, runCommit, runCurrent, runDiff, runFix,+ runFresh, runHop, runNew, runPush, runResolve, runStash, runStatus, runSync,+ runUncommit, runUnstash) import Hit.Issue (runIssue) import qualified Data.Text as T@@ -30,13 +32,19 @@ Issue issueNum me -> if me then getUsername >>= runIssue issueNum . Just else runIssue issueNum Nothing- Commit message noIssue -> runCommit message noIssue- Fix message -> runFix message+ Stash -> runStash+ Unstash -> runUnstash+ Commit opts -> runCommit opts+ Uncommit -> runUncommit+ Fix message pushBool -> runFix message pushBool Amend -> runAmend Resolve branchName -> runResolve branchName Push isForce -> runPush isForce Sync -> runSync+ Clear isForce -> runClear isForce Current -> runCurrent >>= flip whenJust (flip runIssue Nothing . Just)+ Status commit -> runCurrent >> runStatus commit+ Diff commit -> runDiff commit Clone name -> runClone name ----------------------------------------------------------------------------@@ -52,32 +60,49 @@ data HitCommand = Hop (Maybe Text) | Fresh (Maybe Text)- | New Int+ | New Text | Issue (Maybe Int) Bool- | Commit Text Bool- | Fix (Maybe Text)+ | Stash+ | Unstash+ | Commit CommitOptions+ | Uncommit+ | Fix+ (Maybe Text) -- ^ Text of the fix commit+ PushBool -- ^ Force push | Amend | Resolve (Maybe Text)- | Push Bool+ | Push PushBool | Sync+ | Clear PushBool | Current+ | Status (Maybe Text)+ | Diff (Maybe Text) | Clone Text -- | Commands parser. hitP :: Parser HitCommand hitP = subparser- $ command "hop" (info (helper <*> hopP) $ progDesc "Switch to branch and sync it")- <> command "fresh" (info (helper <*> freshP) $ progDesc "Rebase current branch on remote one")- <> command "new" (info (helper <*> newP) $ progDesc "Create new branch from current one")- <> command "commit" (info (helper <*> commitP) $ progDesc "Commit all local changes and prepend issue number")- <> command "fix" (info (helper <*> fixP) $ progDesc "Fix requested changes to the last commit")- <> command "amend" (info (helper <*> amendP) $ progDesc "Amend changes to the last commit and force push")- <> command "issue" (info (helper <*> issueP) $ progDesc "Show the information about the issue")- <> command "push" (info (helper <*> pushP) $ progDesc "Push the current branch")- <> command "sync" (info (helper <*> syncP) $ progDesc "Sync local branch with its remote")- <> command "resolve" (info (helper <*> resolveP) $ progDesc "Switch to master, sync and delete the branch")- <> command "current" (info (helper <*> currentP) $ progDesc "Show info about current branch and issue (if applicable)")- <> command "clone" (info (helper <*> cloneP) $ progDesc "Clone the repo. Use 'reponame' or 'username/reponame' formats")+ $ com "hop" hopP "Switch to branch and sync it"+ <> com "fresh" freshP "Rebase current branch on remote one"+ <> com "new" newP "Create new branch from the current one"+ <> com "stash" stashP "Stash all local changes"+ <> com "unstash" unstashP "Unstash previously stashed changes"+ <> com "commit" commitP "Commit all local changes and prepend issue number"+ <> com "uncommit" uncommitP "Reset to the previous commit saving the changes"+ <> com "fix" fixP "Fix requested changes to the last commit"+ <> com "amend" amendP "Amend changes to the last commit and force push"+ <> com "issue" issueP "Show the information about the issue"+ <> com "push" pushP "Push the current branch"+ <> com "sync" syncP "Sync local branch with its remote"+ <> com "resolve" resolveP "Switch to master, sync and delete the branch"+ <> com "clear" clearP "Remove all local changes permanently"+ <> com "current" currentP "Show info about current branch and issue (if applicable)"+ <> com "status" statusP "Show current branch and beautiful stats with COMMIT_HASH (by default HEAD)"+ <> com "diff" diffP "Display beautiful diff with COMMIT_HASH (by default HEAD)"+ <> com "clone" cloneP "Clone the repo. Use 'reponame' or 'username/reponame' formats"+ where+ com :: String -> Parser HitCommand -> String -> Mod CommandFields HitCommand+ com name p desc = command name (info (helper <*> p) $ progDesc desc) hopP :: Parser HitCommand hopP = Hop <$> maybeBranchP@@ -86,7 +111,7 @@ freshP = Fresh <$> maybeBranchP newP :: Parser HitCommand-newP = New <$> issueNumP+newP = New <$> strArgument (metavar "ISSUE_NUMBER_OR_BRANCH_NAME") issueP :: Parser HitCommand issueP = do@@ -97,34 +122,57 @@ <> help "Assigned to me" pure $ Issue num me +stashP :: Parser HitCommand+stashP = pure Stash++unstashP :: Parser HitCommand+unstashP = pure Unstash+ commitP :: Parser HitCommand commitP = do- msg <- strArgument (metavar "COMMIT_MESSAGE")- noIssue <- switch+ coName <- optional $ strArgument (metavar "COMMIT_MESSAGE")+ coNoIssueNumber <- switch $ long "no-issue" <> short 'n' <> help "Do not add [#ISSUE_NUMBER] prefix when specified"- pure $ Commit msg noIssue+ coPush <- switch+ $ long "push"+ <> short 'p'+ <> help "Push current branch with this commit"+ coIsForcePush <- pushBoolP+ pure $ Commit CommitOptions{..} +uncommitP :: Parser HitCommand+uncommitP = pure Uncommit++{- HLINT ignore "Use <$>"-} fixP :: Parser HitCommand-fixP = Fix <$> commitMessageP+fixP = do+ commitMsg <- commitMessageP+ isForce <- pushBoolP+ pure $ Fix commitMsg isForce amendP :: Parser HitCommand amendP = pure Amend pushP :: Parser HitCommand-pushP = Push <$> switch- ( long "force"- <> short 'f'- <> help "Force push"- )+pushP = Push <$> pushBoolP syncP :: Parser HitCommand syncP = pure Sync +clearP :: Parser HitCommand+clearP = Clear <$> pushBoolP+ currentP :: Parser HitCommand currentP = pure Current +statusP :: Parser HitCommand+statusP = Status <$> maybeCommitP++diffP :: Parser HitCommand+diffP = Diff <$> maybeCommitP+ resolveP :: Parser HitCommand resolveP = Resolve <$> maybeBranchP @@ -133,11 +181,23 @@ -- | Parse optional branch name as an argument. maybeBranchP :: Parser (Maybe Text)-maybeBranchP = optional $ strArgument (metavar "BRANCH_NAME")+maybeBranchP = optional $ strArgument $ metavar "BRANCH_NAME" --- / Parse optional commit message as an argument+-- | Parse optional commit hash as an argument.+maybeCommitP :: Parser (Maybe Text)+maybeCommitP = optional $ strArgument $ metavar "COMMIT_HASH"++-- | Parse optional commit message as an argument commitMessageP :: Parser (Maybe Text)-commitMessageP = optional $ strArgument (metavar "COMMIT_MESSAGE")+commitMessageP = optional $ strArgument $ metavar "COMMIT_MESSAGE"++-- | Parse flag of force push.+pushBoolP :: Parser PushBool+pushBoolP = flag Simple Force+ ( long "force"+ <> short 'f'+ <> help "Force push"+ ) -- | Parse issue number as an argument. issueNumP :: Parser Int
src/Hit/ColorTerminal.hs view
@@ -4,17 +4,11 @@ ( Color (..) , putStrFlush , beautyPrint- , boldCode- , blueCode- , blueBg , bold , boldText , boldDefault- , greenCode , italic- , redCode , reset- , resetCode , setColor , successMessage , warningMessage@@ -22,6 +16,23 @@ , infoMessage , skipMessage + -- * Colors+ , redCode+ , blueCode+ , greenCode+ , yellowCode+ , cyanCode+ , magentaCode++ -- * Other formatters+ , boldCode+ , resetCode+ , blueBg++ -- * Input with prompt+ , Answer (..)+ , prompt+ , arrow ) where @@ -30,7 +41,9 @@ SGR (..), setSGR, setSGRCode) import System.IO (hFlush) +import qualified Data.Text as T + -- | Explicit flush ensures prompt messages are in the correct order on all systems. putStrFlush :: Text -> IO () putStrFlush msg = do@@ -79,14 +92,42 @@ infoMessage = colorMessage Blue skipMessage = colorMessage Cyan -blueCode, greenCode, boldCode, redCode, resetCode, blueBg :: Text-redCode = toText $ setSGRCode [SetColor Foreground Vivid Red]-blueCode = toText $ setSGRCode [SetColor Foreground Vivid Blue]-greenCode = toText $ setSGRCode [SetColor Foreground Vivid Green]-boldCode = toText $ setSGRCode [SetConsoleIntensity BoldIntensity]-resetCode = toText $ setSGRCode [Reset]-blueBg = toText $ setSGRCode [SetColor Foreground Dull White, SetColor Background Dull Blue]+redCode, blueCode, greenCode, yellowCode, cyanCode, magentaCode :: Text+redCode = mkColor Red+blueCode = mkColor Blue+greenCode = mkColor Green+yellowCode = mkColor Yellow+cyanCode = mkColor Cyan+magentaCode = mkColor Magenta +boldCode, resetCode, blueBg :: Text+boldCode = toText $ setSGRCode [SetConsoleIntensity BoldIntensity]+resetCode = toText $ setSGRCode [Reset]+blueBg = toText $ setSGRCode [SetColor Foreground Dull White, SetColor Background Dull Blue]++mkColor :: Color -> Text+mkColor color = toText $ setSGRCode [SetColor Foreground Vivid color]+ -- | Arrow symbol arrow :: Text-arrow = " ➤ "+arrow = " ➤ "++-- | Represents a user's answer+data Answer = Y | N++-- | Parse an answer to 'Answer'+yesOrNo :: Text -> Maybe Answer+yesOrNo (T.toLower -> answer )+ | T.null answer = Just Y+ | answer `elem` ["yes", "y", "ys"] = Just Y+ | answer `elem` ["no", "n"] = Just N+ | otherwise = Nothing++prompt :: IO Answer+prompt = do+ answer <- yesOrNo . T.strip <$> getLine+ case answer of+ Just ans -> pure ans+ Nothing -> do+ errorMessage "This wasn't a valid choice."+ prompt
+ src/Hit/Core.hs view
@@ -0,0 +1,28 @@+{- | This module contains core data types used in the package.+-}++module Hit.Core+ ( PushBool (..)+ , CommitOptions (..)+ ) where+++-- | Data type to represent the type of @push@: force-push or not.+data PushBool+ = Simple+ | Force+ deriving (Show, Eq)++-- | Options of the @hit commit@ command.+data CommitOptions = CommitOptions+ { {- | Commit name. If not specified use the issue name.+ If issue number is not applicable do not perform any actions.+ -}+ coName :: !(Maybe Text)+ -- | Do not use the issue num in the commit name.+ , coNoIssueNumber :: !Bool+ -- | Push immediately.+ , coPush :: !Bool+ -- | Use Force push?+ , coIsForcePush :: !PushBool+ }
src/Hit/Git.hs view
@@ -8,20 +8,32 @@ , runNew , runPush , runResolve+ , runStash+ , runUnstash , runCommit+ , runUncommit , runFix , runAmend , runSync+ , runClear , runCurrent+ , runStatus+ , runDiff , runClone , getUsername ) where +import Control.Exception (bracket) import Data.Char (isAlphaNum, isDigit, isSpace) import Shellmet (($|))+import System.Directory (findExecutable)+import System.Process (callCommand) -import Hit.ColorTerminal (arrow, errorMessage, greenCode, resetCode)+import Hit.ColorTerminal (Answer (..), arrow, boldCode, errorMessage, greenCode, infoMessage,+ prompt, resetCode)+import Hit.Core (CommitOptions (..), PushBool (..))+import Hit.Git.Status (showPrettyDiff) import Hit.Issue (getIssueTitle, mkIssueId) import qualified Data.Text as T@@ -40,13 +52,15 @@ "git" ["rebase", "origin/" <> branch] -- | @hit new@ command.-runNew :: Int -> IO ()-runNew issueNum = do+runNew :: Text -> IO ()+runNew issueOrName = do login <- getUsername- let issueId = mkIssueId issueNum- issueTitle <- getIssueTitle issueId- let shortDesc = mkShortDesc issueTitle- let branchName = login <> "/" <> show issueNum <> "-" <> shortDesc+ title <- case readMaybe @Int $ toString issueOrName of+ Just issueNum -> do+ issueTitle <- getIssueTitle $ mkIssueId issueNum+ pure $ show issueNum <> "-" <> mkShortDesc issueTitle+ Nothing -> pure $ mkShortDesc issueOrName+ let branchName = login <> "/" <> title "git" ["checkout", "-b", branchName] where mkShortDesc :: Text -> Text@@ -54,51 +68,90 @@ T.intercalate "-" . take 5 . words- . T.filter (\c -> isAlphaNum c || isDigit c || isSpace c)+ . T.filter (\c -> isAlphaNum c+ || isDigit c+ || isSpace c+ || c `elem` ("_-./" :: String)+ ) -- | @hit commit@ command.-runCommit :: Text -> Bool -> IO ()-runCommit (T.strip -> msg) (not -> hasIssue)- | msg == "" = errorMessage "Commit message cannot be empty"- | otherwise = do- branch <- getCurrentBranch- let issueNum = issueFromBranch branch- "git" ["add", "."]- "git" ["commit", "-m", showMsg $ guard hasIssue *> issueNum]+runCommit :: CommitOptions -> IO ()+runCommit CommitOptions{..} = case coName of+ Just (T.strip -> msg)+ | msg == "" -> errorMessage "Commit message cannot be empty"+ | otherwise -> getCurrentIssue >>= commitCmds msg+ {- if the commit name is not specified then check the branchName+ If this is issue-related branch, take the issue name as the commit name.+ Otherwise print errorMessage.+ -}+ Nothing -> do+ issueNum <- getCurrentIssue+ case issueNum of+ Nothing -> errorMessage "Commit message cannot be empty: can not be taken from the context"+ Just n -> do+ title <- getIssueTitle (mkIssueId n)+ commitCmds title issueNum where- showMsg :: Maybe Int -> Text- showMsg = \case+ commitCmds :: Text -> Maybe Int -> IO ()+ commitCmds msg issueNum = do+ "git" ["add", "."]+ "git" ["commit", "-m", showMsg msg $ guard hasIssue *> issueNum]+ when (coPush || coIsForcePush == Force) $ runPush coIsForcePush++ getCurrentIssue :: IO (Maybe Int)+ getCurrentIssue = issueFromBranch <$> getCurrentBranch++ showMsg :: Text -> Maybe Int -> Text+ showMsg msg = \case Nothing -> msg Just n -> let issue = "#" <> show n in "[" <> issue <> "] " <> msg <> "\n\nResolves " <> issue --- / @hit fix@ command-runFix :: Maybe Text -> IO ()-runFix msg = do+ hasIssue :: Bool+ hasIssue = not coNoIssueNumber++-- | @hit uncommit@ command+runUncommit :: IO ()+runUncommit = "git" ["reset", "HEAD~1"]++-- | @hit fix@ command+runFix :: Maybe Text -> PushBool -> IO ()+runFix msg pushBool = do "git" ["add", "."] "git" ["commit", "-m", message]- runPush False- where- message = fromMaybe "Fix after review" msg+ runPush pushBool+ where+ message :: Text+ message = fromMaybe "Fix" msg +-- | @hit stash@ command: save all local changes to stash.+runStash :: IO ()+runStash = do+ "git" ["add", "."]+ "git" ["stash"] +-- | @hit unstash@ command: pop all saved changes.+runUnstash :: IO ()+runUnstash = "git" ["stash", "pop"]+ -- | @hit amend@ command. runAmend :: IO () runAmend = do "git" ["add", "."] "git" ["commit", "--amend", "--no-edit"]- runPush True+ runPush Force -- | @hit push@ command.-runPush :: Bool -> IO ()+runPush :: PushBool -> IO () runPush isForce = getCurrentBranch >>= \branch -> "git" $ ["push", "--set-upstream", "origin", branch]- ++ ["--force" | isForce]+ ++ ["--force" | isForce == Force] -- | @hit sync@ command. runSync :: IO ()-runSync = getCurrentBranch >>= \branch -> "git" ["pull", "--rebase", "origin", branch]+runSync = getCurrentBranch >>= \branch ->+ "git" ["pull", "--rebase", "origin", branch] -- | @hit resolve@ command. runResolve :: Maybe Text -> IO ()@@ -107,6 +160,25 @@ runHop $ Just master when (curBranch /= master) $ "git" ["branch", "-D", curBranch] +-- | Remove all local changes permanently.+runClear :: PushBool -> IO ()+runClear = \case+ Force -> clearChanges+ Simple -> do+ putText $ unlines+ [ "This command permanently deletes all uncommited changes"+ , "Hint: if you want to save changes, use 'hit stash' command."+ , "Are you sure you want to delete changes? " <> boldCode <> "[y]" <> resetCode <> "/n"+ ]+ prompt >>= \case+ N -> infoMessage "Aborting local clean up"+ Y -> clearChanges+ where+ clearChanges :: IO ()+ clearChanges = do+ "git" ["add", "."]+ "git" ["reset", "--hard"]+ {- | Part of the @hit current@ command. Prints the current branch and returns the current issue number if possible. -}@@ -116,6 +188,22 @@ putTextLn $ arrow <> "Current branch: " <> greenCode <> branchName <> resetCode pure $ issueFromBranch branchName +{- | Show stats from the given commit. If commit is not specified, uses HEAD.+-}+runStatus :: Maybe Text -> IO ()+runStatus (fromMaybe "HEAD" -> commit) = withUntrackedFiles $ showPrettyDiff commit++{- | Show diff from the given commit. If commit is not specified, uses HEAD.+This commands checks whether @diff-hightligh@ is on path and if not, just calls+@git diff@.+-}+runDiff :: Maybe Text -> IO ()+runDiff (fromMaybe "HEAD" -> commit) = withUntrackedFiles $+ findExecutable "diff-highlight" >>= \case+ Nothing -> "git" ["diff", commit]+ Just _ -> callCommand $ toString $+ "git diff " <> commit <> " --color=always | diff-highlight | less -rFX"+ {- | @hit clone@ command receives the name of the repo in the following formats: @@ -151,7 +239,6 @@ then errorMessage "user.login is not specified" >> exitFailure else pure login - nameOrMaster :: Maybe Text -> Text nameOrMaster = fromMaybe "master" @@ -172,3 +259,23 @@ . T.takeWhile isDigit . T.drop 1 . T.dropWhile (/= '/')++{- | Perform given action by adding all untracked files to index and returning+them back after action.+-}+withUntrackedFiles :: IO a -> IO a+withUntrackedFiles action = bracket+ addUntrackedFiles+ removeUntrackedFiles+ (const action)+ where+ -- Add all untracked file to index so they will appear in diff+ addUntrackedFiles :: IO [Text]+ addUntrackedFiles = do+ untrackedFiles <- lines <$> "git" $| ["ls-files", "--others", "--exclude-standard"]+ for_ untrackedFiles $ \file -> void $ "git" $| ["add", file]+ pure untrackedFiles++ -- Return untracked files back to not spoil git state and have unexpected behavior+ removeUntrackedFiles :: [Text] -> IO ()+ removeUntrackedFiles = mapM_ $ \file -> void $ "git" $| ["reset", file]
+ src/Hit/Git/Status.hs view
@@ -0,0 +1,177 @@+{- | Data types that describe model of the @git diff@ command to display diffs+in pretty way.+-}++module Hit.Git.Status+ ( showPrettyDiff+ ) where++import Shellmet (($?), ($|))+import System.Process (callCommand)++import Hit.ColorTerminal (blueCode, boldCode, cyanCode, greenCode, magentaCode, redCode, resetCode,+ yellowCode)++import qualified Data.Text as T+++-- | Enum that represents all possible types of file modifications.+data PatchType+ = Added+ | Copied+ | Deleted+ | Modified+ | Renamed+ | TypeChanged+ | Unmerged+ | Unknown+ | BrokenPairing++-- | Map conventional characters to 'PatchType'+parsePatchType :: Text -> Maybe PatchType+parsePatchType = \case+ "A" -> Just Added+ "C" -> Just Copied+ "D" -> Just Deleted+ "M" -> Just Modified+ "R" -> Just Renamed+ "T" -> Just TypeChanged+ "U" -> Just Unmerged+ "X" -> Just Unknown+ "B" -> Just BrokenPairing+ _ -> Nothing++-- | Display 'PatchType' in colorful and expanded text.+displayPatchType :: PatchType -> Text+displayPatchType = \case+ Added -> coloredIn greenCode "added"+ Copied -> coloredIn blueCode "copied"+ Deleted -> coloredIn redCode "deleted"+ Modified -> coloredIn magentaCode "modified"+ Renamed -> coloredIn yellowCode "renamed"+ TypeChanged -> coloredIn cyanCode "type-changed"+ Unmerged -> inBold "unmerged"+ Unknown -> inBold "unknown"+ BrokenPairing -> inBold "broken"+ where+ coloredIn :: Text -> Text -> Text+ coloredIn color text = color <> inBold text++ inBold :: Text -> Text+ inBold text = boldCode <> text <> resetCode++-- | Output of the @git diff --name-status@ command.+data DiffName = DiffName+ { diffNameFile :: !Text -- ^ file name+ , diffNameType :: !PatchType -- ^ type of the changed file+ }++parseDiffName :: [Text] -> Maybe DiffName+parseDiffName [t, name] = DiffName name <$> parsePatchType t+parseDiffName _ = Nothing++-- | Output of the @git diff --stat@ command.+data DiffStat = DiffStat+ { diffStatFile :: !Text -- ^ file name+ , diffStatCount :: !Text -- ^ number of changed lines+ , diffStatSigns :: !Text -- ^ + and - stats+ }++{- | This command parses diff stats in the following format:++@+<filename> | <n> <pluses-and-minuses>+@++It also handles special case of binary files. Typical raw text returned by @git@+can look like this:++@+ .foo.un~ | Bin 0 -> 523 bytes+ README.md | 4 +++++ foo | 1 ++@+-}+parseDiffStat :: [Text] -> Maybe DiffStat+parseDiffStat = \case+ [diffStatFile, diffStatCount, diffStatSigns] -> Just DiffStat{..}+ diffStatFile:"Bin":rest -> Just DiffStat+ { diffStatCount = "Bin"+ , diffStatSigns = unwords rest+ , ..+ }+ _ -> Nothing++showPrettyDiff :: Text -> IO ()+showPrettyDiff commit = do+ -- 1. Check rebase in progress and tell about it+ whenM isRebaseInProgress $ do+ putTextLn gitRebaseHelp+ showConlictFiles++ -- 2. Output pretty diff+ diffName <- map words . lines <$> "git" $| ["diff", commit, "--name-status"]+ diffStat <- map toStats . lines <$> "git" $| ["diff", commit, "--stat", "--color=always"]+ let fileTypes = sortWith diffNameFile $ mapMaybe parseDiffName diffName+ let fileStats = sortWith diffStatFile $ mapMaybe parseDiffStat diffStat+ let rows = zipWith joinDiffs fileTypes fileStats+ putText $ formatTableAligned rows+ where+ toStats :: Text -> [Text]+ toStats = foldMap words . T.split (== '|')++ joinDiffs :: DiffName -> DiffStat -> (Text, Text, Text, Text)+ joinDiffs DiffName{..} DiffStat{..} =+ (displayPatchType diffNameType, diffNameFile, diffStatCount, diffStatSigns)++ formatTableAligned :: [(Text, Text, Text, Text)] -> Text+ formatTableAligned rows = unlines $ map formatRow rows+ where+ formatRow :: (Text, Text, Text, Text) -> Text+ formatRow (fileType, fileName, fileCount, fileSigns) =+ padRight typeSize fileType+ <> " "+ <> padRight nameSize fileName+ <> " | "+ <> padLeft countSize fileCount+ <> " "+ <> fileSigns++ padRight :: Int -> Text -> Text+ padRight n t = t <> T.replicate (n - T.length t) " "++ padLeft :: Int -> Text -> Text+ padLeft n t = T.replicate (n - T.length t) " " <> t++ typeSize, nameSize :: Int+ typeSize = maxOn (\(a, _, _, _) -> a) rows+ nameSize = maxOn (\(_, b, _, _) -> b) rows+ countSize = maxOn (\(_, _, c, _) -> c) rows++ maxOn :: (a -> Text) -> [a] -> Int+ maxOn f = foldl' (\acc a -> max acc $ T.length $ f a) 0++{- | Returns 'True' if rebase is in progress. Calls magic comand and if this+command exits with code 1 then there's no rebase in progress.+-}+isRebaseInProgress :: IO Bool+isRebaseInProgress = do+ let checkRebaseCmd = callCommand "ls `git rev-parse --git-dir` | grep rebase > /dev/null 2>&1"+ True <$ checkRebaseCmd $? pure False++gitRebaseHelp :: Text+gitRebaseHelp = unlines+ [ ""+ , boldCode <> yellowCode <> "Rebase in progress! What you can do:" <> resetCode+ , " " <> cyanCode <> "git rebase --continue " <> resetCode <> ": after fixing conflicts"+ , " " <> cyanCode <> "git rebase --skip " <> resetCode <> ": to skip this patch"+ , " " <> cyanCode <> "git rebase --abort " <> resetCode <> ": to abort to the original branch"+ ]++showConlictFiles :: IO ()+showConlictFiles = do+ conflictFiles <- lines <$> "git" $| ["diff", "--name-only", "--diff-filter=U"]+ unless (null conflictFiles) $+ putTextLn $ unlines $+ ( boldCode <> redCode <> "Conflict files:" <> resetCode )+ : map (" " <>) conflictFiles
+ test/Main.hs view
@@ -0,0 +1,32 @@++module Main (main) where++import GitHub.Data.Name (Name (..))+import Test.Hspec (describe, hspec, it, shouldBe)++import Hit.Issue (parseOwnerRepo)+++main :: IO ()+main = hspec $ do+ describe "parseOwnerRepo" $ do+ let expectedOwnerName = N "kowainik"+ let expectedRepoName = N "hit-on"+ let expectedCredentials = Just (expectedOwnerName, expectedRepoName)+ it "Parses a GitHub repo link (SSH) and gives use the Owner Name and Repo Name separately" $ do+ parseOwnerRepo "git@github.com:kowainik/hit-on.git" `shouldBe` expectedCredentials+ it "Parses a GitHub repo link (SSH) and gives use the Owner Name and Repo Name separately" $ do+ parseOwnerRepo "git@github.com:kowainik/hit-on" `shouldBe` expectedCredentials+ it "Parses a GitHub repo link (HTTPS) and gives use the Owner Name and Repo Name separately" $ do+ parseOwnerRepo "https://github.com/kowainik/hit-on.git" `shouldBe` expectedCredentials+ it "Parses a GitHub repo link (HTTPS) and gives use the Owner Name and Repo Name separately" $ do+ parseOwnerRepo "https://github.com/kowainik/hit-on" `shouldBe` expectedCredentials+ it "Parses an invalid GitHub repo link and returns a `Nothing`" $ do+ parseOwnerRepo "github.com/kowainik/hit-on" `shouldBe` Nothing+ it "Parses an invalid GitHub repo link and returns a `Nothing`" $ do+ parseOwnerRepo "https://githu.com/kowainik/hit-on" `shouldBe` Nothing+ it "Parses an invalid GitHub repo link and returns a `Nothing`" $ do+ parseOwnerRepo "https://github.com/kowainik" `shouldBe` Nothing+ it "Parses an invalid GitHub repo link and returns a `Nothing`" $ do+ parseOwnerRepo "git@github.com" `shouldBe` Nothing+