Show a maintainer's email
[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', metavar='STATE',
21               help='Show a requests with a specified state.')
22 @cmdln.option('-f', '--full-view', action='store_true', default=False,
23               help='Make a full view for show command (default False)')
24 @cmdln.alias("cb")
25 def do_contrib(self, subcmd, opts, *args):
26     """${cmd_name}: Handling a requests for Contrib
27
28 osc subcommand for maintenance of openSUSE Contrib repository. This command
29 tries to make the maintenance process easier than common osc commands. These
30 commands are derived from existing ones, but have a different arguments.
31
32 For a backward compatibility with osc commands, all contrib commands (excluding
33 new) expects PACKAGE name, or a request ID.
34
35 osc contrib show [PACKAGE|ID]
36 Show all new requests towards Contrib. The optional argument package (or request id) will
37 filter only a requests to it.
38
39 Options:
40     -s, --state     filter the state (type 'any' for all)
41     -f, --full-view a full view of requests
42
43 osc contrib new [DEST_PACKAGE]
44 osc contrib new PROJECT PACKAGE [DEST_PACKAGE]
45 A request for adding a new package to Contrib. When requesting from package dir
46 all necessary informations are read from osc metadata. Only a DEST_PACKAGE
47 should be givven, if you want to have another name of your package in Contrib.
48
49 If you are in common dir, then you have to specify the PROJECT and PACKAGE
50 manually and DEST_PACKAGE is also optional.
51
52 osc contrib checkout PACKAGE|ID [PACKAGE2|ID2 ...]
53 Checkout the requested package(s) (or a submit request(s)) to the current dir.
54
55 Options:
56     -i, --id        id of request (if multiple exists)
57     -l, --last      use a last request (if multiple exists)
58     -p, --package   checkout a package, not a request
59
60
61 osc contrib [accept|decline|revoke] PACKAGE|ID
62 Change the state of package (or request id) to <STATE>
63
64 Options:
65     -i, --id        id of request (if multiple exists)
66     -l, --last      use a last request (if multiple exists)
67     -m, --message   the submit message (optional for accept)
68
69 osc contrib maintainer PACKAGE [PACKAGE2 ...]
70 Show the maintainer of defined package(s)
71
72 Options:
73     -f, --full-view      show also e-mail of maintainer
74
75 osc contrib build [ARCH]
76 Calls osc build with proper --alternative-project for test build.
77     """
78
79     import types
80     cmds = [cmd[12:] for cmd in dir(self) if cmd[0:12] == '_do_contrib_' and type(getattr(self, cmd)) == types.MethodType]
81     if not args or args[0] not in cmds:
82         raise oscerr.WrongArgs("Unknown contrib action. Choose one of %s." \
83                                 % ', '.join(cmds))
84     
85     command = args[0]
86
87     self.project = 'openSUSE:Factory:Contrib'
88     #self.project = 'home:mvyskocil'
89     self.apiurl  = conf.config['apiurl']
90
91     # call
92     getattr(self, "_do_contrib_%s" % (command))(opts, args[1:])
93
94 def _sr_from_package(self, package, reqid=None, use_last=False, req_state='new'):
95     if package.isdigit():
96         req = get_submit_request(self.apiurl, package)
97         if req.dst_project != self.project:
98             raise oscerr.WrongArgs("Request#'%s' has dst_project '%s', expected '%s'" % (package, req.dst_project, self.project))
99         return req
100     requests = get_submit_request_list(self.apiurl, self.project, package, req_state=req_state)
101     if len(requests) == 0:
102         raise oscerr.WrongArgs("No request for package %s found" % (package))
103     elif len(requests) > 1:
104         if use_last:
105             requests = requests[-1:]
106         elif reqid == None:
107             raise oscerr.WrongArgs(
108             "There are multiple requests (%s) towards package %s. Specify one by -i/--id, or use -l/--last-request argument!" %
109             (", ".join([str(r.reqid) for r in requests]), package))
110         else:
111             ret = [req for req in requests if req.reqid == int(reqid)]
112             if len(ret) == 0:
113                 raise oscerr.WrongArgs("The package %s and request id %s doesn't match! \
114                         Use one of these (%s)" % (package, reqid, ", ".join([str(r.reqid) for r in requests])))
115             requests = ret
116
117     return requests[0]
118
119 def _do_contrib_show(self, opts, args):
120
121     package = ''
122     if len(args) > 0:
123         package = args[0]
124
125     state = opts.state or 'new'
126     if state == 'any':
127         state = ''
128     
129     srs = get_submit_request_list(self.apiurl, self.project, package, req_state=state)
130     if opts.full_view:
131         for sr in srs:
132             print(sr)
133     else:
134         for sr in srs:
135             print(sr.list_view())
136
137 def _do_contrib_new(self, opts, args):
138     if is_package_dir(os.getcwdu()):
139         src_project = store_read_project(os.getcwdu())
140         src_package = store_read_package(os.getcwdu())
141         dest_package = src_package
142         if len(args) > 0:
143             dest_package = args[0]
144     else:
145         if len(args) < 2:
146             raise oscerr.WrongArgs("The source project and package names are mandatory!!")
147         src_project, src_package = args[0], args[1]
148         if not src_package in meta_get_packagelist(self.apiurl, src_project):
149             raise oscerr.WrongArgs("Package '%s' don't exists in project '%s'" % (src_package, src_project))
150         dest_package = src_package
151         if len(args) == 3:
152             dest_package = args[2]
153
154     message = opts.message or "please add a '%s' to Contrib" % (dest_package)
155     if src_package.isdigit():
156         raise oscerr.WrongArgs('Numeric name of package is not allowed. Please add some alpha character')
157     id = create_submit_request(self.apiurl, src_project, src_package, self.project, dest_package, message)
158     print("Request id %s created" % (id))
159
160 def _do_contrib_accept(self, opts, args):
161
162     return self._contrib_sr_change(opts, args, "accepted", 
163            "Reviewed and checked OK.")
164
165 def _do_contrib_decline(self, opts, args):
166
167     if not opts.message:
168         raise oscerr.WrongArgs('A message is mandatory for decline')
169     
170     return self._contrib_sr_change(opts, args, "declined",
171            opts.message)
172
173 def _do_contrib_revoke(self, opts, args):
174     
175     if not opts.message:
176         raise oscerr.WrongArgs('A message is mandatory for decline')
177     
178     return self._contrib_sr_change(opts, args, "revoked",
179            opts.message)
180
181
182 def _do_contrib_co(self, opts, args):
183     return self._do_contrib_checkout(opts, args)
184
185 def _do_contrib_checkout(self, opts, args):
186     if len(args) < 1:
187         raise oscerr.WrongArgs("The package names are mandatory!!")
188
189     for package in args:
190         if opts.package:
191             src_project = self.project
192             src_package = package
193         else:
194             try:
195                 request = self._sr_from_package(package, opts.id, opts.last_request)
196             except oscerr.WrongArgs, wa_exc:
197                 raise oscerr.WrongArgs("".join(wa_exc.args) + \
198                     "\nUse -p/--package argument is you try to download a package from %s" % (self.project))
199             src_project = request.src_project
200             src_package = request.src_package
201
202         checkout_package(self.apiurl,
203                 src_project, src_package,
204                 expand_link=True)
205
206 def _do_contrib_build(self, opts, args):
207     import optparse
208     opts = optparse.Values(defaults=self.do_build.optparser.defaults)
209     opts.alternative_project = self.project
210     arch = ''
211     if len(args) > 1:
212         arch = args[0]
213     print('osc build --alternative-project %s standard %s' % (self.project, arch))
214     return self.do_build('build', opts, 'standard', arch)
215
216 def _do_contrib_maintainer(self, opts, args):
217     if len(args) == 0:
218         raise oscerr.WrongArgs("The package names are mandatory!!")
219
220     for package in args:
221         meta = self._get_meta_xml(package)
222         for person in self._get_roles_from_meta(meta, 'maintainer'):
223             userid = person.get('userid')
224             data = userid
225             if opts.full_view:
226                 data = ", ".join(get_user_data(self.apiurl, userid, 'login', 'email'))
227             print "%s: %s" % (package, data)
228
229
230 # the original API is *very* ugly!!
231 # return the meta in an xml form first
232 def _get_meta_xml(self, package):
233     path = quote_plus(self.project),
234     kind = 'prj'
235     if package:
236         path = path + (quote_plus(package),)
237         kind = 'pkg'
238     data = meta_exists(metatype=kind,
239                        path_args=path,
240                        template_args=None,
241                        create_new=False)
242     if data:
243         return ET.fromstring(''.join(data))
244     raise oscerr.PackageError('Meta data for package %s missing' % (package))
245
246 # return all persons from meta
247 def _get_persons_from_meta(self, meta):
248     return meta.getiterator('person')
249
250 def _get_roles_from_meta(self, meta, role):
251     assert(role in ['maintainer', 'bugowner'])
252     return [p for p in self._get_persons_from_meta(meta) if p.get('role') == role]
253
254 def _has_user_role(self, meta, role, user):
255     assert(role in ['maintainer', 'bugowner'])
256     if not get_user_meta(self.apiurl, user):
257         raise oscerr.WrongArgs("The user %s doesn't exists" % (user))
258
259     return user in [p.get('userid') for p in self._get_roles_from_meta(meta, role)]
260
261 # from osc.core, FIXME, this is broken
262 # look at the svn, or send a patch to obs
263 def _addBugowner(self, apiurl, prj, pac, user):
264     """ add a new bugowner to a package or project """
265     path = quote_plus(prj),
266     kind = 'prj'
267     if pac:
268         path = path + (quote_plus(pac),)
269         kind = 'pkg'
270     data = meta_exists(metatype=kind,
271                        path_args=path,
272                        template_args=None,
273                        create_new=False)
274                        
275     if data and get_user_meta(apiurl, user) != None:
276         tree = ET.fromstring(''.join(data))
277         found = False
278         for person in tree.getiterator('person'):
279             if person.get('userid') == user and person.get('role') == 'bugowner':
280                 found = True
281                 print "user already exists"
282                 break
283         if not found:
284             # the xml has a fixed structure
285             tree.insert(2, ET.Element('person', role='bugowner', userid=user))
286             print 'user \'%s\' added to \'%s\'' % (user, pac or prj)
287             edit_meta(metatype=kind,
288                       path_args=path,
289                       data=ET.tostring(tree))
290     else:
291         print "osc: an error occured"
292
293 def _contrib_sr_change(self, opts, args, action, message):
294
295     if len(args) == 0:
296         raise oscerr.WrongArgs('The package name is mandatory for %s' % (action))
297
298     request = self._sr_from_package(args[0], opts.id, opts.last_request)
299     package = request.dst_package
300     
301     id = str(request.reqid)
302
303     # check the current state
304     sr = get_submit_request(self.apiurl, id)
305
306     if sr.state.name != 'new':
307         print "The state of %s request was changed to '%s' by '%s'" % (package, sr.state.name, sr.state.who)
308         res = raw_input("Do you want to change it to '%s'? [y/N] " % (action))
309         if res != 'y' and res != 'Y':
310             return
311
312     # check before change of commit request
313     is_new_package = not package in meta_get_packagelist(self.apiurl, self.project)
314     if action == 'accepted' and is_new_package:
315         message = "You are now a maintainer of %s in openSUSE:Factory:Contrib" % (package)
316
317         if package in meta_get_packagelist(self.apiurl, 'openSUSE:Factory'):
318             ret = raw_input("The '%s' was found in openSUSE:Factory project. Are you sure to accept this package [y/N] " % (package))
319             if ret != 'y' and ret != 'Y':
320                 print "Package was not accepted"
321                 return 0
322
323     # change to the state action
324     response = change_submit_request_state(self.apiurl, id, action, message)
325     
326     # change the state for a new packages
327     if action == 'accepted' and is_new_package:
328         # fix the maintainer and a bugowner
329         meta = self._get_meta_xml(package)
330         who = sr.state.who
331         if len(sr.statehistory) != 0:
332             who = sr.statehistory[0].who
333         if not self._has_user_role(meta, 'maintainer', who):
334             new_sr = get_submit_request(self.apiurl, id)
335             delMaintainer(self.apiurl, self.project, package, new_sr.state.who)
336             addMaintainer(self.apiurl, self.project, package, who)
337         if not self._has_user_role(meta, 'bugowner', who):
338             self._addBugowner(self.apiurl, self.project, package, who)
339
340     print(response)