;tools: Shake changelogs: simplify, don't duplicate heading

This commit is contained in:
Simon Michael 2024-12-08 10:41:21 -10:00
parent fc3b21b81b
commit 0cb009a503

151
Shake.hs
View File

@ -659,18 +659,33 @@ main = do
-- CHANGELOGS -- CHANGELOGS
-- update all changelogs with latest commits
phony "changelogs" $ do
need changelogs
when commit $ commitIfChanged ";doc: update changelogs" changelogs
-- [PKG/]CHANGES.md [-n|--dry-run]
-- Add any new commit messages to the specified changelog, idempotently.
-- Or with -n/--dry-run, just print the new content to stdout.
-- Assumptions/requirements:
-- 1. All releases nowadays are full releases (including all four packages).
-- 2. The changelog's topmost markdown heading text is a release heading like "1.18.1 2020-06-21", or a more recent commit id like "4fffe6e7".
-- 3. When a release heading is added to a changelog, a corresponding release tag is also created.
phonys (\out -> if out `notElem` changelogs
then Nothing
else Just $ do
let let
-- git log showing short commit hashes. -- shell command to run git log showing short commit hashes.
-- In 2024 git is showing 9 digits, 1 more than jj - show 8 for easier interop -- In 2024 git is showing 9 digits, 1 more than jj - show 8 for easier interop
gitlog = "git log --abbrev=8" gitlog = "git log --abbrev=8"
-- git log formats suitable for changelogs/release notes -- a git log format suitable for changelogs/release notes
-- %s=subject, %an=author name, %n=newline if needed, %w=width/indent1/indent2, %b=body, %h=hash -- %s=subject, %an=author name, %n=newline if needed, %w=width/indent1/indent2, %b=body, %h=hash
changelogGitFormat = "--pretty=format:'- %s (%an)%n%w(0,2,2)%b\n'" changelogGitFormat = "--pretty=format:'- %s (%an)%n%w(0,2,2)%b\n'"
-- changelogVerboseGitFormat = "--pretty=format:'- %s (%an)%n%w(0,2,2)%b%h' --stat" -- changelogVerboseGitFormat = "--pretty=format:'- %s (%an)%n%w(0,2,2)%b%h' --stat"
-- Format a git log message, with one of the formats above, as a changelog item -- shell command to format a git log message with the above format, as a changelog item
changelogCleanupCmd = unwords [ commitMessageToChangelogItemCmd = unwords [
sed sed
,"-e 's/^( )*\\* /\1- /'" -- ensure bullet lists in descriptions use hyphens not stars ,"-e 's/^( )*\\* /\1- /'" -- ensure bullet lists in descriptions use hyphens not stars
,"-e 's/ \\(Simon Michael\\)//'" -- strip maintainer's author name ,"-e 's/ \\(Simon Michael\\)//'" -- strip maintainer's author name
@ -681,9 +696,9 @@ main = do
,"-e '/./,/^$/!d'" -- replace consecutive newlines with one ,"-e '/./,/^$/!d'" -- replace consecutive newlines with one
] ]
-- Things to exclude when doing git log for project-wide changelog. -- Directories to exclude when doing git log for the project changelog.
-- git exclude pathspecs, https://git-scm.com/docs/gitglossary.html#gitglossary-aiddefpathspecapathspec -- https://git-scm.com/docs/gitglossary.html#gitglossary-aiddefpathspecapathspec
projectChangelogExcludeDirs = unwords [ projectChangelogExcludes = unwords [
":!hledger-lib" ":!hledger-lib"
,":!hledger" ,":!hledger"
,":!hledger-ui" ,":!hledger-ui"
@ -691,102 +706,46 @@ main = do
,":!tests" ,":!tests"
] ]
-- update all changelogs with latest commits mpkg = if dir=="." then Nothing else Just dir where dir = takeDirectory out
phony "changelogs" $ do
need changelogs
when commit $ commitIfChanged ";doc: update changelogs" changelogs
-- [PKG/]CHANGES.md -- Parse the changelog.
-- Add any new non-boring commits to the specified changelog, in
-- an idempotent way, minimising manual toil, as follows. We look at:
--
-- - the changelog's topmost markdown heading, which can be a
-- dev heading (first word is a git revision like 4fffe6e7) or
-- a release heading (first word is a release version & tag
-- like 1.18.1, second word is a date like 2020-06-21) or a
-- package release heading (hledger-ui-1.18.1).
--
-- - the package version, in the adjacent .version file, which
-- can be a dev version like 1.18.99 (first two digits of last
-- part are 97, 98 or 99) or a release version like 1.18.1
-- (any other cabal-style version).
--
-- The old changelog heading is removed if it was a dev heading;
-- new commits in PKG not prefixed with semicolon are added;
-- and a suitable new heading is added: a release heading if
-- the package version looks like a release version, otherwise
-- a dev heading with the current HEAD revision.
--
-- With -n/--dry-run, print new content to stdout instead of
-- updating the changelog.
--
phonys (\out -> if out `notElem` changelogs
then Nothing
else Just $ do
tags <- lines . fromStdout <$> (cmd Shell "git tag" :: Action (Stdout String))
oldlines <- liftIO $ lines <$> readFileStrictly out oldlines <- liftIO $ lines <$> readFileStrictly out
let let
dir = takeDirectory out (preamble, oldheading:rest) = span isnotheading oldlines where isnotheading = not . ("#" `isPrefixOf`)
mpkg | dir=="." = Nothing oldversion = headDef err $ drop 1 $ words oldheading
| otherwise = Just dir
(preamble, oldheading:rest) = span isnotheading oldlines
where isnotheading = not . ("#" `isPrefixOf`)
-- changelog version: a hash or the last release version of this package (or the project)
changelogversion = headDef err $ drop 1 $ words oldheading
where err = error $ "could not parse changelog heading: "++oldheading where err = error $ "could not parse changelog heading: "++oldheading
-- prepend the package name if we are in a package (not the top-level project directory)
maybePrependPackage s = maybe s (++("-"++s)) mpkg
toTag = maybePrependPackage
isOldRelease rev = isReleaseVersion rev && toTag rev `elem` tags
isNewRelease rev = isReleaseVersion rev && toTag rev `notElem` tags
-- git revision corresponding to the changelog version:
-- a hash (a3f19c15), package release tag (hledger-ui-1.20), or project release tag (1.20)
lastrev
| isOldRelease changelogversion = toTag changelogversion -- package release tag
| isNewRelease changelogversion =
trace (out ++ "'s version \""++changelogversion++"\" is not yet tagged, can't list changes")
"HEAD"
| otherwise = changelogversion
-- interesting commit messages between lastrev and HEAD, cleaned up -- Find the latest commit that has been scanned for this changelog, as a commit id or tag name.
let lastscannedrev
interestingpaths = fromMaybe projectChangelogExcludeDirs mpkg | isCommitHash oldversion = oldversion
-- interestingmessages = "--invert-grep --grep '^;'" -- ignore commits beginning with ; | otherwise = maybe oldversion (++("-"++oldversion)) mpkg
-- TODO: update for new commit conventions. ; now means skip CI,
-- feat:/imp:/fix: means release notes, pkg:/lib: means changelogs, etc.
interestingmessages = ""
newitems <- fromStdout <$>
(cmd Shell gitlog changelogGitFormat (lastrev++"..") interestingmessages "--" interestingpaths
"|" changelogCleanupCmd :: Action (Stdout String))
-- git revision of current HEAD -- Find the latest commit (HEAD).
headrev <- unwords . words . fromStdout <$> latestrev <- unwords . words . fromStdout <$> (cmd Shell gitlog "-1 --pretty=%h" :: Action (Stdout String))
(cmd Shell gitlog "-1 --pretty=%h -- " interestingpaths :: Action (Stdout String))
-- package version: the version number currently configured for this package (or the project) -- If it's newer,
packageversion <- when (lastscannedrev /= latestrev) $ do
let versionfile = dir </> ".version"
err = error $ "could not parse a version in "++versionfile -- Find the new commit messages relevant to this changelog, and clean them.
in liftIO $ headDef err . words <$> readFileStrictly versionfile let scanpath = fromMaybe projectChangelogExcludes mpkg
date <- liftIO getCurrentDay newitems <- fromStdout <$> (cmd Shell
let "set -o pipefail;" -- so git log failure will cause this action to fail
-- the new changelog heading will be a final (dated, versioned) heading if gitlog changelogGitFormat (lastscannedrev++"..") "--" scanpath
-- the configured package version is a new release version (non-dev & non-tagged) "|" commitMessageToChangelogItemCmd
(newrev, newheading) :: Action (Stdout String))
| isNewRelease packageversion = (toTag packageversion, unwords [packageversion, show date])
| otherwise = (headrev, headrev) -- Add the new heading and change items to the changelog, or print them.
newcontent = "# "++newheading++"\n" ++ newitems let newcontent = "# " ++ latestrev ++ "\n" ++ newitems
newchangelog = liftIO $ if dryrun
then putStr $ out ++ ":\n" ++ newcontent
else do
writeFile out $ concat [
unlines preamble unlines preamble
++ newcontent ,newcontent
++ (if isCommitHash changelogversion then "" else oldheading) ,if isCommitHash oldversion then "" else oldheading
++ unlines rest ,unlines rest
]
liftIO $ if putStrLn (out ++ ": updated to " ++ latestrev)
| lastrev == newrev -> pure () -- putStrLn $ out ++ ": up to date"
| dryrun -> putStr $ out ++ ":\n" ++ newcontent
| otherwise -> do
writeFile out newchangelog
putStrLn $ out ++ ": updated to " ++ newrev
) )