]> git.ozlabs.org Git - patchwork/blob - apps/patchwork/bin/pwclient
b0bd1d175b8105464b42951ad40c9a8db1abedfa
[patchwork] / apps / patchwork / bin / pwclient
1 #!/usr/bin/env python
2 #
3 # Patchwork command line client
4 # Copyright (C) 2008 Nate Case <ncase@xes-inc.com>
5 #
6 # This file is part of the Patchwork package.
7 #
8 # Patchwork is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
12 #
13 # Patchwork is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with Patchwork; if not, write to the Free Software
20 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
21
22 import os
23 import sys
24 import xmlrpclib
25 import getopt
26 import string
27 import tempfile
28 import subprocess
29 import base64
30 import ConfigParser
31
32 # Default Patchwork remote XML-RPC server URL
33 # This script will check the PW_XMLRPC_URL environment variable
34 # for the URL to access.  If that is unspecified, it will fallback to
35 # the hardcoded default value specified here.
36 DEFAULT_URL = "http://patchwork/xmlrpc/"
37 CONFIG_FILES = [os.path.expanduser('~/.pwclientrc')]
38
39 class Filter:
40     """Filter for selecting patches."""
41     def __init__(self):
42         # These fields refer to specific objects, so they are special
43         # because we have to resolve them to IDs before passing the
44         # filter to the server
45         self.state = ""
46         self.project = ""
47
48         # The dictionary that gets passed to via XML-RPC
49         self.d = {}
50
51     def add(self, field, value):
52         if field == 'state':
53             self.state = value
54         elif field == 'project':
55             self.project = value
56         else:
57             # OK to add directly
58             self.d[field] = value
59
60     def resolve_ids(self, rpc):
61         """Resolve State, Project, and Person IDs based on filter strings."""
62         if self.state != "":
63             id = state_id_by_name(rpc, self.state)
64             if id == 0:
65                 sys.stderr.write("Note: No State found matching %s*, " \
66                                  "ignoring filter\n" % self.state)
67             else:
68                 self.d['state_id'] = id
69
70         if self.project != "":
71             id = project_id_by_name(rpc, self.project)
72             if id == 0:
73                 sys.stderr.write("Note: No Project found matching %s, " \
74                                  "ignoring filter\n" % self.project)
75             else:
76                 self.d['project_id'] = id
77
78     def __str__(self):
79         """Return human-readable description of the filter."""
80         return str(self.d)
81
82 class BasicHTTPAuthTransport(xmlrpclib.Transport):
83
84     def __init__(self, username = None, password = None):
85         self.username = username
86         self.password = password
87         xmlrpclib.Transport.__init__(self)
88
89     def authenticated(self):
90         return self.username != None and self.password != None
91
92     def send_host(self, connection, host):
93         xmlrpclib.Transport.send_host(self, connection, host)
94         if not self.authenticated():
95             return
96         credentials = '%s:%s' % (self.username, self.password)
97         auth = 'Basic ' + base64.encodestring(credentials).strip()
98         connection.putheader('Authorization', auth)
99
100 def usage():
101     sys.stderr.write("Usage: %s <action> [options]\n\n" % \
102                         (os.path.basename(sys.argv[0])))
103     sys.stderr.write("Where <action> is one of:\n")
104     sys.stderr.write(
105 """        apply <ID>    : Apply a patch (in the current dir, using -p1)
106         get <ID>      : Download a patch and save it locally
107         projects      : List all projects
108         states        : Show list of potential patch states
109         list [str]    : List patches, using the optional filters specified
110                         below and an optional substring to search for patches
111                         by name
112         search [str]  : Same as 'list'
113         view <ID>     : View a patch
114         update [-s state] [-c commit-ref] <ID>
115                       : Update patch\n""")
116     sys.stderr.write("""\nFilter options for 'list' and 'search':
117         -s <state>    : Filter by patch state (e.g., 'New', 'Accepted', etc.)
118         -p <project>  : Filter by project name (see 'projects' for list)
119         -w <who>      : Filter by submitter (name, e-mail substring search)
120         -d <who>      : Filter by delegate (name, e-mail substring search)
121         -n <max #>    : Restrict number of results\n""")
122     sys.stderr.write("""\nActions that take an ID argument can also be \
123 invoked with:
124         -h <hash>     : Lookup by patch hash\n""")
125     sys.exit(1)
126
127 def project_id_by_name(rpc, linkname):
128     """Given a project short name, look up the Project ID."""
129     if len(linkname) == 0:
130         return 0
131     projects = rpc.project_list(linkname, 0)
132     for project in projects:
133         if project['linkname'] == linkname:
134             return project['id']
135     return 0
136
137 def state_id_by_name(rpc, name):
138     """Given a partial state name, look up the state ID."""
139     if len(name) == 0:
140         return 0
141     states = rpc.state_list(name, 0)
142     for state in states:
143         if state['name'].lower().startswith(name.lower()):
144             return state['id']
145     return 0
146
147 def person_ids_by_name(rpc, name):
148     """Given a partial name or email address, return a list of the
149     person IDs that match."""
150     if len(name) == 0:
151         return []
152     people = rpc.person_list(name, 0)
153     return map(lambda x: x['id'], people)
154
155 def list_patches(patches):
156     """Dump a list of patches to stdout."""
157     print("%-5s %-12s %s" % ("ID", "State", "Name"))
158     print("%-5s %-12s %s" % ("--", "-----", "----"))
159     for patch in patches:
160         print("%-5d %-12s %s" % (patch['id'], patch['state'], patch['name']))
161
162 def action_list(rpc, filter, submitter_str, delegate_str):
163     filter.resolve_ids(rpc)
164
165     if submitter_str != "":
166         ids = person_ids_by_name(rpc, submitter_str)
167         if len(ids) == 0:
168             sys.stderr.write("Note: Nobody found matching *%s*\n", \
169                              submitter_str)
170         else:
171             for id in ids:
172                 person = rpc.person_get(id)
173                 print "Patches submitted by %s <%s>:" % \
174                         (person['name'], person['email'])
175                 f = filter
176                 f.add("submitter_id", id)
177                 patches = rpc.patch_list(f.d)
178                 list_patches(patches)
179         return
180
181     if delegate_str != "":
182         ids = person_ids_by_name(rpc, delegate_str)
183         if len(ids) == 0:
184             sys.stderr.write("Note: Nobody found matching *%s*\n", \
185                              delegate_str)
186         else:
187             for id in ids:
188                 person = rpc.person_get(id)
189                 print "Patches delegated to %s <%s>:" % \
190                         (person['name'], person['email'])
191                 f = filter
192                 f.add("delegate_id", id)
193                 patches = rpc.patch_list(f.d)
194                 list_patches(patches)
195         return
196
197     patches = rpc.patch_list(filter.d)
198     list_patches(patches)
199
200 def action_projects(rpc):
201     projects = rpc.project_list("", 0)
202     print("%-5s %-24s %s" % ("ID", "Name", "Description"))
203     print("%-5s %-24s %s" % ("--", "----", "-----------"))
204     for project in projects:
205         print("%-5d %-24s %s" % (project['id'], \
206                 project['linkname'], \
207                 project['name']))
208
209 def action_states(rpc):
210     states = rpc.state_list("", 0)
211     print("%-5s %s" % ("ID", "Name"))
212     print("%-5s %s" % ("--", "----"))
213     for state in states:
214         print("%-5d %s" % (state['id'], state['name']))
215
216 def action_get(rpc, patch_id):
217     patch = rpc.patch_get(patch_id)
218     s = rpc.patch_get_mbox(patch_id)
219
220     if patch == {} or len(s) == 0:
221         sys.stderr.write("Unable to get patch %d\n" % patch_id)
222         sys.exit(1)
223
224     base_fname = fname = os.path.basename(patch['filename'])
225     i = 0
226     while os.path.exists(fname):
227         fname = "%s.%d" % (base_fname, i)
228         i += 1
229
230     try:
231         f = open(fname, "w")
232     except:
233         sys.stderr.write("Unable to open %s for writing\n" % fname)
234         sys.exit(1)
235
236     try:
237         f.write(s)
238         f.close()
239         print "Saved patch to %s" % fname
240     except:
241         sys.stderr.write("Failed to write to %s\n" % fname)
242         sys.exit(1)
243
244 def action_apply(rpc, patch_id):
245     patch = rpc.patch_get(patch_id)
246     if patch == {}:
247         sys.stderr.write("Error getting information on patch ID %d\n" % \
248                          patch_id)
249         sys.exit(1)
250     print "Applying patch #%d to current directory" % patch_id
251     print "Description: %s" % patch['name']
252     s = rpc.patch_get_mbox(patch_id)
253     if len(s) > 0:
254         proc = subprocess.Popen(['patch', '-p1'], stdin = subprocess.PIPE)
255         proc.communicate(s)
256     else:
257         sys.stderr.write("Error: No patch content found\n")
258         sys.exit(1)
259
260 def action_update_patch(rpc, patch_id, state = None, commit = None):
261     patch = rpc.patch_get(patch_id)
262     if patch == {}:
263         sys.stderr.write("Error getting information on patch ID %d\n" % \
264                          patch_id)
265         sys.exit(1)
266
267     params = {}
268
269     if state:
270         state_id = state_id_by_name(rpc, state)
271         if state_id == 0:
272             sys.stderr.write("Error: No State found matching %s*\n" % state)
273             sys.exit(1)
274         params['state'] = state_id
275
276     if commit:
277         params['commit_ref'] = commit
278
279     success = False
280     try:
281         success = rpc.patch_set(patch_id, params)
282     except xmlrpclib.Fault, f:
283         sys.stderr.write("Error updating patch: %s\n" % f.faultString)
284
285     if not success:
286         sys.stderr.write("Patch not updated\n")
287
288 def patch_id_from_hash(rpc, project, hash):
289     try:
290         patch = rpc.patch_get_by_project_hash(project, hash)
291     except xmlrpclib.Fault:
292         # the server may not have the newer patch_get_by_project_hash function,
293         # so fall back to hash-only.
294         patch = rpc.patch_get_by_hash(hash)
295
296     if patch == {}:
297         return None
298
299     return patch['id']
300
301 auth_actions = ['update']
302
303 def main():
304     try:
305         opts, args = getopt.getopt(sys.argv[2:], 's:p:w:d:n:c:h:')
306     except getopt.GetoptError, err:
307         print str(err)
308         usage()
309
310     if len(sys.argv) < 2:
311         usage()
312
313     action = sys.argv[1].lower()
314
315     # set defaults
316     filt = Filter()
317     submitter_str = ""
318     delegate_str = ""
319     project_str = ""
320     commit_str = ""
321     state_str = ""
322     hash_str = ""
323     url = DEFAULT_URL
324
325     config = ConfigParser.ConfigParser()
326     config.read(CONFIG_FILES)
327
328     # grab settings from config files
329     if config.has_option('base', 'url'):
330         url = config.get('base', 'url')
331
332     if config.has_option('base', 'project'):
333         project_str = config.get('base', 'project')
334
335     for name, value in opts:
336         if name == '-s':
337             state_str = value
338         elif name == '-p':
339             project_str = value
340         elif name == '-w':
341             submitter_str = value
342         elif name == '-d':
343             delegate_str = value
344         elif name == '-c':
345             commit_str = value
346         elif name == '-h':
347             hash_str = value
348         elif name == '-n':
349             try:
350                 filt.add("max_count", int(value))
351             except:
352                 sys.stderr.write("Invalid maximum count '%s'\n" % value)
353                 usage()
354         else:
355             sys.stderr.write("Unknown option '%s'\n" % name)
356             usage()
357
358     if len(args) > 1:
359         sys.stderr.write("Too many arguments specified\n")
360         usage()
361
362     (username, password) = (None, None)
363     transport = None
364     if action in auth_actions:
365         if config.has_option('auth', 'username') and \
366                 config.has_option('auth', 'password'):
367
368             transport = BasicHTTPAuthTransport( \
369                     config.get('auth', 'username'),
370                     config.get('auth', 'password'))
371
372         else:
373             sys.stderr.write(("The %s action requires authentication, "
374                     "but no username or password\nis configured\n") % action)
375             sys.exit(1)
376
377     if project_str:
378         filt.add("project", project_str)
379
380     if state_str:
381         filt.add("state", state_str)
382
383     try:
384         rpc = xmlrpclib.Server(url, transport = transport)
385     except:
386         sys.stderr.write("Unable to connect to %s\n" % url)
387         sys.exit(1)
388
389     patch_id = None
390     if hash_str:
391         patch_id = patch_id_from_hash(rpc, project_str, hash_str)
392         if patch_id is None:
393             sys.stderr.write("No patch has the hash provided\n")
394             sys.exit(1)
395
396
397     if action == 'list' or action == 'search':
398         if len(args) > 0:
399             filt.add("name__icontains", args[0])
400         action_list(rpc, filt, submitter_str, delegate_str)
401
402     elif action.startswith('project'):
403         action_projects(rpc)
404
405     elif action.startswith('state'):
406         action_states(rpc)
407
408     elif action == 'view':
409         try:
410             patch_id = patch_id or int(args[0])
411         except:
412             sys.stderr.write("Invalid patch ID given\n")
413             sys.exit(1)
414
415         s = rpc.patch_get_mbox(patch_id)
416         if len(s) > 0:
417             print unicode(s).encode("utf-8")
418
419     elif action == 'get' or action == 'save':
420         try:
421             patch_id = patch_id or int(args[0])
422         except:
423             sys.stderr.write("Invalid patch ID given\n")
424             sys.exit(1)
425
426         action_get(rpc, patch_id)
427
428     elif action == 'apply':
429         try:
430             patch_id = patch_id or int(args[0])
431         except:
432             sys.stderr.write("Invalid patch ID given\n")
433             sys.exit(1)
434
435         action_apply(rpc, patch_id)
436
437     elif action == 'update':
438         try:
439             patch_id = patch_id or int(args[0])
440         except:
441             sys.stderr.write("Invalid patch ID given\n")
442             sys.exit(1)
443
444         action_update_patch(rpc, patch_id, state = state_str,
445                 commit = commit_str)
446
447     else:
448         sys.stderr.write("Unknown action '%s'\n" % action)
449         usage()
450
451 if __name__ == "__main__":
452     main()