packages feed

rhbzquery 0.2 → 0.3

raw patch · 9 files changed

+317/−228 lines, 9 files

Files

ChangeLog.md view
@@ -1,5 +1,11 @@ # rhbzquery releases +## 0.3 (2020-12-10)+- add --list-fields option+- field validation: error for unknown fields+- for 'content' field, map '~'/'!~' to matches/notmatches+- refactor operator handling to generate help+ ## 0.2 (2020-12-09) - support most search type operators (regexp, substr, isempty, and negatives) 
README.md view
@@ -12,20 +12,22 @@  `rhbzquery closed rawhide xyz` : closed rawhide bugs for package xyz -`rhbzquery --mine` : your open bugs (reads userid from `.bugzillarc`)+`rhbzquery --mine` : your open bugs (gets userid from `.bugzillarc`)  `rhbzquery rhel8.3 bash` : RHEL 8.3 bash bugs  `rhbzquery "Package Review" reporter_realname="Your Name"` : open package reviews you reported -`rhbzquery --mine all flag~fedora-review+` : all your open and closed approved reviews+`rhbzquery --mine all flag~fedora-review+` : all open and closed approved reviews you reviewed  `rhbzquery component~bugzilla summary~bugzilla` : open bugs with component and summary containing "bugzilla"  `rhbzquery --file f33 xyz` : file a bug against the xyz package in F33  ### Help-`rhbzquery --help` : lists the many fields (note: not all well supported yet, eg timestamps).+`rhbzquery --help` describes arguments++`rhbzquery --list-fields` : lists the many fields (not all well supported yet, eg timestamps).  ## Installation Run `stack install` or `cabal install`.
TODO view
@@ -1,9 +1,7 @@-- copy url to clipboard+- --list-operators?+- --url for server - tui bug browser-- help: --fields-matching-- use NOT Closed ?+- --json output - cc=me, reporter=me - config file: for defining "my team" etc-- --help: only show most important fields-  --help-all for all-- --count bugs+- --count bugs (no such api though)
rhbzquery.cabal view
@@ -1,5 +1,5 @@ name:                rhbzquery-version:             0.2+version:             0.3 synopsis:            Bugzilla query tool description:         A CLI tool for creating bugzilla queries for bugzilla.redhat.com.@@ -25,6 +25,7 @@ executable rhbzquery   main-is:             Main.hs   other-modules:       Bugzilla+                       Common                        Fields                        Help                        ParseArg
+ src/Common.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE CPP #-}++module Common (error') where++#if MIN_VERSION_simple_cmd(0,1,4)+import SimpleCmd (error')+#else+error' :: String -> a+#if MIN_VERSION_base(4,9,0)+error' = errorWithoutStackTrace+#else+error' = error+#endif+#endif
src/Fields.hs view
@@ -1,4 +1,5 @@ module Fields (+  allBzFields,   BzFields(..),   argToFields,   argToSimpleField@@ -8,6 +9,7 @@ import Data.Version import Numeric.Natural +import Common import ParseArg (ArgType(..), Operator(..), ProductVersion(..), showOp)  data BzFields = BzProduct@@ -24,25 +26,14 @@   show (BzParameter f) = f   show (BzMeta c n) = c: show n -mapField :: String -> String-mapField "itm" = "cf_internal_target_milestone"-mapField "itr" = "cf_internal_target_release"-mapField "status" = "bug_status"-mapField "verified" = "cf_verified"-mapField "sst" = "agile_team.name"-mapField "summary" = "short_desc"-mapField "flag" = "flagtypes.name"-mapField "flags" = "flagtypes.name"-mapField p = p- argToSimpleField :: ArgType -> [(BzFields,String)] argToSimpleField (ArgProdVer prodver) =   productVersionQuery prodver argToSimpleField (ArgParameter p Equals v) =   [(BzParameter (mapField p), v)] argToSimpleField (ArgParameter p _ v) =-  error $ "Only '" ++ p ++ "=" ++ v ++ "'allowed here"-argToSimpleField (ArgOther c) =+  error' $ "Only '" ++ p ++ "=" ++ v ++ "'allowed here"+argToSimpleField (ArgComponent c) =   [(BzComponent,c)] -- FIXME or error for "all"? argToSimpleField _ = []@@ -57,16 +48,23 @@           val = if p == "sst"                 then "sst_" ++ v                 else v+          oper = if p == "content"+                 then case op of+                        Equals -> "equals"+                        Substring -> "matches"+                        NotSubstring -> "notmatches"+                        _ -> error' "'content' only supports matches and notmatches"+                 else showOp op       in if '.' `elem` param || op /= Equals          then (i+1,[(BzMeta 'f' i, param)-                   ,(BzMeta 'o' i, showOp op)+                   ,(BzMeta 'o' i, oper)                    ,(BzMeta 'v' i, val)])          else (i,[(BzParameter param, val)])     ArgParameterEmpty p e ->       let param = mapField p       in (i+1,[(BzMeta 'f' i, param)               ,(BzMeta 'o' i, if e then "isempty" else "isnotempty")])-    ArgOther c -> (i,[(BzComponent,c)])+    ArgComponent c -> (i,[(BzComponent,c)])  productVersionQuery :: ProductVersion -> [(BzFields,String)] productVersionQuery (Fedora Nothing) = [(BzProduct, "Fedora")]@@ -79,7 +77,154 @@                                       ,(BzVersion, show n)] productVersionQuery (RHEL ver) =   case versionBranch ver of-    [] -> error "Can't search RHEL without version"+    [] -> error' "Can't search RHEL without version"     [major] -> [(BzProduct, "Red Hat Enterprise Linux " ++ show major)]     (major:_) ->  [(BzProduct, "Red Hat Enterprise Linux " ++ show major)                   ,(BzVersion, showVersion ver)]++-- FIXME move map to allBzFields+mapField :: String -> String+mapField f =+  let longname =+        case f of+          "itm" -> "cf_internal_target_milestone"+          "itr" -> "cf_internal_target_release"+          "status" -> "bug_status"+          "verified" -> "cf_verified"+          "sst" -> "agile_team.name"+          "summary" -> "short_desc"+          "flag" -> "flagtypes.name"+          "flags" -> "flagtypes.name"+          p -> p+  in if longname `elem` allBzFields+     then longname+     else error' $ "unknown field: " ++ f++-- https://bugzilla.redhat.com/query.cgi?query_format=advanced+-- https://bugzilla.redhat.com/rest/field/bug+-- or: bugzilla query --json -b bug#+allBzFields :: [String]+allBzFields =+  ["agile_pool.name",+   "agile_team.name",+   "alias",+   "assigned_to",+   "assigned_to_realname",+   "attach_data.thedata",+   "attachments.description",+   "attachments.filename",+   "attachments.isobsolete",+   "attachments.ispatch",+   "attachments.isprivate",+   "attachments.mimetype",+   "attachments.submitter",+   "blocked",+   "bug_agile_pool.pool_id",+   "bug_agile_pool.pool_order",+   "bug_file_loc",+   "bug_group",+   "bug_id",+   "bug_severity",+   "bug_status",+   "cc",+   "cclist_accessible",+   "cf_approved_release",+   "cf_atomic",+   "cf_build_id",+   "cf_business_market_problem",+   "cf_category",+   "cf_clone_of",+   "cf_cloudforms_team",+   "cf_compliance_control_group",+   "cf_compliance_level",+   "cf_conditional_nak",+   "cf_crm",+   "cf_cust_facing",+   "cf_deadline",+   "cf_deadline_type",+   "cf_devdoctest",+   "cf_devel_whiteboard",+   "cf_doc_type",+   "cf_docs_score",+   "cf_documentation_action",+   "cf_environment",+   "cf_epm_cdp",+   "cf_epm_phd",+   "cf_epm_prf_state",+   "cf_epm_pri",+   "cf_epm_ptl",+   "cf_epm_put",+   "cf_final_deadline",+   "cf_fixed_in",+   "cf_internal_target_milestone",+   "cf_internal_target_release",+   "cf_internal_whiteboard",+   "cf_last_closed",+   "cf_mount_type",+   "cf_ovirt_team",+   "cf_pgm_internal",+   "cf_pm_score",+   "cf_qa_whiteboard",+   "cf_qe_conditional_nak",+   "cf_regression_status",+   "cf_release_notes",+   "cf_srtnotes",+   "cf_story_points",+   "cf_target_upstream_version",+   "cf_type",+   "cf_verified_branch",+   "cf_zstream_target_release",+   "classification",+   "comment_tag",+   "commenter",+   "component",+   "content",+   "creation_ts",+   "days_elapsed",+   "deadline",+   "delta_ts",+   "dependent_products",+   "dependson",+   "docs_contact",+   "docs_contact_realname",+   "estimated_time",+   "everconfirmed",+   "ext_bz_bug_map.ext_bz_bug_id",+   "ext_bz_bug_map.ext_status",+   "external_bugzilla.description",+   "external_bugzilla.url",+   "extra_components",+   "extra_versions",+   "flagtypes.name",+   "keywords",+   "last_visit_ts",+   "longdesc",+   "longdescs.count",+   "longdescs.isprivate",+   "op_sys",+   "owner_idle_time",+   "percentage_complete",+   "priority",+   "product",+   "qa_contact",+   "qa_contact_realname",+   "remaining_time",+   "rep_platform",+   "reporter",+   "reporter_accessible",+   "reporter_realname",+   "requestees.login_name",+   "resolution",+   "rh_rule",+   "rh_sub_components",+   "see_also",+   "setters.login_name",+   "short_desc",+   "status_whiteboard",+   "tag",+   "target_milestone",+   "target_release",+   "version",+   "view",+   "votes",+   "work_time"]
src/Help.hs view
@@ -8,149 +8,19 @@ #endif import qualified Options.Applicative.Help.Pretty as P -import ParseArg (statusList)+import ParseArg (operators, showOpHelp, statusList)  detailedHelp :: P.Doc detailedHelp =   P.vcat-  [ P.text "Tool for generating bugzilla queries"-  , P.empty-  , P.text "FIELDS = " <> P.lbrace <> P.align (P.hsep (P.punctuate P.comma (map P.text allBzFields)) <> P.rbrace)+  [ P.empty+  , P.text "Tool for generating bugzilla queries"   , P.empty   , P.text "STATUS = " <> P.lbrace <> P.align (P.fillCat (P.punctuate P.comma (map P.text (statusList ++ ["ALL"]))) <> P.rbrace)   , P.empty   , P.text "PRODUCTVERSION = " <> P.lbrace <> P.align (P.fillCat (P.punctuate P.comma (map P.text ["rawhide", "fedora", "fXY", "epel", "epelX", "rhel8", "rhel7", "rhelX.Z"])) <> P.rbrace)   , P.empty-  , P.text "'op' is " <> P.align (P.fillCat (P.punctuate P.comma (map P.text ["'='(equals)", "'!='(notequals)", "'~'(substring)", "'!~'(notsubstring)", "'=~'(regexp)", "'!=~'(notregexp)", "'~a~'(allwordssubstr)", "'!a~'(nowordssubstr)", "'~c~'(casesubstring)", "'~e~'(isempty)", "'!e~'(isnotempty)", "'~w~'(allwords)", "'!w~'(nowords)"])))+  , P.text "'op' is " <> P.align (P.fillCat (P.punctuate P.comma (map (P.text . showOpHelp) operators)) P.<//> P.text "[content~ & content!~ map to matches/notmatches]")   , P.empty   , P.text "See https://github.com/juhp/rhbzquery#readme for examples"   ]---- FIXME: filter by @redhat.com--- bugzilla query --json -b 1865911-allBzFields :: [String]-allBzFields =--- from https://bugzilla.redhat.com/rest/field/bug-  ["agile_pool.name",-   "agile_team.name",-   "alias",-   "assigned_to",-   "assigned_to_realname",-   "attach_data.thedata",-   "attachments.description",-   "attachments.filename",-   "attachments.isobsolete",-   "attachments.ispatch",-   "attachments.isprivate",-   "attachments.mimetype",-   "attachments.submitter",-   "blocked",-   "bug_agile_pool.pool_id",-   "bug_agile_pool.pool_order",-   "bug_file_loc",-   "bug_group",-   "bug_id",-   "bug_severity",-   "bug_status",-   "cc",-   "cclist_accessible",-   "cf_approved_release",-   "cf_atomic",-   "cf_build_id",-   "cf_business_market_problem",-   "cf_category",-   "cf_clone_of",-   "cf_cloudforms_team",-   "cf_compliance_control_group",-   "cf_compliance_level",-   "cf_conditional_nak",-   "cf_crm",-   "cf_cust_facing",-   "cf_deadline",-   "cf_deadline_type",-   "cf_devdoctest",-   "cf_devel_whiteboard",-   "cf_doc_type",-   "cf_docs_score",-   "cf_documentation_action",-   "cf_environment",-   "cf_epm_cdp",-   "cf_epm_phd",-   "cf_epm_prf_state",-   "cf_epm_pri",-   "cf_epm_ptl",-   "cf_epm_put",-   "cf_final_deadline",-   "cf_fixed_in",-   "cf_internal_target_milestone",-   "cf_internal_target_release",-   "cf_internal_whiteboard",-   "cf_last_closed",-   "cf_mount_type",-   "cf_ovirt_team",-   "cf_pgm_internal",-   "cf_pm_score",-   "cf_qa_whiteboard",-   "cf_qe_conditional_nak",-   "cf_regression_status",-   "cf_release_notes",-   "cf_srtnotes",-   "cf_story_points",-   "cf_target_upstream_version",-   "cf_type",-   "cf_verified_branch",-   "cf_zstream_target_release",-   "classification",-   "comment_tag",-   "commenter",-   "component",-   "content",-   "creation_ts",-   "days_elapsed",-   "deadline",-   "delta_ts",-   "dependent_products",-   "dependson",-   "docs_contact",-   "docs_contact_realname",-   "estimated_time",-   "everconfirmed",-   "ext_bz_bug_map.ext_bz_bug_id",-   "ext_bz_bug_map.ext_status",-   "external_bugzilla.description",-   "external_bugzilla.url",-   "extra_components",-   "extra_versions",-   "flagtypes.name",-   "keywords",-   "last_visit_ts",-   "longdesc",-   "longdescs.count",-   "longdescs.isprivate",-   "op_sys",-   "owner_idle_time",-   "percentage_complete",-   "priority",-   "product",-   "qa_contact",-   "qa_contact_realname",-   "remaining_time",-   "rep_platform",-   "reporter",-   "reporter_accessible",-   "reporter_realname",-   "requestees.login_name",-   "resolution",-   "rh_rule",-   "rh_sub_components",-   "see_also",-   "setters.login_name",-   "short_desc",-   "status_whiteboard",-   "tag",-   "target_milestone",-   "target_release",-   "version",-   "view",-   "votes",-   "work_time"]
src/Main.hs view
@@ -15,21 +15,22 @@ import Network.HTTP.Types import Options.Applicative (fullDesc, header, progDescDoc, #if !MIN_VERSION_simple_cmd_args(0,1,4)-                            some+                            many #endif   )-import SimpleCmd+import SimpleCmd (cmd_) import SimpleCmdArgs import System.Directory  import Bugzilla+import Common import Fields import Help import ParseArg import Paths_rhbzquery import User -data OtherQuery = CreateBug | APIQuery+data QueryMode = BugList | ListFields | CreateBug | APIQuery  main :: IO () main =@@ -37,30 +38,43 @@   run <$>   switchWith 'n' "dryrun" "Do not open url" <*>   switchWith 'm' "mine" "My bugs" <*>-  optional (flagWith' CreateBug 'f' "file" "File a bug" <|>-            flagWith' APIQuery 'w' "api" "Web API query") <*>-  some (strArg argHelp)+  (flagWith' ListFields 'l' "list-fields" "List the query FIELDs" <|>+   flagWith' CreateBug 'f' "file" "File a bug" <|>+   flagWith BugList APIQuery 'w' "api" "Web API query") <*>+  many (strArg argHelp)   where-    run :: Bool -> Bool -> Maybe OtherQuery -> [String] -> IO ()-    run dryrun mine mquery args = do+    run :: Bool -> Bool -> QueryMode -> [String] -> IO ()+    run _ _ ListFields _ = mapM_ putStrLn allBzFields+    run dryrun mine mode args = do+      when (null args) $ error' "Please specify at least one argument"       user <- if mine         then do         mail <- getBzUser         return [ArgParameter "assigned_to" Equals mail]         else return []       let argtypes = mapMaybe readBzQueryArg args-          url = case mquery of-            Just CreateBug ->-              let query = L.nub $ concatMap argToSimpleField argtypes-              in "https://" <> B.pack brc <> "/enter_bug.cgi" <> renderQuery True (bzQuery query)-            _ ->-              let status = [ArgParameter "bug_status" Equals "__open__" | not (hasStatusSet argtypes)]-                  query = L.nub $ numberMetaFields $ status ++ user ++ argtypes-              in "https://" <> B.pack brc <> "/" <> (if isNothing mquery then "buglist.cgi" else "rest/bug")<> renderQuery True (bzQuery query)+          url =+            let query =+                  case mode of+                    CreateBug -> L.nub $ concatMap argToSimpleField argtypes+                    _ ->+                      let status = [ArgParameter "bug_status" Equals "__open__" | not (hasStatusSet argtypes)]+                      in L.nub $ numberMetaFields $ status ++ user ++ argtypes+            in queryURL mode query       B.putStrLn url       unless dryrun $ do         whenJustM (findExecutable "xdg-open") $ \xdgOpen ->           cmd_ xdgOpen [B.unpack url]++    queryURL :: QueryMode -> [(BzFields,String)] -> B.ByteString+    queryURL mode query =+      "https://" <> B.pack brc <> "/" <>+      case mode of+        APIQuery -> "rest/bug"+        BugList -> "buglist.cgi"+        CreateBug -> "enter_bug.cgi"+        ListFields -> "query.cgi" -- unused+      <> renderQuery True (bzQuery query)      hasStatusSet :: [ArgType] -> Bool     hasStatusSet [] = False
src/ParseArg.hs view
@@ -3,7 +3,9 @@   readBzQueryArg,   ArgType(..),   Operator(..),+  operators,   showOp,+  showOpHelp,   ProductVersion(..),   statusList   )@@ -15,45 +17,80 @@ import Data.Version.Extra import Numeric.Natural +import Common+ argHelp :: String argHelp = "[COMPONENT|STATUS|PRODUCTVERSION|FIELD=VALUE|FIELDopVALUE]..."  data Operator = Equals | NotEqual-              | Contains | NotContain-              | ContainsCase               | Regexp |NotRegexp-              | ContainsAll | NotContainAll-              | ContainsWords | NotContainWords+              | Substring | NotSubstring+              | CaseSubstring+              | AllWordsSubstr | NoWordsSubstr+              | AllWords | NoWords+              | IsEmpty | IsNotEmpty               -- | ContentMatches | ContentNotMatch-  deriving Eq+  deriving (Eq,Show,Enum,Bounded)  showOp :: Operator -> String-showOp Equals = "equals"-showOp NotEqual = "notequals"-showOp Contains = "substring"-showOp NotContain = "notsubstring"-showOp ContainsCase = "casesubstring"-showOp Regexp = "regexp"-showOp NotRegexp = "notregexp"-showOp ContainsAll = "allwordssubstr"-showOp NotContainAll = "nowordssubstr"-showOp ContainsWords = "allwords"-showOp NotContainWords = "nowords"---showOp IsEmpty = "isempty"---showOp IsNotEmpty = "isnotempty"+showOp = lower . show +operators :: [Operator]+operators = enumFromTo minBound maxBound++data OperatorData = OpUnary String | OpNull String+  deriving Eq++opSyntax :: OperatorData -> String+opSyntax (OpUnary s) = s+opSyntax (OpNull s) = s++showOpHelp :: Operator -> String+showOpHelp op =+  "'" ++ opSyntax (opData op) ++ "'(" ++ showOp op ++ ")"++instance Ord OperatorData where+  compare op1 op2 =+    let sop1 = opSyntax op1+        sop2 = opSyntax op2+    in+      if sop1 `isInfixOf` sop2 then GT+      else if sop2 `isInfixOf` sop1 then LT+           else if sop1 == sop2+                then error' $ sop1 ++ " listed twice in 'operators'"+                else compare (reverse sop1) (reverse sop2)++-- FIXME check all unique+opData :: Operator -> OperatorData+opData Equals = OpUnary "="+opData NotEqual = OpUnary "!="+opData Substring = OpUnary "~"+opData NotSubstring = OpUnary "!~"+opData CaseSubstring = OpUnary "~c~"+opData Regexp = OpUnary "=~"+opData NotRegexp = OpUnary "!=~"+opData AllWordsSubstr = OpUnary "~a~"+opData NoWordsSubstr = OpUnary "!a~"+opData AllWords = OpUnary "~w~"+opData NoWords = OpUnary "!w~"+opData IsEmpty = OpNull "~e~"+opData IsNotEmpty = OpNull "!e~"+-- -- only for 'content':+-- opData Matches = OpUnary "~m~"+-- opData NotMatches = OpUnary "!m~"+ data ArgType = ArgProdVer ProductVersion              | ArgStatusAll              | ArgParameter String Operator String              | ArgParameterEmpty String Bool-             | ArgOther String+             | ArgComponent String  readBzQueryArg :: String -> Maybe ArgType readBzQueryArg s =   ArgProdVer <$> readProductVersion s <|>   parseStatus s <|>-  parseParam s <|>-  pure (ArgOther s)+  parseField s <|>+  parseComponent s  data ProductVersion = Fedora (Maybe Natural)                     | Rawhide@@ -80,36 +117,38 @@     else       ArgParameter "bug_status" Equals <$> find (== upper s) statusList -parseParam :: String -> Maybe ArgType-parseParam ps =-  parseParamWith "!=~" NotRegexp <|>-  parseParamWith "=~" Regexp <|>-  parseParamWith "!=" NotEqual <|>-  parseParamWith "=" Equals <|>-  parseParamWith "!~" NotContain <|>-  parseParamWith "~a~" ContainsAll <|>-  parseParamWith "!a~" NotContainAll <|>-  parseParamWith "~c~" ContainsCase <|>-  parseParamEmpty "~e~" True <|>-  parseParamEmpty "!e~" False <|>-  parseParamWith "~w~" ContainsWords <|>-  parseParamWith "!w~" NotContainWords <|>---  parseParamWith "~m~" ContentMatches <|>---  parseParamWith "!m~" ContentNotMatch <|>-  parseParamWith "~" Contains+parseField :: String -> Maybe ArgType+parseField ps =+  foldr ((<|>) . parseParam) Nothing $ sortOn opData operators   where-    parseParamWith :: String -> Operator -> Maybe ArgType-    parseParamWith op oper =-      case splitOn op ps of-        [a,b] -> if null a || null b-                 then error $ "bad parameter: " ++ ps-                 else Just (ArgParameter a oper b)-        _ -> Nothing+    parseParam :: Operator -> Maybe ArgType+    parseParam oper =+      case opData oper of+        OpUnary op ->+          if op `isInfixOf` ps then+            case splitOn op ps of+              (f:val) -> Just (ArgParameter f oper (intercalate op val))+              _ -> Nothing+          else Nothing+        OpNull op ->+          case stripSuffix op ps of+            Just a ->+              if null a+              then error' $ "bad parameter: " ++ ps+              else Just (ArgParameterEmpty a (oper == IsEmpty))+            _ -> Nothing -    parseParamEmpty :: String -> Bool -> Maybe ArgType-    parseParamEmpty op empty =-      case stripSuffix op ps of-        Just a -> if null a-                  then error $ "bad parameter: " ++ ps-                  else Just (ArgParameterEmpty a empty)-        _ -> Nothing+-- https://fedoraproject.org/wiki/Packaging:Naming?rd=Packaging:NamingGuidelines#Common_Character_Set_for_Package_Naming+-- abcdefghijklmnopqrstuvwxyz+-- ABCDEFGHIJKLMNOPQRSTUVWXYZ+-- 0123456789-._++parseComponent :: String -> Maybe ArgType+parseComponent ps =+  if all isPackageChar ps+  then Just (ArgComponent ps)+  else error' $ "Invalid component name: " ++ ps+  where+    isPackageChar :: Char -> Bool+    isPackageChar c =+      -- Bugzilla components can contain a space though+      isAsciiUpper c || isAsciiLower c || isDigit c || c `elem` " -._+"