1+ import json
12import os
23import re
34import shlex
45import subprocess
56import tempfile
67import textwrap
8+ import urllib .request as urllib
79from collections import deque
810from dataclasses import dataclass
911from datetime import datetime , timedelta , timezone
2022from snakeoil .strings import pluralism
2123
2224from .. import git
23- from .argparsers import cwd_repo_argparser , git_repo_argparser
25+ from .argparsers import cwd_repo_argparser , git_repo_argparser , BugzillaApiKey
2426
2527mask = arghparse .ArgumentParser (
2628 prog = "pkgdev mask" ,
2729 description = "mask packages" ,
2830 parents = (cwd_repo_argparser , git_repo_argparser ),
2931)
32+ BugzillaApiKey .mangle_argparser (mask )
3033mask .add_argument (
3134 "targets" ,
3235 metavar = "TARGET" ,
8184 ``x11-misc/xdg-utils`` package.
8285 """ ,
8386)
87+ mask_opts .add_argument (
88+ "--file-bug" ,
89+ action = "store_true" ,
90+ help = "file a last-rite bug" ,
91+ docs = """
92+ Files a last-rite bug for the masked package, which blocks listed
93+ reference bugs. ``PMASKED`` keyword is added all all referenced bugs.
94+ """ ,
95+ )
8496
8597
8698@mask .bind_final_check
8799def _mask_validate (parser , namespace ):
88- atoms = []
100+ atoms = set ()
101+ maintainers = set ()
102+
103+ if not namespace .rites and namespace .file_bug :
104+ mask .error ("bug filing requires last rites" )
105+ if namespace .file_bug and not namespace .api_key :
106+ mask .error ("bug filing requires a Bugzilla API key" )
89107
90108 if namespace .email and not namespace .rites :
91109 mask .error ("last rites required for email support" )
@@ -96,23 +114,30 @@ def _mask_validate(parser, namespace):
96114 restrict = namespace .repo .path_restrict (x )
97115 pkg = next (namespace .repo .itermatch (restrict ))
98116 atom = pkg .versioned_atom
117+ maintainers .update (maintainer .email for maintainer in pkg .maintainers )
99118 else :
100119 try :
101120 atom = atom_cls (x )
102121 except MalformedAtom :
103122 mask .error (f"invalid atom: { x !r} " )
104- if not namespace .repo .match (atom ):
123+ if pkgs := namespace .repo .match (atom ):
124+ maintainers .update (
125+ maintainer .email for pkg in pkgs for maintainer in pkg .maintainers
126+ )
127+ else :
105128 mask .error (f"no repo matches: { x !r} " )
106- atoms .append (atom )
129+ atoms .add (atom )
107130 else :
108131 restrict = namespace .repo .path_restrict (os .getcwd ())
109132 # repo, category, and package level restricts
110133 if len (restrict ) != 3 :
111134 mask .error ("not in a package directory" )
112135 pkg = next (namespace .repo .itermatch (restrict ))
113- atoms .append (pkg .unversioned_atom )
136+ atoms .add (pkg .unversioned_atom )
137+ maintainers .update (maintainer .email for maintainer in pkg .maintainers )
114138
115139 namespace .atoms = sorted (atoms )
140+ namespace .maintainers = sorted (maintainers ) or ["maintainer-needed@gentoo.org" ]
116141
117142
118143@dataclass (frozen = True )
@@ -208,38 +233,24 @@ def __str__(self):
208233 return "" .join (self .header ) + "\n \n " .join (map (str , self .masks ))
209234
210235
211- def get_comment (bugs , rites : int ):
236+ def get_comment ():
212237 """Spawn editor to get mask comment."""
213238 tmp = tempfile .NamedTemporaryFile (mode = "w" )
214- summary = []
215- if rites :
216- summary .append (f"Removal on { datetime .now (timezone .utc ) + timedelta (days = rites ):%Y-%m-%d} ." )
217- if bugs :
218- # Bug(s) #A, #B, #C
219- bug_list = ", " .join (f"#{ b } " for b in bugs )
220- s = pluralism (bugs )
221- summary .append (f"Bug{ s } { bug_list } ." )
222- if summary := " " .join (summary ):
223- tmp .write (f"\n { summary } " )
224239 tmp .write (
225240 textwrap .dedent (
226241 """
227242
228243 # Please enter the mask message. Lines starting with '#' will be ignored.
229244 #
230- # - Best last rites (removal) practices -
245+ # If last-rite was requested, it would be added automatically.
231246 #
232- # Include the following info:
233- # a) reason for masking
234- # b) bug # for the removal (and yes you should have one)
235- # c) date of removal (either the date or "in x days")
247+ # For rules on writing mask messages, see GLEP-84:
248+ # https://glep.gentoo.org/glep-0084.html
236249 #
237250 # Example:
238251 #
239- # Masked for removal in 30 days. Doesn't work
240- # with new libfoo. Upstream dead, gtk-1, smells
252+ # Doesn't work with new libfoo. Upstream dead, gtk-1, smells
241253 # funny.
242- # Bug #987654
243254 """
244255 )
245256 )
@@ -262,10 +273,71 @@ def get_comment(bugs, rites: int):
262273 comment = "\n " .join (comment ).strip ().splitlines ()
263274 if not comment :
264275 mask .error ("empty mask comment" )
265-
266276 return comment
267277
268278
279+ def message_removal_notice (bugs : list [int ], rites : int ):
280+ summary = []
281+ if rites :
282+ summary .append (f"Removal on { datetime .now (timezone .utc ) + timedelta (days = rites ):%Y-%m-%d} ." )
283+ if bugs :
284+ # Bug(s) #A, #B, #C
285+ bug_list = ", " .join (f"#{ b } " for b in bugs )
286+ s = pluralism (bugs )
287+ summary .append (f"Bug{ s } { bug_list } ." )
288+ return " " .join (summary )
289+
290+
291+ def file_last_rites_bug (options , message : str ) -> int :
292+ summary = f"{ ', ' .join (map (str , options .atoms ))} : removal"
293+ if len (summary ) > 90 and len (options .atoms ) > 1 :
294+ summary = f"{ options .atoms [0 ]} and friends: removal"
295+ request_data = dict (
296+ Bugzilla_api_key = options .api_key ,
297+ product = "Gentoo Linux" ,
298+ component = "Current packages" ,
299+ version = "unspecified" ,
300+ summary = summary ,
301+ description = "\n " .join ([* message , "" , "package list:" , * map (str , options .atoms )]).strip (),
302+ keywords = ["PMASKED" ],
303+ assigned_to = options .maintainers [0 ],
304+ cc = options .maintainers [1 :] + ["treecleaner@gentoo.org" ],
305+ deadline = (datetime .now (timezone .utc ) + timedelta (days = options .rites )).strftime ("%Y-%m-%d" ),
306+ blocks = list (options .bugs ),
307+ )
308+ request = urllib .Request (
309+ url = "https://bugs.gentoo.org/rest/bug" ,
310+ data = json .dumps (request_data ).encode ("utf-8" ),
311+ method = "POST" ,
312+ headers = {
313+ "Content-Type" : "application/json" ,
314+ "Accept" : "application/json" ,
315+ },
316+ )
317+ with urllib .urlopen (request , timeout = 30 ) as response :
318+ reply = json .loads (response .read ().decode ("utf-8" ))
319+ return int (reply ["id" ])
320+
321+
322+ def update_bugs_pmasked (api_key : str , bugs : list [int ]):
323+ request_data = dict (
324+ Bugzilla_api_key = api_key ,
325+ ids = bugs ,
326+ keywords = dict (add = ["PMASKED" ]),
327+ )
328+ request = urllib .Request (
329+ url = f"https://bugs.gentoo.org/rest/bug/{ bugs [0 ]} " ,
330+ data = json .dumps (request_data ).encode ("utf-8" ),
331+ method = "PUT" ,
332+ headers = {
333+ "Content-Type" : "application/json" ,
334+ "Accept" : "application/json" ,
335+ },
336+ )
337+ with urllib .urlopen (request , timeout = 30 ) as response :
338+ return response .status == 200
339+
340+
269341def send_last_rites_email (m : Mask , subject_prefix : str ):
270342 try :
271343 atoms = ", " .join (map (str , m .atoms ))
@@ -298,16 +370,25 @@ def _mask(options, out, err):
298370 p = git .run ("config" , "user.email" , stdout = subprocess .PIPE )
299371 email = p .stdout .strip ()
300372
301- # initial args for Mask obj
302- mask_args = {
303- "author" : author ,
304- "email" : email ,
305- "date" : today .strftime ("%Y-%m-%d" ),
306- "comment" : get_comment (options .bugs , options .rites ),
307- "atoms" : options .atoms ,
308- }
309-
310- m = Mask (** mask_args )
373+ message = get_comment ()
374+ if options .file_bug :
375+ if bug_no := file_last_rites_bug (options , message ):
376+ out .write (out .fg ("green" ), f"filed bug https://bugs.gentoo.org/{ bug_no } " , out .reset )
377+ out .flush ()
378+ if not update_bugs_pmasked (options .api_key , options .bugs ):
379+ err .write (err .fg ("red" ), "failed to update referenced bugs" , err .reset )
380+ err .flush ()
381+ options .bugs .insert (0 , bug_no )
382+ if removal := message_removal_notice (options .bugs , options .rites ):
383+ message .append (removal )
384+
385+ m = Mask (
386+ author = author ,
387+ email = email ,
388+ date = today .strftime ("%Y-%m-%d" ),
389+ comment = message ,
390+ atoms = options .atoms ,
391+ )
311392 mask_file .add (m )
312393 mask_file .write ()
313394
0 commit comments