diff options
author | Alexander Foremny <aforemny@posteo.de> | 2024-03-14 07:10:03 +0100 |
---|---|---|
committer | Alexander Foremny <aforemny@posteo.de> | 2024-03-14 07:10:03 +0100 |
commit | 11284c7c12c44e12de1cfc712c0391d5ee32a9f2 (patch) | |
tree | 553a527ff19f5ef105cbc2f026284e75fa5900db /app | |
parent | c8ab97e77c8ab56b9835d9f260dc222a10e9b3c6 (diff) | |
parent | 09e26c37de7e7227d856ffe15c9554af36b50c58 (diff) |
Merge remote-tracking branch 'origin/feature/review'main
Diffstat (limited to 'app')
-rw-r--r-- | app/Comment.hs | 11 | ||||
-rw-r--r-- | app/Comment/Language.hs | 8 | ||||
-rw-r--r-- | app/Data/List/NonEmpty/Zipper/Extra.hs | 8 | ||||
-rw-r--r-- | app/Exception.hs | 8 | ||||
-rw-r--r-- | app/Extract.hs | 17 | ||||
-rw-r--r-- | app/Git.hs | 36 | ||||
-rw-r--r-- | app/Git/CommitHash.hs | 5 | ||||
-rw-r--r-- | app/Main.hs | 82 | ||||
-rw-r--r-- | app/Patch.hs | 29 | ||||
-rw-r--r-- | app/Render.hs | 16 | ||||
-rw-r--r-- | app/Review.hs | 219 |
11 files changed, 394 insertions, 45 deletions
diff --git a/app/Comment.hs b/app/Comment.hs index 123acec..febd47e 100644 --- a/app/Comment.hs +++ b/app/Comment.hs @@ -50,15 +50,8 @@ data Point = Point getComments :: Git.CommitHash -> FilePath -> IO [Comment] getComments commitHash filePath = fmap mergeLineComments - . ( extractComments - filePath - ( -- TODO Support amiguous file languages - -- - -- @backlog - N.head language - ) - . LB.toStrict - ) + . extractComments filePath language + . LB.toStrict =<< catch (Git.readTextFileOfBS commitHash filePath) (\(_ :: E.CannotReadFile) -> pure "") diff --git a/app/Comment/Language.hs b/app/Comment/Language.hs index 7a9963f..3f8c7a4 100644 --- a/app/Comment/Language.hs +++ b/app/Comment/Language.hs @@ -25,9 +25,13 @@ newtype Language = Language {languageKey :: L.LanguageKey} deriving (Eq, Show, Generic) deriving newtype (Binary) -fromPath :: FilePath -> N.NonEmpty Language +-- TODO Support amiguous file languages +-- +-- @backlog +fromPath :: FilePath -> Language fromPath fp = - fromMaybe (throw $ E.UnknownFile fp) + N.head + . fromMaybe (throw $ E.UnknownFile fp) . N.nonEmpty . map (Language . L.languageName) $ L.languagesForPath fp diff --git a/app/Data/List/NonEmpty/Zipper/Extra.hs b/app/Data/List/NonEmpty/Zipper/Extra.hs new file mode 100644 index 0000000..638a9bd --- /dev/null +++ b/app/Data/List/NonEmpty/Zipper/Extra.hs @@ -0,0 +1,8 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module Data.List.NonEmpty.Zipper.Extra where + +import Data.Binary (Binary) +import Data.List.NonEmpty.Zipper (Zipper) + +instance Binary a => Binary (Zipper a) diff --git a/app/Exception.hs b/app/Exception.hs index db7612b..6ac243b 100644 --- a/app/Exception.hs +++ b/app/Exception.hs @@ -8,6 +8,7 @@ module Exception InvalidIssue (..), CannotReadFile (..), UnsupportedLanguage (..), + NoAncestor (..), ) where @@ -16,6 +17,7 @@ import Control.Exception import Data.ByteString.Lazy.Char8 as LB import Data.Text qualified as T import Data.Void (Void) +import Git.CommitHash qualified as Git import System.Exit (ExitCode) import Text.Megaparsec qualified as P @@ -27,6 +29,7 @@ data AnyException | InvalidDiff' InvalidDiff | InvalidIssue' InvalidIssue | UnsupportedLanguage' UnsupportedLanguage + | NoAncestor' NoAncestor deriving (Show) instance Exception AnyException @@ -74,3 +77,8 @@ data UnsupportedLanguage = UnsupportedLanguage T.Text deriving (Show) instance Exception UnsupportedLanguage + +data NoAncestor = NoAncestor Git.CommitHash Git.CommitHash + deriving (Show) + +instance Exception NoAncestor diff --git a/app/Extract.hs b/app/Extract.hs deleted file mode 100644 index e351898..0000000 --- a/app/Extract.hs +++ /dev/null @@ -1,17 +0,0 @@ -module Extract where - -data Comment = Comment - { -- result fields - file :: String, - file_type :: FileType, - -- match fields - kind :: String, - name :: String, - text :: T.Text, - start :: Position, - end :: Position - } - -extractComments :: T.Text -> IO [Comment] -extractComments = do - parer <- ts_parser_new @@ -8,6 +8,9 @@ module Git getCommitOf, readTextFileOfText, readTextFileOfBS, + resolveRef, + getCommitsBetween, + diffOf, ) where @@ -27,7 +30,8 @@ import Data.Time.Clock (UTCTime, getCurrentTime) import Exception qualified as E import GHC.Generics (Generic) import Git.CommitHash -import Process (proc, sh) +import Patch qualified as A +import Process (proc, sh, sh_) import Text.Printf (printf) getCommitHashes :: IO (NonEmpty T.Text) @@ -91,7 +95,7 @@ readTextFileOfText :: CommitHash -> FilePath -> IO LT.Text readTextFileOfText = readTextFileOf LT.readFile LT.decodeUtf8 readTextFileOfBS :: CommitHash -> FilePath -> IO LB.ByteString -readTextFileOfBS = readTextFileOf LB.readFile (\x->x) +readTextFileOfBS = readTextFileOf LB.readFile id readTextFileOf :: (FilePath -> IO a) -> (LB.ByteString -> a) -> CommitHash -> FilePath -> IO a readTextFileOf readFile _ WorkingTree filePath = @@ -102,3 +106,31 @@ readTextFileOf _ decode (Commit hash) filePath = catch (decode <$> sh (proc "git show %:%" hash filePath)) (\(_ :: E.ProcessException) -> throwIO (E.CannotReadFile (printf "%s:%s" hash filePath))) + +resolveRef :: T.Text -> IO CommitHash +resolveRef = + fmap (Commit . T.strip . T.decodeUtf8 . LB.toStrict) + . sh + . proc "git rev-parse %" + +-- | `getCommitsBetween prevCommit commit` returns the commits from `prevCommit` to `commit`. The result excludes `prevCommit`, but includes `commit`. +-- +-- If `prevCommit` is not an ancestor of `commit`, this functions throws `NoAncestor commit prevCommit`. +getCommitsBetween :: CommitHash -> CommitHash -> IO [CommitHash] +getCommitsBetween WorkingTree commit@(Commit _) = + throwIO (E.NoAncestor WorkingTree commit) +getCommitsBetween WorkingTree WorkingTree = pure [WorkingTree] +getCommitsBetween prevCommit WorkingTree = + fmap (++ [WorkingTree]) . getCommitsBetween prevCommit + =<< resolveRef "HEAD" +getCommitsBetween prevCommit@(Commit prevHash) commit@(Commit hash) = do + catch + (sh_ (proc "git merge-base --is-ancestor % %" prevHash hash)) + (\(_ :: E.ProcessException) -> throwIO (E.NoAncestor commit prevCommit)) + map (Commit . T.strip) . T.lines . T.decodeUtf8 . LB.toStrict + <$> sh (proc "git log --format=%%H %..%" prevHash hash) + +diffOf :: CommitHash -> CommitHash -> IO A.Patch +diffOf prevHash hash = + A.parse . T.decodeUtf8 . LB.toStrict + <$> sh (proc "git diff % %" (toTextUnsafe prevHash) (toTextUnsafe hash)) diff --git a/app/Git/CommitHash.hs b/app/Git/CommitHash.hs index db7a478..0caecf4 100644 --- a/app/Git/CommitHash.hs +++ b/app/Git/CommitHash.hs @@ -2,6 +2,7 @@ module Git.CommitHash ( CommitHash (..), toShortText, toText, + toTextUnsafe, ) where @@ -23,6 +24,10 @@ toText :: CommitHash -> Maybe T.Text toText WorkingTree = Nothing toText (Commit hash) = Just hash +toTextUnsafe :: CommitHash -> T.Text +toTextUnsafe (Commit hash) = hash +toTextUnsafe _ = error "toTextUnsafe: WorkingDir" + instance P.Render CommitHash where render = P.render . P.Detailed diff --git a/app/Main.hs b/app/Main.hs index 52a316d..f9fedea 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -1,3 +1,39 @@ +-- TODO Add `anissue review-comments` +-- +-- The command `review-comments` should list all review comments within the current review. +-- +-- @assigned aforemny +-- @priority medium +-- @topic review + +-- TODO `anissue review --base` should take own commits into account +-- +-- To facilitate reviewing, the `--base` parameter of `anissue review` should implement some heuristic to review a set of changes multiple times. +-- +-- The first time a review is performed, `--base` should default to the base branch. Any subsequent time, it should default to the last review commit added by myself. +-- +-- @assigned aforemny +-- @priority high +-- @topic review + +-- TODO Add `anissue merge` +-- +-- The command `anissue merge` should merge the currenlty checked out feature request, if there are no unresolved review comments. +-- +-- If there are unresolved review comments, it should fail with a warning. +-- +-- @assigned aforemny +-- @priority high +-- @topic review + +-- TODO Add `anissue request-review` +-- +-- The command `request-review` should create an empty commit, stating that a review is requested. It should mention the eventual "base branch" for inclusion of the feature. +-- +-- @assigned aforemny +-- @priority high +-- @topic review + -- TODO Compute history from the top -- -- Currently we are computing the history from the bottom (ie. earliest commit @@ -319,14 +355,17 @@ module Main where import Comment qualified as G import Control.Applicative ((<|>)) +import Control.Exception (catch) import Data.Function ((&)) import Data.List (find, intersperse) +import Data.List.NonEmpty qualified as NE import Data.Map qualified as M import Data.Maybe (fromMaybe) import Data.Text qualified as T import Data.Text.IO qualified as T import Data.Text.Lazy qualified as LT import Data.Text.Lazy.IO qualified as LT +import Exception qualified as E import Git qualified import History qualified as H import Issue (Issue (..)) @@ -338,9 +377,11 @@ import Issue.Render () import Issue.Sort qualified as I import Options.Applicative ((<**>)) import Options.Applicative qualified as O +import Patch qualified as A import Process (proc, sh_, textInput) import Render ((<<<)) import Render qualified as P +import Review qualified as R import Settings (Settings (..), readSettings) import System.Console.Terminal.Size qualified as Terminal import System.Exit (ExitCode (ExitFailure), exitWith) @@ -424,6 +465,11 @@ data Command | Open { id :: String } + | Review + { baseBranch :: T.Text, + featureBranch :: T.Text, + perCommit :: Bool + } | Search { pattern :: R.RE, closed :: Bool, @@ -444,6 +490,8 @@ cmd = O.progDesc "Show a log of all issues", O.command "open" . O.info openCmd $ O.progDesc "Open file containing an issue", + O.command "review" . O.info reviewCmd $ + O.progDesc "Review changes", O.command "search" . O.info searchCmd $ O.progDesc "List issues matching a pattern", O.command "show" . O.info showCmd $ @@ -480,6 +528,33 @@ openCmd = Open <$> idArg +reviewCmd :: O.Parser Command +reviewCmd = + Review + <$> baseBranchArg + <*> featureBranchArg + <*> perCommitArg + +baseBranchArg :: O.Parser T.Text +baseBranchArg = + O.strOption $ + O.long "base" + <> O.short 'b' + <> O.metavar "BRANCH" + <> O.help "Base branch from which to review changes. Defaults to `main`." + <> O.value "main" + +featureBranchArg :: O.Parser T.Text +featureBranchArg = + O.strArgument (O.metavar "BRANCH_NAME" <> O.value "HEAD") + +perCommitArg :: O.Parser Bool +perCommitArg = + O.switch + ( O.long "per-commit" + <> O.help "Review commits individually. (Default: review combined patches)" + ) + showCmd :: O.Parser Command showCmd = Show @@ -548,6 +623,13 @@ main :: IO () main = do settings <- readSettings O.execParser (O.info (options <**> O.helper) O.idm) >>= \case + Options {command = Review {baseBranch, featureBranch, perCommit}} -> do + sh_ "test -z $(git status --porcelain --untracked-files=no)" + `catch` \(_ :: E.ProcessException) -> + error "working directory not clean, aborting.." + plan <- R.formulatePlan perCommit baseBranch featureBranch + R.commitReview plan . A.Patch . concat + =<< mapM R.reviewStep (NE.toList plan.steps) Options {colorize, noPager, width, command = List {sort, filters, files, group = Just group, closed}} -> do ungroupedIssues <- I.applySorts sort diff --git a/app/Patch.hs b/app/Patch.hs index 9e6ed88..f170817 100644 --- a/app/Patch.hs +++ b/app/Patch.hs @@ -1,7 +1,7 @@ {-# LANGUAGE DerivingStrategies #-} module Patch - ( Patch, + ( Patch (..), parse, ) where @@ -11,6 +11,7 @@ import Data.Binary (Binary (..)) import Data.Text qualified as T import Exception qualified as E import GHC.Generics (Generic) +import Prettyprinter (pretty) import Render ((<<<)) import Render qualified as P import Text.Diff.Extra () @@ -31,14 +32,28 @@ instance P.Render Patch where instance P.Render (P.Detailed Patch) where render (P.Detailed (Patch {..})) = - P.vsep $ map prettyFileDelta fileDeltas + P.vsep (map prettyFileDelta fileDeltas) <<< ("\n" :: T.Text) where - prettyFileDelta (D.FileDelta {..}) = prettyContent fileDeltaContent + prettyFileDelta (D.FileDelta {..}) = + ("diff --git " <> fileDeltaSourceFile <> " " <> fileDeltaDestFile <> "\n") + <<< (prettySourceFile fileDeltaSourceFile <<< ("\n" :: T.Text)) + <<< (prettyDestFile fileDeltaDestFile <<< ("\n" :: T.Text)) + <<< prettyContent fileDeltaContent + prettySourceFile file = P.styled [P.bold] $ ("---" :: T.Text) <<< file + prettyDestFile file = P.styled [P.bold] $ ("+++" :: T.Text) <<< file prettyContent D.Binary = P.emptyDoc prettyContent (D.Hunks hunks) = P.vsep (map prettyHunk hunks) - prettyHunk (D.Hunk {..}) = P.vsep $ map prettyLine hunkLines + prettyHunk (D.Hunk {..}) = + P.styled [P.color P.Blue] $ + (prettySourceRange hunkSourceRange hunkDestRange <<< ("\n" :: T.Text)) + <<< P.vsep (map prettyLine hunkLines) + prettySourceRange srcRange dstRange = + ("" :: T.Text) <<< ("@@ -" <> prettyRange srcRange <> " +" <> prettyRange dstRange <> " @@") + prettyRange (D.Range line lineNo) = + T.pack (show line) <> "," <> T.pack (show lineNo) prettyLine (D.Line {..}) = case lineAnnotation of - D.Added -> P.styled [P.color P.Green] $ P.plus @P.AnsiStyle <<< lineContent - D.Removed -> P.styled [P.color P.Red] $ P.minus @P.AnsiStyle <<< lineContent - D.Context -> P.styled [P.color P.White] $ P.space @P.AnsiStyle <<< lineContent + D.Added -> P.styled [P.color P.Green] $ P.plus @P.AnsiStyle <> pretty lineContent + D.Removed -> P.styled [P.color P.Red] $ P.minus @P.AnsiStyle <> pretty lineContent + D.Context -> P.styled [P.color P.White] $ P.space @P.AnsiStyle <> pretty lineContent + D.Comment -> P.styled [P.color P.White] $ P.hash @P.AnsiStyle <> pretty lineContent diff --git a/app/Render.hs b/app/Render.hs index 907ef15..56d78aa 100644 --- a/app/Render.hs +++ b/app/Render.hs @@ -24,8 +24,9 @@ module Render renderAsMarkdown, -- * Additional symbols - plus, + hash, minus, + plus, ) where @@ -81,15 +82,15 @@ instance Render a => Render (IO a) where (Just a, Nothing) -> a (Nothing, Just b) -> b (Just a, Just b) -> - if endsWithNL a || startsWithNL b + if endsWithWS a || startsWithWS b then a <> b else a <> space <> b where nonEmpty x' = let x = render x' in if not (null (show x)) then Just (render x) else Nothing - startsWithNL = ("\n" `isPrefixOf`) . show . render - endsWithNL = ("\n" `isSuffixOf`) . show . render + startsWithWS = ((||) <$> ("\n" `isPrefixOf`) <*> (" " `isPrefixOf`)) . show . render + endsWithWS = ((||) <$> ("\n" `isSuffixOf`) <*> (" " `isSuffixOf`)) . show . render (===) :: (Render a, Render b) => a -> b -> Doc AnsiStyle (===) a' b' = @@ -244,8 +245,7 @@ instance Render D.Node where pretty ("\")" :: T.Text) ] -plus :: Doc ann -plus = pretty ("+" :: T.Text) - -minus :: Doc ann +hash, minus, plus :: Doc ann +hash = pretty ("#" :: T.Text) minus = pretty ("-" :: T.Text) +plus = pretty ("+" :: T.Text) diff --git a/app/Review.hs b/app/Review.hs new file mode 100644 index 0000000..721d8e3 --- /dev/null +++ b/app/Review.hs @@ -0,0 +1,219 @@ +module Review + ( Plan (..), + PlanStep (..), + formulatePlan, + reviewStep, + commitReview, + ) +where + +import Comment.Language qualified as L +import Control.Monad (ap, forM, forM_, when) +import Data.Binary qualified as B +import Data.ByteString.Lazy qualified as LB +import Data.Function ((&)) +import Data.List.NonEmpty qualified as NE +import Data.Text qualified as T +import Data.Text.Encoding qualified as T +import Data.Text.IO qualified as T +import GHC.Generics (Generic) +import Git qualified +import Patch qualified as A +import Process (proc, sh, sh_) +import Render (renderAsText) +import System.Directory (createDirectoryIfMissing) +import System.FilePath (takeDirectory, (</>)) +import System.IO.Temp (withSystemTempDirectory) +import System.Process.Typed qualified as P +import Text.Diff.Extra () +import Text.Diff.Parse.Types qualified as D + +data Plan = Plan + { baseBranch :: BranchName, + featureBranch :: BranchName, + commit :: Git.CommitHash, + perCommit :: Bool, + steps :: NE.NonEmpty PlanStep + } + deriving (Show, Generic, B.Binary) + +type BranchName = T.Text + +data PlanStep = PlanStep + { commit :: Git.CommitHash, + earlierCommit :: Git.CommitHash, + changes :: D.FileDeltas + } + deriving (Show, Generic, B.Binary) + +formulatePlan :: Bool -> T.Text -> T.Text -> IO Plan +formulatePlan perCommit baseBranch featureBranch = do + baseCommit <- Git.resolveRef baseBranch + featureCommit <- Git.resolveRef featureBranch + + commits <- + if perCommit + then do + commits <- + reverse <$> Git.getCommitsBetween baseCommit featureCommit + pure $ zipWith (,) commits (baseCommit : commits) + else pure [(featureCommit, baseCommit)] + + fileDeltas <- + fmap concat . forM commits $ + \(commit, earlierCommit) -> + map ((commit, earlierCommit),) . (: []) . (.fileDeltas) + <$> Git.diffOf earlierCommit commit + + pure + Plan + { steps = + NE.fromList + ( map + ( \((commit, earlierCommit), changes) -> + PlanStep {..} + ) + fileDeltas + ), + commit = featureCommit, + .. + } + +reviewStep :: PlanStep -> IO D.FileDeltas +reviewStep step = do + commitMessages <- + T.decodeUtf8 . LB.toStrict + <$> sh + ( proc + "git log %..%" + (Git.toTextUnsafe step.earlierCommit) + (Git.toTextUnsafe step.commit) + ) + separateReview step.earlierCommit step.changes + =<< reviewPatch commitMessages step.changes + +reviewPatch :: T.Text -> D.FileDeltas -> IO D.FileDeltas +reviewPatch commitMessages fileDeltas = + withSystemTempDirectory "anissue" $ \tmp -> do + let patchFile = tmp </> "a.patch" + patchFile' = tmp </> "b.patch" + patchContents = renderAsText (A.Patch fileDeltas) + T.writeFile patchFile patchContents + T.writeFile patchFile' (addCommitMessages <> patchContents) + sh_ (proc "${EDITOR-vi} %" patchFile') + T.writeFile patchFile' + . (renderAsText . A.Patch) + . addComments + . ((.fileDeltas) . A.parse) + . stripCommitMessages + =<< T.readFile patchFile' + ((.fileDeltas) . A.parse . T.decodeUtf8 . LB.toStrict) + <$> sh (proc "rediff % %" patchFile patchFile') + where + addCommitMessages = + T.unlines . map ("# " <>) . T.lines $ commitMessages + stripCommitMessages = + T.unlines . dropWhile ("# " `T.isPrefixOf`) . T.lines + +addComments :: D.FileDeltas -> D.FileDeltas +addComments = + map + ( \fileDelta@(D.FileDelta {D.fileDeltaSourceFile}) -> + ( mapContent . mapHunks . mapLines $ + \line@(D.Line {..}) -> + if lineAnnotation == D.Comment + then + let language = L.fromPath (T.unpack fileDeltaSourceFile) + in D.Line D.Added (L.lineStart language <> " REVIEW" <> lineContent) + else line + ) + fileDelta + ) + where + mapContent f x = x {D.fileDeltaContent = f x.fileDeltaContent} + mapHunks _ D.Binary = D.Binary + mapHunks f (D.Hunks hs) = D.Hunks (map f hs) + mapLines f x = x {D.hunkLines = map f x.hunkLines} + +separateReview :: + Git.CommitHash -> + D.FileDeltas -> + D.FileDeltas -> + IO D.FileDeltas +separateReview commit fileDeltas fileDeltas' = + withTempSourceFiles commit fileDeltas $ \tmp -> do + T.writeFile (tmp </> patchFile) (renderAsText (A.Patch fileDeltas)) + T.writeFile (tmp </> patchFile') (renderAsText (A.Patch fileDeltas')) + sh_ + ( proc "patch --quiet -p0 <../%" patchFile + & P.setWorkingDir (tmp </> "a") + ) + sh_ + ( proc "patch --quiet -p0 <../%" patchFile' + & P.setWorkingDir (tmp </> "b") + ) + ( ap (flip if' [] . LB.null) $ + (.fileDeltas) . A.parse . T.decodeUtf8 . LB.toStrict + ) + <$> sh + ( proc "git diff --no-index -- a b || :" + & P.setWorkingDir tmp + ) + where + patchFile = "a.patch" + patchFile' = "b.patch" + +withTempSourceFiles :: Git.CommitHash -> D.FileDeltas -> (FilePath -> IO a) -> IO a +withTempSourceFiles commit fileDeltas action = do + withSystemTempDirectory "anissue" $ \tmp -> do + createDirectoryIfMissing False (tmp </> "a") + createDirectoryIfMissing False (tmp </> "b") + forM_ sourceFiles $ \sourceFile -> do + let sourceDir = takeDirectory sourceFile + fileContents <- + if sourceFile /= "/dev/null" + then case commit of + Git.Commit hash -> sh (proc "git show %:%" hash sourceFile) + Git.WorkingTree -> sh (proc "cat" sourceFile) + else pure "" + createDirectoryIfMissing True (tmp </> "a" </> sourceDir) + LB.writeFile (tmp </> "a" </> sourceFile) fileContents + createDirectoryIfMissing True (tmp </> "b" </> sourceDir) + LB.writeFile (tmp </> "b" </> sourceFile) fileContents + action tmp + where + sourceFiles = map (T.unpack . (.fileDeltaSourceFile)) fileDeltas + +if' :: Bool -> a -> a -> a +if' True a _ = a +if' False _ b = b + +commitReview :: Plan -> A.Patch -> IO () +commitReview plan patch = do + withSystemTempDirectory "anissue" $ \tmp -> do + when (not (null patch.fileDeltas)) do + T.writeFile (tmp </> "review.patch") (renderAsText patch) + sh_ (proc "patch -p1 <%/review.patch" tmp) + T.writeFile (tmp </> "commit_editmsg") (commit_editmsg plan) + sh_ (proc "git add %" (map (T.drop (T.length "b/") . (.fileDeltaDestFile)) patch.fileDeltas)) + sh_ (proc "git commit --allow-empty --template %/commit_editmsg" tmp) + +commit_editmsg :: Plan -> T.Text +commit_editmsg plan = do + T.unlines + [ "", + "# Please enter the commit message for your review. Lines starting", + "# with '#' will be ignored, and an empty message aborts the commit.", + "#", + "# To approve the changes, format your commit message like this:", + "#", + "# review: approve " <> plan.featureBranch, + "#", + "# Reviewed branch " <> plan.featureBranch <> " at commit " <> Git.toTextUnsafe plan.commit <> ".", + "#", + "# To requst changes, format your commit message like this:", + "#", + "# review: request-changes " <> plan.featureBranch, + "#", + "# Reviewed branch " <> plan.featureBranch <> " at commit " <> Git.toTextUnsafe plan.commit <> "." + ] |