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