read todo.csv from ftp.suse.com, remove uncessary caching support
[opensuse:osc-contrib.git] / osc-contrib.py
1 #
2 #   A contrib plugin for osc
3 #
4 #   This tool makes maintenance of openSUSE:Factory:Contrib more easier than an
5 #   universal osc commands.
6 #
7 #   Copyright: (c) 2009 Michal Vyskocil <mvyskocil@suse.cz>
8 #
9 #   Version: 0.1
10 #
11
12 @cmdln.option('-p', '--package', metavar='PACKAGE',
13               help='use a Package, not a submitrequest', action="store_true", default=False)
14 @cmdln.option('-m', '--message', metavar='MESSAGE',
15               help='the request message (optional)')
16 @cmdln.option('-i', '--id', metavar='REQUEST_ID',
17               help='The concrete request id. Usefull whe multiple requests exists.')
18 @cmdln.option('-l', '--last-request', action='store_true', default=False,
19               help='Use a last request (default False)')
20 @cmdln.option('-s', '--state', default = 'new',
21               help='only list requests in one of the comma separated given states [default=new]')
22 @cmdln.option('-f', '--full-view', action='store_true', default=False,
23               help='Make a full view for show command (default False)')
24 @cmdln.option('-d', '--devel-project', default=None,
25               help='Add a devel project for accepted package (default is Contrib)')
26 @cmdln.option('-k', '--keep-devel', default=False, action='store_true',
27               help='Keep a source project as a devel project (default is no keep). Can be suppressed by "-d"')
28 @cmdln.alias("cb")
29 def do_contrib(self, subcmd, opts, *args):
30     """${cmd_name}: Handling a requests for Contrib
31
32 osc subcommand for maintenance of openSUSE Contrib repository. This command
33 tries to make the maintenance process easier than common osc commands. These
34 commands are derived from existing ones, but have a different arguments.
35
36 For a backward compatibility with osc commands, all contrib commands (excluding
37 new) expects PACKAGE name, or a request ID.
38
39 osc contrib show [PACKAGE|ID]
40 Show all new requests towards Contrib. The optional argument package (or request id) will
41 filter only a requests to it.
42
43 Options:
44     -s, --state     filter the state (type 'any' for all)
45     -f, --full-view a full view of requests
46     -M, --mine      show only requests created by mine (needs osc > 0.117)
47
48 osc contrib new [DEST_PACKAGE]
49 osc contrib new PROJECT PACKAGE [DEST_PACKAGE]
50 A request for adding a new package to Contrib. When requesting from package dir
51 all necessary informations are read from osc metadata. Only a DEST_PACKAGE
52 should be givven, if you want to have another name of your package in Contrib.
53
54 If you are in common dir, then you have to specify the PROJECT and PACKAGE
55 manually and DEST_PACKAGE is also optional.
56
57 osc contrib checkout PACKAGE|ID [PACKAGE2|ID2 ...]
58 Checkout the requested package(s) (or a submit request(s)) to the current dir.
59
60 Options:
61     -i, --id        id of request (if multiple exists)
62     -l, --last      use a last request (if multiple exists)
63     -p, --package   checkout a package, not a request
64
65
66 osc contrib [accept|decline|revoke] [PACKAGE|ID]
67 Change the state of package (or request id) to <STATE>. If no package name is given, use one from cwd.
68
69 Options:
70     -i, --id        id of request (if multiple exists)
71     -l, --last      use a last request (if multiple exists)
72     -m, --message   the submit message (optional for accept)
73     -d, --devel-project setup the devel project in accept (default is Contrib itself)
74
75 osc contrib maintainer [PACKAGE [PACKAGE2 ...]]
76 Show the maintainer of defined package(s)
77
78 Options:
79     -f, --full-view      show also e-mail of maintainer
80
81 osc contrib bugonwer [PACKAGE [PACKAGE2 ...]]
82 Show the bugonwer of defined package(s)
83
84 Options:
85     -f, --full-view      show also e-mail of bugonwer
86
87 osc contrib build [ARCH]
88 Calls osc build with proper --alternative-project for test build.
89
90 osc contrib todo
91 A list of packages which should be updated.
92     """
93
94     import types
95     cmds = [cmd[12:] for cmd in dir(self) if cmd[0:12] == '_do_contrib_' and type(getattr(self, cmd)) == types.MethodType]
96     if not args or args[0] not in cmds:
97         raise oscerr.WrongArgs("Unknown contrib action. Choose one of %s." \
98                                 % ', '.join(cmds))
99     
100     command = args[0]
101
102     self.project = 'openSUSE:Factory:Contrib'
103     #self.project = 'home:mvyskocil'
104     self.apiurl  = conf.config['apiurl']
105
106     self.todo_url = "ftp://ftp.suse.com/pub/people/mvyskocil/contrib-new-version/new_version.csv"
107
108     # call
109     getattr(self, "_do_contrib_%s" % (command))(opts, args[1:])
110
111 def _compat_request(self, req):
112     #XXX: emulate the old API
113     act = req.actions[len(req.actions)-1]
114     req.dst_project = act.dst_project
115     req.dst_package = act.dst_package
116     req.src_project = act.src_project
117     req.src_package = act.src_package
118     return req
119
120
121 def _get_request_list(self, package, req_state=('new', )):
122     try:
123         return get_submit_request_list(self.apiurl, self.project, package, req_state=req_state)
124     except NameError:
125         reqs = get_request_list(self.apiurl, self.project, package, req_state=req_state)
126         return [self._compat_request(req) for req in reqs]
127
128 def _get_request(self, id):
129     try:
130         return self._compat_request(get_request(self.apiurl, id))
131     except NameError:
132         return get_submit_request(self.apiurl, id)
133
134 def _sr_from_package(self, package, reqid=None, use_last=False, req_state=('new', )):
135     if package.isdigit():
136         req = self._get_request(package)
137         if req.dst_project != self.project:
138             raise oscerr.WrongArgs("Request#'%s' has dst_project '%s', expected '%s'" % (package, req.dst_project, self.project))
139         return req
140     requests = self._get_request_list(package, req_state=req_state)
141     if len(requests) == 0:
142         raise oscerr.WrongArgs("No request for package %s found" % (package))
143     elif len(requests) > 1:
144         if use_last:
145             requests = requests[-1:]
146         elif reqid == None:
147             raise oscerr.WrongArgs(
148             "There are multiple requests (%s) towards package %s. Specify one by -i/--id, or use -l/--last-request argument!" %
149             (", ".join([str(r.reqid) for r in requests]), package))
150         else:
151             ret = [req for req in requests if req.reqid == int(reqid)]
152             if len(ret) == 0:
153                 raise oscerr.WrongArgs("The package %s and request id %s doesn't match! \
154                         Use one of these (%s)" % (package, reqid, ", ".join([str(r.reqid) for r in requests])))
155             requests = ret
156
157     return requests[0]
158
159 def _do_contrib_show(self, opts, args):
160
161     package = ''
162     if len(args) > 0:
163         package = args[0]
164
165     state_list = opts.state.split(',') if opts.state != 'any' else ('', )
166     
167     srs = self._get_request_list(package, req_state=state_list)
168     if opts.full_view:
169         for sr in srs:
170             print(sr)
171     else:
172         for sr in srs:
173             print(sr.list_view())
174
175 def _do_contrib_new(self, opts, args):
176     if is_package_dir(os.getcwdu()):
177         src_project = store_read_project(os.getcwdu())
178         src_package = store_read_package(os.getcwdu())
179         dest_package = src_package
180         if len(args) > 0:
181             dest_package = args[0]
182     else:
183         if len(args) < 2:
184             raise oscerr.WrongArgs("The source project and package names are mandatory!!")
185         src_project, src_package = args[0], args[1]
186         if not src_package in meta_get_packagelist(self.apiurl, src_project):
187             raise oscerr.WrongArgs("Package '%s' don't exists in project '%s'" % (src_package, src_project))
188         dest_package = src_package
189         if len(args) == 3:
190             dest_package = args[2]
191
192     message = opts.message or "please add a '%s' to Contrib" % (dest_package)
193     if src_package.isdigit():
194         raise oscerr.WrongArgs('Numeric name of package is not allowed. Please add some alpha character')
195     id = create_submit_request(self.apiurl, src_project, src_package, self.project, dest_package, message)
196     print("Request id %s created" % (id))
197
198 def _do_contrib_accept(self, opts, args):
199
200     return self._contrib_sr_change(opts, args, "accepted", 
201            opts.message)
202
203 def _do_contrib_decline(self, opts, args):
204
205     if not opts.message:
206         raise oscerr.WrongArgs('A message is mandatory for decline')
207     
208     return self._contrib_sr_change(opts, args, "declined",
209            opts.message)
210
211 def _do_contrib_revoke(self, opts, args):
212     
213     if not opts.message:
214         raise oscerr.WrongArgs('A message is mandatory for decline')
215     
216     return self._contrib_sr_change(opts, args, "revoked",
217            opts.message)
218
219
220 def _do_contrib_co(self, opts, args):
221     return self._do_contrib_checkout(opts, args)
222
223 def _do_contrib_checkout(self, opts, args):
224     if len(args) < 1:
225         raise oscerr.WrongArgs("The package names are mandatory!!")
226
227     for package in args:
228         if opts.package:
229             src_project = self.project
230             src_package = package
231         else:
232             try:
233                 request = self._sr_from_package(package, opts.id, opts.last_request)
234             except oscerr.WrongArgs, wa_exc:
235                 raise oscerr.WrongArgs("".join(wa_exc.args) + \
236                     "\nUse -p/--package argument is you try to download a package from %s" % (self.project))
237             src_project = request.src_project
238             src_package = request.src_package
239
240         try:
241             checkout_package(self.apiurl,
242                 src_project, src_package,
243                 expand_link=True)
244         except oscerr.NoWorkingCopy:
245             # suppress this message
246             pass
247
248 def _do_contrib_build(self, opts, args):
249     import optparse
250     opts = optparse.Values(defaults=self.do_build.optparser.defaults)
251     opts.alternative_project = self.project
252     arch = ''
253     if len(args) > 1:
254         arch = args[0]
255     print('osc build --alternative-project %s standard %s' % (self.project, arch))
256     return self.do_build('build', opts, 'standard', arch)
257
258 def __do_contrib_role(self, role, opts, args):
259     if len(args) == 0:
260         args = [self._get_package(args, "The package names are mandatory!!"), ]
261
262     for package in args:
263         meta = self._get_meta_xml(package)
264         for person in self._get_roles_from_meta(meta, role):
265             userid = person.get('userid')
266             data = userid
267             if opts.full_view:
268                 data = ", ".join(get_user_data(self.apiurl, userid, 'login', 'email'))
269             print "%s: %s" % (package, data)
270
271 def _do_contrib_maintainer(self, opts, args):
272     return self.__do_contrib_role('maintainer', opts, args)
273
274 def _do_contrib_bugowner(self, opts, args):
275     return self.__do_contrib_role('bugowner', opts, args)
276
277 def _do_contrib_todo(self, opts, args):
278     todo = self._get_todo_info()
279     self._pprint(todo)
280
281 # the original API is *very* ugly!!
282 # return the meta in an xml form first
283 def _get_meta_xml(self, package):
284     path = quote_plus(self.project),
285     kind = 'prj'
286     if package:
287         path = path + (quote_plus(package),)
288         kind = 'pkg'
289     data = meta_exists(metatype=kind,
290                        path_args=path,
291                        template_args=None,
292                        create_new=False)
293     if data:
294         return ET.fromstring(''.join(data))
295     raise oscerr.PackageError('Meta data for package %s missing' % (package))
296
297 # return all persons from meta
298 def _get_persons_from_meta(self, meta):
299     return meta.getiterator('person')
300
301 def _get_roles_from_meta(self, meta, role):
302     assert(role in ['maintainer', 'bugowner'])
303     return [p for p in self._get_persons_from_meta(meta) if p.get('role') == role]
304
305 def _has_user_role(self, meta, role, user):
306     assert(role in ['maintainer', 'bugowner'])
307     if not get_user_meta(self.apiurl, user):
308         raise oscerr.WrongArgs("The user %s doesn't exists" % (user))
309
310     return user in [p.get('userid') for p in self._get_roles_from_meta(meta, role)]
311
312 # from osc.core, FIXME, this is broken
313 # look at the svn, or send a patch to obs
314 def _addBugowner(self, apiurl, prj, pac, user):
315     """ add a new bugowner to a package or project """
316     path = quote_plus(prj),
317     kind = 'prj'
318     if pac:
319         path = path + (quote_plus(pac),)
320         kind = 'pkg'
321     data = meta_exists(metatype=kind,
322                        path_args=path,
323                        template_args=None,
324                        create_new=False)
325                        
326     if data and get_user_meta(apiurl, user) != None:
327         tree = ET.fromstring(''.join(data))
328         found = False
329         for person in tree.getiterator('person'):
330             if person.get('userid') == user and person.get('role') == 'bugowner':
331                 found = True
332                 print "user already exists"
333                 break
334         if not found:
335             # the xml has a fixed structure
336             tree.insert(2, ET.Element('person', role='bugowner', userid=user))
337             print 'user \'%s\' added to \'%s\'' % (user, pac or prj)
338             edit_meta(metatype=kind,
339                       path_args=path,
340                       data=ET.tostring(tree))
341     else:
342         print "osc: an error occured"
343
344 # from osc.core, FIXME, this is broken
345 # look at the svn, or send a patch to obs
346 def _addDevelProject(self, apiurl, prj, pac, devel_project, devel_package=None):
347     """ add a new devel project to a package """
348     path = (quote_plus(prj), quote_plus(pac))
349     data = meta_exists(metatype='pkg',
350                        path_args=path,
351                        template_args=None,
352                        create_new=False)
353                        
354     if data:
355         tree = ET.fromstring(''.join(data))
356
357         if tree.find('devel') == None:
358             ET.SubElement(tree, 'devel')
359         elem = tree.find('devel')
360         elem.attrib['project'] = prj
361         elem.attrib['package'] = devel_package or pac
362         
363         edit_meta(metatype='pkg',
364                     path_args=path,
365                     data=ET.tostring(tree))
366     else:
367         print "osc: an error occured"
368
369 def _get_package(self, args, error_message):
370     """ read package name from cwd if is not specified """
371     if len(args) == 0:
372         if not is_package_dir('.'):
373             raise oscerr.WrongArgs(error_message)
374         return store_read_package('.')
375     
376     return args[0]
377
378 def _change_request_state(self, id, newstate, message=''):
379     try:
380         return change_request_state(self.apiurl, id, newstate, message)
381     except NameError:
382         return change_submit_request_state(self.apiurl, id, newstate, message)
383
384
385 def _contrib_sr_change(self, opts, args, action, message):
386
387 #    if len(args) == 0:
388 #        raise oscerr.WrongArgs('The package name is mandatory for %s' % (action))
389
390     pkg = self._get_package(args, \
391           'The package name is mandatory for %s,\nif you are not in a working dir' % (action))
392
393     request = self._sr_from_package(pkg, opts.id, opts.last_request)
394     package = request.dst_package
395     
396     id = str(request.reqid)
397
398     # check the current state
399     sr = self._get_request(id)
400
401     if sr.state.name != 'new':
402         print "The state of %s request was changed to '%s' by '%s'" % (package, sr.state.name, sr.state.who)
403         res = raw_input("Do you want to change it to '%s'? [y/N] " % (action))
404         if res != 'y' and res != 'Y':
405             return
406
407     # check before change of commit request
408     is_new_package = not package in meta_get_packagelist(self.apiurl, self.project)
409     if action == 'accepted' and is_new_package:
410         if not message:
411             message = ""
412         else:
413             message += "\n"
414         message += "You are now a maintainer of %s in openSUSE:Factory:Contrib" % (package)
415
416         if package in meta_get_packagelist(self.apiurl, 'openSUSE:Factory'):
417             ret = raw_input("The '%s' was found in openSUSE:Factory project. Are you sure to accept this package [y/N] " % (package))
418             if ret != 'y' and ret != 'Y':
419                 print "Package was not accepted"
420                 return 0
421
422     # change to the state action
423     response = self._change_request_state(id, action, message)
424     
425     # change the state for a new packages
426     if action == 'accepted' and is_new_package:
427         # fix the maintainer and a bugowner
428         meta = self._get_meta_xml(package)
429         who = sr.state.who
430         if len(sr.statehistory) != 0:
431             who = sr.statehistory[0].who
432         if not self._has_user_role(meta, 'maintainer', who):
433             new_sr = self._get_request(id)
434             delMaintainer(self.apiurl, self.project, package, new_sr.state.who)
435             addMaintainer(self.apiurl, self.project, package, who)
436         if not self._has_user_role(meta, 'bugowner', who):
437             self._addBugowner(self.apiurl, self.project, package, who)
438         
439         if opts.devel_project:
440             devel_project = opts.devel_project
441         elif opts.keep_devel:
442             devel_project = request.src_project
443         else:
444             devel_project = self.project
445
446         self._addDevelProject(self.apiurl, self.project, package, devel_project)
447
448     print(response)
449
450 ########################################
451 #           TODO HANDLING              #
452 ########################################
453 def _get_todo_from_list(self, todo_list):
454     import csv
455     todo = dict()
456     # read a content
457     todo_reader = csv.reader(todo_list)
458     
459     try:
460         for pkg, new, old, maintainers in todo_reader:
461             todo[pkg] = (new, old, maintainers)
462     except ValueError:
463         pass
464
465     return todo
466
467 def _pprint(self, todo):
468     max_w = [0, 0, 0, 0]
469     title = ('Package', 'New', 'Current', 'Maintainer')
470     max_w[0] = max(map(lambda x: len(x), todo.keys())     + [len(title[0]), ])
471     max_w[1] = max(map(lambda x: len(x[0]), todo.values())+ [len(title[1]), ])
472     max_w[2] = max(map(lambda x: len(x[1]), todo.values())+ [len(title[2]), ])
473
474     i = [s.ljust(max_w[i]) for i, s in enumerate(title)]
475     str_title = "%s %s %s %s" % (i[0], i[1], i[2], i[3])
476     print str_title
477     print len(str_title)*"-"
478
479     for key in todo:
480         pkg = key
481         new, old, maintainers = todo[key]
482         i = [s.ljust(max_w[i]) for i, s in enumerate((pkg, new, old))]
483         print "%s %s %s (%s)" % (i[0], i[1], i[2], maintainers.replace(" ", ", "))
484     print len(str_title)*"-"
485     print "(This is just for your information.\nYou decide whether to upgrade a package or not.)"
486
487
488 def _get_todo_info(self):
489     import urllib2
490
491     todo = dict()
492
493     try:
494         resp = urllib2.urlopen(self.todo_url)
495
496         if resp.headers.get('content-type') != 'text/x-comma-separated-values':
497             print >>sys.stderr, "Unknown content-type %s" % (resp.headers.get('content-type'), )
498             sys.exit(2)
499
500         todo = self._get_todo_from_list(resp.readlines())
501
502     except urllib2.URLError, ue:
503         print >>sys.stderr, ue.reason
504         sys.exit(1)
505
506     finally:
507         resp.close()
508
509     return todo