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