From 59ddbdb3d54de4009c2b3238661272f7e9fb80bc Mon Sep 17 00:00:00 2001
From: Fabian Kirchner <kirchner@posteo.de>
Date: Tue, 7 Nov 2023 14:21:33 +0100
Subject: add --group-by option

---
 anissue.cabal        |  2 ++
 app/Issue/GroupBy.hs | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++
 app/Main.hs          | 67 ++++++++++++++++++----------------------------------
 3 files changed, 90 insertions(+), 44 deletions(-)
 create mode 100644 app/Issue/GroupBy.hs

diff --git a/anissue.cabal b/anissue.cabal
index 188a418..4b068e6 100644
--- a/anissue.cabal
+++ b/anissue.cabal
@@ -78,6 +78,7 @@ executable anissue
       History.PartialCommitInfo
       Issue
       Issue.Filter
+      Issue.GroupBy
       Issue.Provenance
       Issue.Sort
       Issue.Tag
@@ -99,6 +100,7 @@ executable anissue
                       attoparsec,
                       binary,
                       bytestring,
+                      containers,
                       directory,
                       filepath,
                       optparse-applicative,
diff --git a/app/Issue/GroupBy.hs b/app/Issue/GroupBy.hs
new file mode 100644
index 0000000..0e8941f
--- /dev/null
+++ b/app/Issue/GroupBy.hs
@@ -0,0 +1,65 @@
+module Issue.GroupBy
+  ( groupByArg,
+    groupIssuesBy,
+  )
+where
+
+import Data.Map (Map)
+import Data.Map qualified as M
+import Data.Maybe (mapMaybe)
+import Data.Text qualified as T
+import Issue (Issue (..))
+import Issue.Tag (Tag (..))
+import Issue.Tag qualified as I
+import Options.Applicative qualified as O
+
+
+-- FIXME Parse group by using @topic syntax
+
+-- TODO Display number of issues within one group
+--
+-- ```
+-- Issue D
+-- Issue E
+--
+-- @topic tags  (2 issues)
+-- Issue A
+-- Issue B @topic ids
+--
+-- @topic ids  (2 issues)
+-- Issue B @topic tags
+-- Issue C
+-- ```
+
+-- TODO Add issues marker as internal tags
+--
+-- The internal makers `TODO`, `FIXME`, etc. should be available via the
+-- internal tag @type
+
+-- TODO Add author and editor as internal tags
+
+groupByArg :: O.Parser (Maybe T.Text)
+groupByArg =
+  O.optional
+    ( O.strOption
+        ( O.long "group-by"
+            <> O.metavar "TAG"
+            <> O.help "Group selected issues."
+        )
+    )
+
+groupIssuesBy :: T.Text -> [Issue] -> Map T.Text [Issue]
+groupIssuesBy groupBy issues =
+  foldl
+    ( \collected issue ->
+        foldl
+          (flip $ M.alter (Just . maybe [issue] (issue :)))
+          collected
+          (groupsOfIssue groupBy issue)
+    )
+    M.empty
+    issues
+  where
+    groupsOfIssue group issue =
+      mapMaybe I.tagValue $
+        filter (\(Tag key _) -> key == group) (issue.tags ++ issue.internalTags)
diff --git a/app/Main.hs b/app/Main.hs
index 3e9dc1d..ee9e850 100644
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -249,48 +249,6 @@
 -- allow that). The first marker should have priority in case we have to pick
 -- one.
 
--- TODO Add option to group issue list
---
--- When running `anissue list` with the `--group-by` option, the output
--- will be grouped.  E.g. running `anissue list --group-by topic` would
--- produce
---
--- ```
--- Issue D
--- Issue E
---
--- @topic tags  (2 issues)
--- Issue A
--- Issue B @topic ids
---
--- @topic ids  (2 issues)
--- Issue B @topic tags
--- Issue C
--- ```
---
--- Other possible grouping options could be
---
--- type
--- : the issue type, i.e. `TODO`, `FIXME`, `QUESTION`, etc.
---
--- priority
--- : the priority, groups should be sorted with the highest priority
--- first
---
--- author
--- : the person which created the issue
---
--- editor
--- : the person who updated the issue last
---
--- @topic options
---
--- COMMENT For consistency, I would like the option to use `--group-by @topic`.
--- Arbitrary tag grouping seems sufficient for this, assuming we add the
--- respective *internal tags*.
---
--- COMMENT Sounds good!
-
 -- TODO Support issue comments
 -- Currently, comments do not get picked up in issue descriptions; see the
 -- issue below.
@@ -437,6 +395,7 @@ module Main where
 import Control.Applicative ((<|>))
 import Data.Function ((&))
 import Data.List (find, isPrefixOf)
+import Data.Map qualified as M
 import Data.Maybe (catMaybes)
 import Data.Text qualified as T
 import Data.Text.IO qualified as T
@@ -450,6 +409,7 @@ import Issue (Issue (..))
 import Issue qualified as I
 import Issue.Filter (Filter, applyFilters)
 import Issue.Filter qualified as I
+import Issue.GroupBy qualified as I
 import Issue.Provenance qualified as I
 import Issue.Sort (Sort, applySorts)
 import Issue.Sort qualified as I
@@ -539,7 +499,8 @@ data Command
   = List
       { files :: [String],
         filters :: [Filter],
-        sort :: [Sort]
+        sort :: [Sort],
+        groupBy :: Maybe T.Text
       }
   | Log
   | Show
@@ -565,6 +526,7 @@ listCmd =
     <$> filesArg
     <*> I.filterArg
     <*> I.sortArg
+    <*> I.groupByArg
 
 logCmd :: O.Parser Command
 logCmd =
@@ -603,7 +565,24 @@ main :: IO ()
 main = do
   settings <- readSettings
   O.execParser (O.info (options <**> O.helper) O.idm) >>= \case
-    Options {colorize, noPager, width, command = List {sort, filters, files}} -> do
+    Options {colorize, noPager, width, command = List {sort, filters, files, groupBy = Just groupBy}} -> do
+      let withinPath issue = if null files then True else any (\file -> file `isPrefixOf` issue.file) files
+      ungroupedIssues <- applySorts sort . applyFilters filters . filter withinPath . fst <$> getHistory
+      let groupedIssues = I.groupIssuesBy groupBy ungroupedIssues
+      putDoc colorize noPager width . P.vsep $
+        map
+          ( \(name, issues) ->
+              P.vsep $
+                P.hardline
+                  : P.annotate (P.color P.Red) (P.pretty name)
+                  : map
+                    ( \issue ->
+                        P.indent 4 $ P.annotate P.bold $ P.pretty issue.title
+                    )
+                    issues
+          )
+          (M.toList groupedIssues)
+    Options {colorize, noPager, width, command = List {sort, filters, files, groupBy = Nothing}} -> do
       let withinPath issue = if null files then True else any (\file -> file `isPrefixOf` issue.file) files
       issues <- applySorts sort . applyFilters filters . filter withinPath . fst <$> getHistory
       putDoc colorize noPager width . P.vsep $
-- 
cgit v1.2.3