-- TODO Compute history from the top -- -- Currently we are computing the history from the bottom (ie. earliest commit -- first). When computing history from the top, it might allow us to interrupt -- the process and present slightly inaccurate information earlier. -- TODO Add support for cache progress -- -- When generating caches, `anissue` can sometimes run for a long time without -- producing any output. I would like `anissue` to output a live progress -- report when run from an interactive shell. -- -- That progress output should include: -- -- - the total number of commits having to be analyzed -- - the number of analyzed (including cached) commits -- - the estimated time of completion in format %M:%s -- -- The progress output should not exceed a single terminal line. -- TODO Add `anissue format` -- -- `anissue format` should format all open issues according to the formatting rules. -- -- The formatting rules are given implicitly through the `Render (Detailed Issue)` instance. -- -- The formatting width should be configurable through `Settings`, but should be unlimited by default. -- -- @topic formatting -- TODO Add `anissue lint` -- -- `anissue lint` should provide helpful tips in order to increase the qualify of issues. -- -- Reported issues should be able to be automatically `--fix`ed, if possible. -- -- Firstly, I would like `anissue lint` to report on the following: -- -- - Referencing issues through misspelled ids\* -- - Defining tags outside of tag paragraphs -- - Having tags not sorted alphabetically -- -- \* We might have to have for this a notion what tags are referencing issue ids. This should eventually be configurable through `Settings`, but we can hardcode `@references`, `@supersedes` and `@related` for now. -- -- @supersedes add-anissue-review -- @topic linting -- -- COMMENT While I generally prefer "review" for this, I think we should call it "lint", as "review" is overloaded with reviewing a pull request, ie. branch for inclusion in the main branch. -- TODO Tag improvements (OR-filtering) -- -- Currently it is not possible to filter for an issue satisfying one filter or another. We could add the following syntax allowing it: -- -- `--filter '(@assigned aforemny) OR (@due 2023-10-4)'` -- -- @topic tags -- @backlog -- TODO Add support for ammendments -- -- The user can ammend more information to an issue which is located at -- a different place by referencing the issue's id. Example: -- -- ```bash -- #!/usr/bin/env bash -- -- set -efu -- -- ls -al -- # TODO Original issue -- # -- # @id original-issue -- -- ls -- # @original-issue more information on the issue -- -- edited -- ``` -- -- @priority high -- @backlog -- -- COMMENT Ammendments should generate IssueEvents which can be shown in the issue's log. -- TODO Add options to show command to display source code around the issue -- -- The `show` command should have the command line arguments -- `--after/-A`, `--before/-B` and `--context/-C` which take an integer -- n and change the output to contain n lines after, before or around -- the issue. -- -- @topic show -- @topic options -- @backlog -- -- COMMENT Do we expect the code BEFORE an issue to be ever relevant? -- -- COMMENT Hm, this would make it possible for users to use anissue in -- that way at least. I guess most people would assume a comment to go -- before (or for a short comment at the end) the relecant line(s), so -- maybe an `--after/-A` option would suffice. Not sure what's best -- here to be honest. =) -- -- COMMENT I don't mind the `--before|-B` option. I was just wondering out -- loud, also related to whether we want to extract some closed part of the -- AST, seeing that we already have tree sitter, instead of showing context. -- -- But I don't have a good proposal for that myself. It is not my intention to -- block on this issue. -- TODO Expose all command line options as environment variables -- -- It should be possible to provide a default for any command line -- argument via an environment variable. The name of the variable -- should be all upper case and prefixed with `ANISSUE_`, e.g. the -- default value for the command line option `--internal-tags` should be -- settable via `ANISSUE_INTERNAL_TAGS`. -- -- @topic options -- @topic settings -- @backlog -- -- COMMENT Can/ should this be made a feature of optparse-applicative, which we -- could fork? Like, can this work transparently if we fork -- optparse-applicative? -- -- COMMENT Hm, this would only apply to options and flags, but not sub -- commands, I guess. I would say, we could first start by just -- manually adding these env variables and see if we run into -- options/flags, where a env variable does not make sense. But then, -- this would be an optional feature in the library, as well, i guess? -- :D -- -- COMMENT I could imagine it not too difficult adding a `fromEnv` modifier -- into `O.Parser`. In any case, this does not have to block on that, as, -- should we decide to do that, we can always migrate to it later. -- TODO Add a subcommand which appends the generated ids to the issue in the sourcecode -- -- Given the following issue -- -- ``` -- # TODO Some title -- # -- # Some description -- ``` -- -- After running `anissue lint`, the issue will be changed to -- -- ``` -- # TODO Some title -- # -- # Some description -- # -- # @id some-title -- ``` -- -- @backlog -- @depends-on add-anissue-lint -- @topic ids -- -- COMMENT Can you elaborate on why this would be useful? I am expecting us to -- do some renaming detection regarding provenance. After that, will it be -- useful still? -- -- COMMENT I think, I mostly find this useful for ammendments, as I then -- neither would have to think about what the generated id was, nor -- would I have to come up with an id. And if we would decide for -- randomized (but easily typeable) generated ids, this would be useful, -- I believe. -- -- COMMENT Assuming the we append **the title** as the ID, I don't see much -- point of this. Maybe better editor integration that auto-completes `@<>` -- would be more appropriate? -- -- I am sympathetic to random identifiers, cf. -- should-automatically-generated-ids-be-random, as long as they are an -- additional mechanisam (so that we can ditch them, should they not work out). -- QUESTION Should automatically generated ids be random? -- -- Right now default id of an issue is the slugification of it's title. -- Once we have a command to automatically append this id to the issue -- within the source code, users might feel tempted to change the id -- after they have adjusted the title. This then would break the -- provenance of the issue. If the generated id was something like -- `tooth-cherry-switch`, it would be just a random string and noone -- would feel the need to change it. -- -- I imagine a workflow where I add issues in the source code. And -- before commiting, I run `anissue lint` to generate ids for all new -- issues. -- -- @topic ids -- @backlog -- -- COMMENT I am not sure we want to automatically append `@id`s to the issue -- description. We probably want to append `@pastId` to an issue if we notice a -- change in the title. I feel users are not as likely change that. -- -- That being said, I am up for experimenting with random IDs *alongside* the -- current design. Maybe we could add a `@uuid` tag automatically that is -- generated from the issues first `@id`, and append that? -- -- COMMENT I think I like the idea of adding an id, once we see a change in the -- title. I'm not sure though, when in the workflow this would happen. -- I think the id generation topic needs some syncronous discussion. :D -- -- COMMENT ACK -- TODO Dependencies between issues -- -- The user should be able to specify that an issue is blocked by -- a different issue by adding `@blocker id-of-blocking-issue`. -- -- When running `anissue list --show-dependencies`, the output should -- render a list of trees deduced from these blocker-relations. E.g. -- -- ``` -- Issue A -- ├ Issue A1 (blocked by A) -- │ ├ Issue A11 (blocked by A1) -- ├─┴ Issue A3A11 (blocked by A and A1) -- └ Issue A2B1 (blocked by A and B) -- Issue B -- └ Issue A2B1 (blocked by A and B) -- Issue C -- ``` -- -- The items should be ordered with the most blocking issues at the top. -- -- @topic dependencies -- @backlog -- -- COMMENT I suggest first adding a command `anissue tree @blocker` which -- generates the suggested forest of issues. -- TODO Display issue type in list and show views -- -- Depending on the type of issue (TODO, FIXME, ...) there should be -- either a prefix in the list view (e.g. 🏗️, 🐞, ...), or the list -- should be grouped by the type. -- -- @difficulty easy -- @backlog -- -- COMMENT I imagine emojis might be off-putting to some potential users. I -- would like a configuration option to disable emojis and display the marker -- instead. -- -- I am fine with having emojis by default. However, seeing that markers are -- extendable, maybe we should make it an opt-in configuration in the first -- place? -- -- COMMENT Yes, thinking about this again, putting the written marker -- instead, might be better. So sth like. -- -- ``` -- FIXME Issue A -- FIXME Issue B -- TODO Story A -- QUESTION Question A -- FIXME Issue C -- ``` -- -- We could still additionally color code them. -- -- COMMENT I propose adding the issue marker as an internal @marker tag. There -- can be several such tags if several markers are specified (and we want to -- allow that). The first marker should have priority in case we have to pick -- one. -- TODO Add global option for specifying first considered commit -- -- Every command offers the option `--first-commit `, which -- tells anissue to only consider commits stargin with the provided -- commit. The default is the first commit HEAD eventually points to; -- this will also happen when the provided commit is not in the history -- of HEAD. -- -- @backlog -- @depends-on compute-history-from-the-top -- @topic cache -- @topic options -- TODO Add format option (Text, JSON, ...) -- -- Add an option `--format` to the list and show commands which adjust -- the output format: -- -- text -- : this is the default -- -- json -- : render all information as json so that it can be used, e.g. by -- other tools -- -- @topic rendering -- @topic options -- @backlog -- TODO Add HTTP server -- -- When running `anissue server`, an HTTP server should be started, -- which serves a JSON-API as well as a HTML-frontend for the current -- directory. -- -- The HTML frontend should display the output of `anissue list` where -- each item links to a separate page containing the infos of `anissue -- show`. -- -- The JSON-Api offers endpoints `GET //` which returns `anissue -- list` as `GET ///` which returns `anissue show`. -- All command line arguments are exposed via query parameters. -- -- @backlog 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 (..)) import Issue qualified as I import Issue.Filter qualified as I import Issue.Group qualified as I import Issue.Meta qualified as I 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) import System.FilePath (()) import System.IO.Temp (withSystemTempDirectory) import System.Process.Typed qualified as P import Text.Printf import Text.RE.TDFA.Text qualified as R import Tuple () import Prelude hiding (id) data Options = Options { command :: Command, colorize :: Color, noPager :: Bool, width :: Maybe Int } data Color = Always | Auto | Never deriving (Show, Eq) options :: O.Parser Options options = Options <$> cmd <*> colorOption <*> noPagerFlag <*> widthOption colorOption :: O.Parser Color colorOption = O.option (O.maybeReader parse) ( O.long "color" <> O.short 'c' <> O.help "Wether to colorize output. (Default: auto)" <> O.value Auto ) where parse "auto" = pure Auto parse "always" = pure Always parse "never" = pure Never parse _ = Nothing noPagerFlag :: O.Parser Bool noPagerFlag = O.switch ( O.long "no-pager" <> O.short 'P' <> O.help "Don't pipe long output to $PAGER." ) widthOption :: O.Parser (Maybe Int) widthOption = O.optional ( O.option O.auto ( O.long "width" <> O.short 'w' <> O.metavar "INT" <> O.help "Wheather to insert line breaks after at most that many characters." ) ) data Command = List { files :: [String], filters :: [I.Filter], sort :: [I.Sort], group :: Maybe T.Text, closed :: Bool, detailed :: Bool, edit :: Bool } | Log { patch :: Bool } | Open { id :: String } | Review { baseBranch :: T.Text, featureBranch :: T.Text, perCommit :: Bool } | Search { pattern :: R.RE, closed :: Bool, detailed :: Bool } | Show { id :: String, edit :: Bool } | Tags cmd :: O.Parser Command cmd = O.hsubparser . mconcat $ [ O.command "list" . O.info listCmd $ O.progDesc "List all issues", O.command "log" . O.info logCmd $ 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 $ O.progDesc "Show details of all issues", O.command "tags" . O.info tagsCmd $ O.progDesc "Show all tags" ] listCmd :: O.Parser Command listCmd = List <$> filesArg <*> I.filterArg <*> I.sortArg <*> I.groupArg <*> closedArg <*> detailedArg <*> editFlag logCmd :: O.Parser Command logCmd = Log <$> patchFlag searchCmd :: O.Parser Command searchCmd = Search <$> patternArg <*> closedArg <*> detailedArg openCmd :: O.Parser Command openCmd = Open <$> idArg reviewCmd :: O.Parser Command reviewCmd = Review <$> baseBranchArg <*> featureBranchArg <*> perCommitArg baseBranchArg :: O.Parser T.Text baseBranchArg = O.option O.auto $ 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 <$> idArg <*> editFlag patternArg :: O.Parser R.RE patternArg = O.argument (O.maybeReader R.compileRegex) (O.metavar "PATTERN") tagsCmd :: O.Parser Command tagsCmd = pure Tags filesArg :: O.Parser [String] filesArg = O.many (O.strArgument (O.metavar "FILE" <> O.action "file")) closedArg :: O.Parser Bool closedArg = O.switch ( O.long "closed" <> O.help "Show closed issues." ) detailedArg :: O.Parser Bool detailedArg = O.switch ( O.long "detailed" <> O.help "Show issues detailed (as in show)." ) idArg :: O.Parser String idArg = O.strArgument ( O.metavar "ID" <> O.completer ( O.listIOCompleter $ map T.unpack . M.keys . (.issues) <$> H.getIssues ) ) editFlag :: O.Parser Bool editFlag = O.switch ( O.long "edit" <> O.help "Edit issue in $EDITOR." ) patchFlag :: O.Parser Bool patchFlag = O.switch ( O.short 'p' <> O.long "patch" <> O.help "Show patches." ) die :: String -> IO a die s = do printf "error: %s\n" s exitWith (ExitFailure 1) 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) -- REVIEW Why is withReviewing in the Status module and not the Review -- module? -- -- RESOLVED `Status` has been dropped in this commit Options {colorize, noPager, width, command = List {sort, filters, files, group = Just group, closed}} -> do ungroupedIssues <- I.applySorts sort . I.applyFilters filters . I.applyPath files . I.applyClosed closed . (M.elems . (.issues)) <$> H.getIssues let groupedIssues = I.groupIssuesByTag group ungroupedIssues putDoc colorize noPager width (group, groupedIssues) Options {colorize, noPager, width, command = List {sort, filters, files, group = Nothing, closed, detailed, edit}} -> do issues <- I.applySorts sort . I.applyFilters filters . I.applyPath files . I.applyClosed closed . (M.elems . (.issues)) <$> H.getIssues if edit then editIssues issues else putDoc colorize noPager width . (P.vsep . intersperse "") $ map (if detailed then (P.render . P.Detailed) else (P.render . P.Summarized)) issues Options {colorize, noPager, width, command = Log {patch}} -> do es <- reverse . (.issueEvents) <$> H.getIssueEvents putDoc colorize noPager width $ if patch then P.vsep . intersperse P.emptyDoc $ map (P.render . P.Detailed) es else P.vsep $ map (P.render . P.Summarized) es Options {colorize, noPager, width, command = Show {id, edit}} -> do issues <- (.issues) <$> H.getIssues issue <- case M.lookup (T.pack id) issues of Nothing -> die (printf "no issue with id `%s'\n" id) Just issue -> pure issue if edit then editIssues [issue] else putDoc colorize noPager width $ showIssue (M.elems issues) issue Options {colorize, noPager, width, command = Tags} -> do issues <- (.issues) <$> H.getIssues putDoc colorize noPager width $ concatMap (.tags) issues Options {command = Open {id}} -> do issues <- (.issues) <$> H.getIssues issue <- case M.lookup (T.pack id) issues of Nothing -> die (printf "no issue with id `%s'\n" id) Just issue -> pure issue sh_ (proc "${EDITOR-vi} +% -- %" issue.startPoint.row issue.file) Options {colorize, noPager, width, command = Search {pattern, closed, detailed}} -> do issues <- I.applyClosed closed . (M.elems . (.issues)) <$> H.getIssues putDoc colorize noPager width . (P.vsep . intersperse "") . map ( if detailed then P.render . P.Detailed else P.render . P.Summarized ) . filter (\issue -> R.matched (P.renderAsText issue R.?=~ pattern)) $ issues showIssue :: [Issue] -> Issue -> P.Doc P.AnsiStyle showIssue issues issue = do let meta = I.getMeta issues issue issue <<< ("\n" :: T.Text) <<< meta editIssues :: [Issue] -> IO () editIssues issues = withSystemTempDirectory "anissue-edit" (go issues) where go :: [Issue] -> FilePath -> IO () go issues cwd = do mapM_ (\issue -> T.writeFile (fp issue) =<< issue.rawText) issues sh_ (proc "${EDITOR-vi} -- %" (map fp issues)) replaceTexts issues where fp issue = cwd (T.unpack issue.id <> ".md") replaceTexts [] = pure () replaceTexts [issue] = do I.replaceText issue =<< T.readFile (fp issue) replaceTexts (issue : issues) = do I.replaceText issue =<< T.readFile (fp issue) issues' <- H.getIssuesOfFile Git.WorkingTree issue.file replaceTexts [fromMaybe issue (find ((==) issue.id . (.id)) issues') | issue <- issues] putDoc :: P.Render a => Color -> Bool -> Maybe Int -> a -> IO () putDoc colorize noPager width renderable = do isTty <- (== 1) <$> c_isatty 1 term <- Terminal.size let s = P.renderLazy $ P.layoutSmart P.defaultLayoutOptions { P.layoutPageWidth = maybe P.Unbounded (\n -> P.AvailablePerLine n 1) (width <|> (Terminal.width <$> term)) } $ ( if colorize == Always || (colorize == Auto && isTty) then (\x -> x) else P.unAnnotate ) $ P.render renderable if not noPager && maybe False (length (LT.lines s) >) (Terminal.height <$> term) then sh_ ( "${PAGER-less}" & P.shell & P.setStdin (textInput s) ) else LT.putStr s foreign import ccall "unistd.h isatty" c_isatty :: Int -> IO Int