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