]> git.ozlabs.org Git - patchwork/blob - apps/patchwork/bin/pwclient
dc836e918121fdac782e2111987817ecf33d2d93
[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.SafeTransport):
83
84     def __init__(self, username = None, password = None, use_https = False):
85         self.username = username
86         self.password = password
87         self.use_https = use_https
88         xmlrpclib.SafeTransport.__init__(self)
89
90     def authenticated(self):
91         return self.username != None and self.password != None
92
93     def send_host(self, connection, host):
94         xmlrpclib.Transport.send_host(self, connection, host)
95         if not self.authenticated():
96             return
97         credentials = '%s:%s' % (self.username, self.password)
98         auth = 'Basic ' + base64.encodestring(credentials).strip()
99         connection.putheader('Authorization', auth)
100
101     def make_connection(self, host):
102         if self.use_https:
103             fn = xmlrpclib.SafeTransport.make_connection
104         else:
105             fn = xmlrpclib.Transport.make_connection
106         return fn(self, host)
107
108 def usage():
109     sys.stderr.write("Usage: %s <action> [options]\n\n" % \
110                         (os.path.basename(sys.argv[0])))
111     sys.stderr.write("Where <action> is one of:\n")
112     sys.stderr.write(
113 """        apply <ID>    : Apply a patch (in the current dir, using -p1)
114         get <ID>      : Download a patch and save it locally
115         projects      : List all projects
116         states        : Show list of potential patch states
117         list [str]    : List patches, using the optional filters specified
118                         below and an optional substring to search for patches
119                         by name
120         search [str]  : Same as 'list'
121         view <ID>     : View a patch
122         update [-s state] [-c commit-ref] <ID>
123                       : Update patch\n""")
124     sys.stderr.write("""\nFilter options for 'list' and 'search':
125         -s <state>    : Filter by patch state (e.g., 'New', 'Accepted', etc.)
126         -p <project>  : Filter by project name (see 'projects' for list)
127         -w <who>      : Filter by submitter (name, e-mail substring search)
128         -d <who>      : Filter by delegate (name, e-mail substring search)
129         -n <max #>    : Restrict number of results\n""")
130     sys.stderr.write("""\nActions that take an ID argument can also be \
131 invoked with:
132         -h <hash>     : Lookup by patch hash\n""")
133     sys.exit(1)
134
135 def project_id_by_name(rpc, linkname):
136     """Given a project short name, look up the Project ID."""
137     if len(linkname) == 0:
138         return 0
139     projects = rpc.project_list(linkname, 0)
140     for project in projects:
141         if project['linkname'] == linkname:
142             return project['id']
143     return 0
144
145 def state_id_by_name(rpc, name):
146     """Given a partial state name, look up the state ID."""
147     if len(name) == 0:
148         return 0
149     states = rpc.state_list(name, 0)
150     for state in states:
151         if state['name'].lower().startswith(name.lower()):
152             return state['id']
153     return 0
154
155 def person_ids_by_name(rpc, name):
156     """Given a partial name or email address, return a list of the
157     person IDs that match."""
158     if len(name) == 0:
159         return []
160     people = rpc.person_list(name, 0)
161     return map(lambda x: x['id'], people)
162
163 def list_patches(patches):
164     """Dump a list of patches to stdout."""
165     print("%-5s %-12s %s" % ("ID", "State", "Name"))
166     print("%-5s %-12s %s" % ("--", "-----", "----"))
167     for patch in patches:
168         print("%-5d %-12s %s" % (patch['id'], patch['state'], patch['name']))
169
170 def action_list(rpc, filter, submitter_str, delegate_str):
171     filter.resolve_ids(rpc)
172
173     if submitter_str != "":
174         ids = person_ids_by_name(rpc, submitter_str)
175         if len(ids) == 0:
176             sys.stderr.write("Note: Nobody found matching *%s*\n", \
177                              submitter_str)
178         else:
179             for id in ids:
180                 person = rpc.person_get(id)
181                 print "Patches submitted by %s <%s>:" % \
182                         (person['name'], person['email'])
183                 f = filter
184                 f.add("submitter_id", id)
185                 patches = rpc.patch_list(f.d)
186                 list_patches(patches)
187         return
188
189     if delegate_str != "":
190         ids = person_ids_by_name(rpc, delegate_str)
191         if len(ids) == 0:
192             sys.stderr.write("Note: Nobody found matching *%s*\n", \
193                              delegate_str)
194         else:
195             for id in ids:
196                 person = rpc.person_get(id)
197                 print "Patches delegated to %s <%s>:" % \
198                         (person['name'], person['email'])
199                 f = filter
200                 f.add("delegate_id", id)
201                 patches = rpc.patch_list(f.d)
202                 list_patches(patches)
203         return
204
205     patches = rpc.patch_list(filter.d)
206     list_patches(patches)
207
208 def action_projects(rpc):
209     projects = rpc.project_list("", 0)
210     print("%-5s %-24s %s" % ("ID", "Name", "Description"))
211     print("%-5s %-24s %s" % ("--", "----", "-----------"))
212     for project in projects:
213         print("%-5d %-24s %s" % (project['id'], \
214                 project['linkname'], \
215                 project['name']))
216
217 def action_states(rpc):
218     states = rpc.state_list("", 0)
219     print("%-5s %s" % ("ID", "Name"))
220     print("%-5s %s" % ("--", "----"))
221     for state in states:
222         print("%-5d %s" % (state['id'], state['name']))
223
224 def action_get(rpc, patch_id):
225     patch = rpc.patch_get(patch_id)
226     s = rpc.patch_get_mbox(patch_id)
227
228     if patch == {} or len(s) == 0:
229         sys.stderr.write("Unable to get patch %d\n" % patch_id)
230         sys.exit(1)
231
232     base_fname = fname = os.path.basename(patch['filename'])
233     i = 0
234     while os.path.exists(fname):
235         fname = "%s.%d" % (base_fname, i)
236         i += 1
237
238     try:
239         f = open(fname, "w")
240     except:
241         sys.stderr.write("Unable to open %s for writing\n" % fname)
242         sys.exit(1)
243
244     try:
245         f.write(unicode(s).encode("utf-8"))
246         f.close()
247         print "Saved patch to %s" % fname
248     except:
249         sys.stderr.write("Failed to write to %s\n" % fname)
250         sys.exit(1)
251
252 def action_apply(rpc, patch_id):
253     patch = rpc.patch_get(patch_id)
254     if patch == {}:
255         sys.stderr.write("Error getting information on patch ID %d\n" % \
256                          patch_id)
257         sys.exit(1)
258     print "Applying patch #%d to current directory" % patch_id
259     print "Description: %s" % patch['name']
260     s = rpc.patch_get_mbox(patch_id)
261     if len(s) > 0:
262         proc = subprocess.Popen(['patch', '-p1'], stdin = subprocess.PIPE)
263         proc.communicate(s)
264     else:
265         sys.stderr.write("Error: No patch content found\n")
266         sys.exit(1)
267
268 def action_update_patch(rpc, patch_id, state = None, commit = None):
269     patch = rpc.patch_get(patch_id)
270     if patch == {}:
271         sys.stderr.write("Error getting information on patch ID %d\n" % \
272                          patch_id)
273         sys.exit(1)
274
275     params = {}
276
277     if state:
278         state_id = state_id_by_name(rpc, state)
279         if state_id == 0:
280             sys.stderr.write("Error: No State found matching %s*\n" % state)
281             sys.exit(1)
282         params['state'] = state_id
283
284     if commit:
285         params['commit_ref'] = commit
286
287     success = False
288     try:
289         success = rpc.patch_set(patch_id, params)
290     except xmlrpclib.Fault, f:
291         sys.stderr.write("Error updating patch: %s\n" % f.faultString)
292
293     if not success:
294         sys.stderr.write("Patch not updated\n")
295
296 def patch_id_from_hash(rpc, project, hash):
297     try:
298         patch = rpc.patch_get_by_project_hash(project, hash)
299     except xmlrpclib.Fault:
300         # the server may not have the newer patch_get_by_project_hash function,
301         # so fall back to hash-only.
302         patch = rpc.patch_get_by_hash(hash)
303
304     if patch == {}:
305         return None
306
307     return patch['id']
308
309 auth_actions = ['update']
310
311 def main():
312     try:
313         opts, args = getopt.getopt(sys.argv[2:], 's:p:w:d:n:c:h:')
314     except getopt.GetoptError, err:
315         print str(err)
316         usage()
317
318     if len(sys.argv) < 2:
319         usage()
320
321     action = sys.argv[1].lower()
322
323     # set defaults
324     filt = Filter()
325     submitter_str = ""
326     delegate_str = ""
327     project_str = ""
328     commit_str = ""
329     state_str = ""
330     hash_str = ""
331     url = DEFAULT_URL
332
333     config = ConfigParser.ConfigParser()
334     config.read(CONFIG_FILES)
335
336     # grab settings from config files
337     if config.has_option('base', 'url'):
338         url = config.get('base', 'url')
339
340     if config.has_option('base', 'project'):
341         project_str = config.get('base', 'project')
342
343     for name, value in opts:
344         if name == '-s':
345             state_str = value
346         elif name == '-p':
347             project_str = value
348         elif name == '-w':
349             submitter_str = value
350         elif name == '-d':
351             delegate_str = value
352         elif name == '-c':
353             commit_str = value
354         elif name == '-h':
355             hash_str = value
356         elif name == '-n':
357             try:
358                 filt.add("max_count", int(value))
359             except:
360                 sys.stderr.write("Invalid maximum count '%s'\n" % value)
361                 usage()
362         else:
363             sys.stderr.write("Unknown option '%s'\n" % name)
364             usage()
365
366     if len(args) > 1:
367         sys.stderr.write("Too many arguments specified\n")
368         usage()
369
370     (username, password) = (None, None)
371     transport = None
372     if action in auth_actions:
373         if config.has_option('auth', 'username') and \
374                 config.has_option('auth', 'password'):
375
376             use_https = url.startswith('https')
377
378             transport = BasicHTTPAuthTransport( \
379                     config.get('auth', 'username'),
380                     config.get('auth', 'password'),
381                     use_https)
382
383         else:
384             sys.stderr.write(("The %s action requires authentication, "
385                     "but no username or password\nis configured\n") % action)
386             sys.exit(1)
387
388     if project_str:
389         filt.add("project", project_str)
390
391     if state_str:
392         filt.add("state", state_str)
393
394     try:
395         rpc = xmlrpclib.Server(url, transport = transport)
396     except:
397         sys.stderr.write("Unable to connect to %s\n" % url)
398         sys.exit(1)
399
400     patch_id = None
401     if hash_str:
402         patch_id = patch_id_from_hash(rpc, project_str, hash_str)
403         if patch_id is None:
404             sys.stderr.write("No patch has the hash provided\n")
405             sys.exit(1)
406
407
408     if action == 'list' or action == 'search':
409         if len(args) > 0:
410             filt.add("name__icontains", args[0])
411         action_list(rpc, filt, submitter_str, delegate_str)
412
413     elif action.startswith('project'):
414         action_projects(rpc)
415
416     elif action.startswith('state'):
417         action_states(rpc)
418
419     elif action == 'view':
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         s = rpc.patch_get_mbox(patch_id)
427         if len(s) > 0:
428             print unicode(s).encode("utf-8")
429
430     elif action == 'get' or action == 'save':
431         try:
432             patch_id = patch_id or int(args[0])
433         except:
434             sys.stderr.write("Invalid patch ID given\n")
435             sys.exit(1)
436
437         action_get(rpc, patch_id)
438
439     elif action == 'apply':
440         try:
441             patch_id = patch_id or int(args[0])
442         except:
443             sys.stderr.write("Invalid patch ID given\n")
444             sys.exit(1)
445
446         action_apply(rpc, patch_id)
447
448     elif action == 'update':
449         try:
450             patch_id = patch_id or int(args[0])
451         except:
452             sys.stderr.write("Invalid patch ID given\n")
453             sys.exit(1)
454
455         action_update_patch(rpc, patch_id, state = state_str,
456                 commit = commit_str)
457
458     else:
459         sys.stderr.write("Unknown action '%s'\n" % action)
460         usage()
461
462 if __name__ == "__main__":
463     main()