rhbzquery 0.2 → 0.3
raw patch · 9 files changed
+317/−228 lines, 9 files
Files
- ChangeLog.md +6/−0
- README.md +5/−3
- TODO +4/−6
- rhbzquery.cabal +2/−1
- src/Common.hs +14/−0
- src/Fields.hs +161/−16
- src/Help.hs +4/−134
- src/Main.hs +30/−16
- src/ParseArg.hs +91/−52
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` " -._+"