packages feed

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 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  ![hit-on logo](https://user-images.githubusercontent.com/4276606/53816638-d86e4a00-3f9e-11e9-83ab-74032363292f.png)-[![Hackage](https://img.shields.io/hackage/v/hit-on.svg)](https://hackage.haskell.org/package/hit-on)-[![MPL-2.0 license](https://img.shields.io/badge/license-MPL--2.0-blue.svg)](LICENSE)+[![Build status](https://img.shields.io/travis/kowainik/hit-on.svg?logo=travis)](https://travis-ci.org/kowainik/hit-on)+[![Hackage](https://img.shields.io/hackage/v/hit-on.svg?logo=haskell)](https://hackage.haskell.org/package/hit-on) [![Stackage Lts](http://stackage.org/package/hit-on/badge/lts)](http://stackage.org/lts/package/hit-on) [![Stackage Nightly](http://stackage.org/package/hit-on/badge/nightly)](http://stackage.org/nightly/package/hit-on)-[![Build status](https://secure.travis-ci.org/kowainik/hit-on.svg)](https://travis-ci.org/kowainik/hit-on)+[![MPL-2.0 license](https://img.shields.io/badge/license-MPL--2.0-blue.svg)](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`: -![Screenshot from 2019-03-17 00-21-23](https://user-images.githubusercontent.com/8126674/54478225-cae07c00-484a-11e9-86b3-c4c54e281274.png)+![hit issue](https://user-images.githubusercontent.com/8126674/54478225-cae07c00-484a-11e9-86b3-c4c54e281274.png) +Or how you can see pretty short stats about your changes:++![hit status example](https://user-images.githubusercontent.com/4276606/60981099-428d6600-a368-11e9-97aa-656a3dd38efa.png)+ ## 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:     ![Screenshot from 2019-03-16 22-34-57](https://user-images.githubusercontent.com/4276606/54476778-30793c00-483c-11e9-9625-a4f6ced40820.png) @@ -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 status example](https://user-images.githubusercontent.com/4276606/60981099-428d6600-a368-11e9-97aa-656a3dd38efa.png)++### 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+