167 lines
		
	
	
		
			4.9 KiB
		
	
	
	
		
			Haskell
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			167 lines
		
	
	
		
			4.9 KiB
		
	
	
	
		
			Haskell
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/env stack
 | |
| {- stack script --resolver nightly-2021-11-19
 | |
|     --package data-default
 | |
|     --package extra
 | |
|     --package hledger-lib
 | |
|     --package process
 | |
|     --package text
 | |
| -}
 | |
| {- stack ghc
 | |
|     --package text
 | |
| -}
 | |
| -- changelog.hs CHANGELOGFILE
 | |
| --
 | |
| -- Manipulate a hledger changelog. Currently does one thing: prompts
 | |
| -- for a rewrite of the oldest uncategorised pending changelog item
 | |
| -- and updates the file, printing a diff.
 | |
| --
 | |
| -- My workflow:
 | |
| -- - In a terminal window (not emacs shell, it will hang) run this on a CHANGES.md file
 | |
| -- - Edit the changelog item to changelog-readiness (if needed)
 | |
| -- - Save and quit (C-x #) to do the next; it will exit after the last. Or C-x C-x to stop.
 | |
| -- - On the side keep an auto-reverting editor open on the file to watch progress or for fixups.
 | |
| -- It might seem a bit pointless, but it made this old chore much more
 | |
| -- pleasant. I can do just one, or more as I feel it, and my attention
 | |
| -- is not dragged on by pending items.
 | |
| 
 | |
| {-# LANGUAGE DeriveAnyClass #-}
 | |
| {-# LANGUAGE DeriveGeneric #-}
 | |
| -- {-# LANGUAGE NamedFieldPuns #-}
 | |
| {-# LANGUAGE RecordWildCards #-}
 | |
| 
 | |
| import Data.Char
 | |
| import Data.Default
 | |
| import GHC.Generics
 | |
| import Data.List.Extra
 | |
| import qualified Data.Text as T
 | |
| import System.Environment
 | |
| import System.IO.Extra
 | |
| -- import System.IO
 | |
| import System.Process
 | |
| import Text.Printf
 | |
| import Hledger.Utils
 | |
| 
 | |
| -- A top level section in the changelog, corresponding to one release.
 | |
| data ChangelogSection = ChangelogSection {
 | |
|    heading          :: ChangelogHeading
 | |
|   ,unknownitems     :: [ChangelogItem]
 | |
|   ,featureitems     :: [ChangelogItem]
 | |
|   ,improvementitems :: [ChangelogItem]
 | |
|   ,fixitems         :: [ChangelogItem]
 | |
|   }
 | |
|   deriving (Show, Eq, Generic, Default)
 | |
| 
 | |
| -- The one-line heading for a top level section in the changelog,
 | |
| -- with the leading #(s) removed.
 | |
| type ChangelogHeading = String
 | |
| 
 | |
| -- One change description in the changelog, with the list bullet and
 | |
| -- corresponding indentation removed.
 | |
| type ChangelogItem = String
 | |
| 
 | |
| main = do
 | |
|   (f:_) <- getArgs
 | |
|   go f
 | |
| 
 | |
| go f = do
 | |
|   -- read specified changelog
 | |
|   (preamble:first:rest) <- splitOn "\n# " <$> readFile f
 | |
|   let
 | |
|     g = f ++ ".new"
 | |
|     s@ChangelogSection{..} = readSection first
 | |
| 
 | |
|   -- ask for an edit of this item's text
 | |
|   s' <- editOneUnknown s
 | |
| 
 | |
|   if s' == s
 | |
|   then return ()  -- if it's unchanged, quit
 | |
|   else do
 | |
|     -- otherwise write to a temp file
 | |
|     writeFile g $ init $ unlines $
 | |
|       preamble :
 | |
|       showSection s' :
 | |
|       map ("# "++) rest
 | |
|     -- and show the diff
 | |
|     system' $ printf "diff %s %s" f g
 | |
|     -- and replace the old file
 | |
|     system' $ printf "mv %s %s" g f
 | |
|     -- and repeat
 | |
|     go f
 | |
| 
 | |
| editOneUnknown :: ChangelogSection -> IO ChangelogSection
 | |
| editOneUnknown s@ChangelogSection{..}
 | |
|   | null unknownitems = return s
 | |
|   | otherwise = do
 | |
|     let
 | |
|       s' = s{unknownitems=init unknownitems}
 | |
|       u = last unknownitems
 | |
|     new <- textEditEditor u
 | |
|     return $ case new of
 | |
|       'f':'e':'a':'t':':':' ':t -> s'{featureitems     = readItem t : featureitems}
 | |
|       'f':'i':'x':':':' ':t     -> s'{fixitems         = readItem t : fixitems}
 | |
|       'i':'m':'p':':':' ':t     -> s'{improvementitems = readItem t : improvementitems}
 | |
|       t                         -> s'{improvementitems = readItem t : improvementitems}
 | |
| 
 | |
| textEditEditor t = withTempFile $ \f -> do
 | |
|   writeFile f t
 | |
|   ed <- getEnv "EDITOR"
 | |
|   system $ printf "%s %s" ed f
 | |
|   readFile f
 | |
| 
 | |
| -- textEditTty u = do    
 | |
| --     putStrLn "Old:"
 | |
| --     putStrLn u
 | |
| --     putStrLn "New: (prefix with feat:/imp:/fix: to categorise, ctrl-d to finish):\n" -- Just an = keeps it unchanged, empty string removes it."
 | |
| --     getContents
 | |
| 
 | |
| -- Parse a changelog section which may or may not have the Features/Improvements/Fixes subheadings.
 | |
| readSection :: String -> ChangelogSection
 | |
| readSection s =
 | |
|   let
 | |
|     (heading,rest) = break (=='\n') s
 | |
|     parts = splitOn "\nFeatures\n" rest
 | |
|     (unknownitems, featureitems, improvementitems, fixitems) =
 | |
|       case parts of
 | |
|         []       -> ([], [], [], [])
 | |
|         [u]      -> (readItems u, [], [], [])
 | |
|         (u:xs:_) -> (readItems u, readItems f, readItems i, readItems x)
 | |
|           where
 | |
|             (f:ys:_) = splitOn "\nImprovements\n" xs
 | |
|             (i:x:_)  = splitOn "\nFixes\n" ys
 | |
|   in ChangelogSection{..}
 | |
|   where
 | |
|     readItems = map readItem . filter (not.all isSpace) . splitOn "\n- "
 | |
| 
 | |
| showSection ChangelogSection{..} =
 | |
|   unlines $
 | |
|        [("# "++heading), ""]
 | |
|     ++ map showItem unknownitems
 | |
|     ++ ["Features", ""]
 | |
|     ++ map showItem featureitems
 | |
|     ++ ["Improvements", ""]
 | |
|     ++ map showItem improvementitems
 | |
|     ++ ["Fixes", ""]
 | |
|     ++ map showItem fixitems
 | |
| 
 | |
| readItem :: String -> ChangelogItem
 | |
| readItem "" = def
 | |
| readItem s =
 | |
|   let
 | |
|     (first:rest) = lines s
 | |
|     stripto2spaces (' ':' ':t) = t
 | |
|     stripto2spaces (' ':t) = t
 | |
|     stripto2spaces t = t
 | |
|   in unlines $
 | |
|      first :
 | |
|      map stripto2spaces rest
 | |
| 
 | |
| showItem "" = ""
 | |
| showItem i =
 | |
|   let (first:rest) = lines i
 | |
|   in unlines $ ("- "++first) : map ("  "++) rest
 | |
| 
 | |
| system' s = putStrLn s >> system s
 | |
| 
 | |
| re = toRegex' . T.pack
 | |
| 
 |