]> git.ozlabs.org Git - patchwork/blob - apps/patchwork/bin/pwclient
pwclient: fix handling of UTF-8 char in submitter name
[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                         (unicode(person['name']).encode("utf-8"), \
183                          unicode(person['email']).encode("utf-8"))
184                 f = filter
185                 f.add("submitter_id", id)
186                 patches = rpc.patch_list(f.d)
187                 list_patches(patches)
188         return
189
190     if delegate_str != "":
191         ids = person_ids_by_name(rpc, delegate_str)
192         if len(ids) == 0:
193             sys.stderr.write("Note: Nobody found matching *%s*\n", \
194                              delegate_str)
195         else:
196             for id in ids:
197                 person = rpc.person_get(id)
198                 print "Patches delegated to %s <%s>:" % \
199                         (person['name'], person['email'])
200                 f = filter
201                 f.add("delegate_id", id)
202                 patches = rpc.patch_list(f.d)
203                 list_patches(patches)
204         return
205
206     patches = rpc.patch_list(filter.d)
207     list_patches(patches)
208
209 def action_projects(rpc):
210     projects = rpc.project_list("", 0)
211     print("%-5s %-24s %s" % ("ID", "Name", "Description"))
212     print("%-5s %-24s %s" % ("--", "----", "-----------"))
213     for project in projects:
214         print("%-5d %-24s %s" % (project['id'], \
215                 project['linkname'], \
216                 project['name']))
217
218 def action_states(rpc):
219     states = rpc.state_list("", 0)
220     print("%-5s %s" % ("ID", "Name"))
221     print("%-5s %s" % ("--", "----"))
222     for state in states:
223         print("%-5d %s" % (state['id'], state['name']))
224
225 def action_get(rpc, patch_id):
226     patch = rpc.patch_get(patch_id)
227     s = rpc.patch_get_mbox(patch_id)
228
229     if patch == {} or len(s) == 0:
230         sys.stderr.write("Unable to get patch %d\n" % patch_id)
231         sys.exit(1)
232
233     base_fname = fname = os.path.basename(patch['filename'])
234     i = 0
235     while os.path.exists(fname):
236         fname = "%s.%d" % (base_fname, i)
237         i += 1
238
239     try:
240         f = open(fname, "w")
241     except:
242         sys.stderr.write("Unable to open %s for writing\n" % fname)
243         sys.exit(1)
244
245     try:
246         f.write(unicode(s).encode("utf-8"))
247         f.close()
248         print "Saved patch to %s" % fname
249     except:
250         sys.stderr.write("Failed to write to %s\n" % fname)
251         sys.exit(1)
252
253 def action_apply(rpc, patch_id):
254     patch = rpc.patch_get(patch_id)
255     if patch == {}:
256         sys.stderr.write("Error getting information on patch ID %d\n" % \
257                          patch_id)
258         sys.exit(1)
259     print "Applying patch #%d to current directory" % patch_id
260     print "Description: %s" % patch['name']
261     s = rpc.patch_get_mbox(patch_id)
262     if len(s) > 0:
263         proc = subprocess.Popen(['patch', '-p1'], stdin = subprocess.PIPE)
264         proc.communicate(s)
265     else:
266         sys.stderr.write("Error: No patch content found\n")
267         sys.exit(1)
268
269 def action_update_patch(rpc, patch_id, state = None, commit = None):
270     patch = rpc.patch_get(patch_id)
271     if patch == {}:
272         sys.stderr.write("Error getting information on patch ID %d\n" % \
273                          patch_id)
274         sys.exit(1)
275
276     params = {}
277
278     if state:
279         state_id = state_id_by_name(rpc, state)
280         if state_id == 0:
281             sys.stderr.write("Error: No State found matching %s*\n" % state)
282             sys.exit(1)
283         params['state'] = state_id
284
285     if commit:
286         params['commit_ref'] = commit
287
288     success = False
289     try:
290         success = rpc.patch_set(patch_id, params)
291     except xmlrpclib.Fault, f:
292         sys.stderr.write("Error updating patch: %s\n" % f.faultString)
293
294     if not success:
295         sys.stderr.write("Patch not updated\n")
296
297 def patch_id_from_hash(rpc, project, hash):
298     try:
299         patch = rpc.patch_get_by_project_hash(project, hash)
300     except xmlrpclib.Fault:
301         # the server may not have the newer patch_get_by_project_hash function,
302         # so fall back to hash-only.
303         patch = rpc.patch_get_by_hash(hash)
304
305     if patch == {}:
306         return None
307
308     return patch['id']
309
310 auth_actions = ['update']
311
312 def main():
313     try:
314         opts, args = getopt.getopt(sys.argv[2:], 's:p:w:d:n:c:h:')
315     except getopt.GetoptError, err:
316         print str(err)
317         usage()
318
319     if len(sys.argv) < 2:
320         usage()
321
322     action = sys.argv[1].lower()
323
324     # set defaults
325     filt = Filter()
326     submitter_str = ""
327     delegate_str = ""
328     project_str = ""
329     commit_str = ""
330     state_str = ""
331     hash_str = ""
332     url = DEFAULT_URL
333
334     config = ConfigParser.ConfigParser()
335     config.read(CONFIG_FILES)
336
337     # grab settings from config files
338     if config.has_option('base', 'url'):
339         url = config.get('base', 'url')
340
341     if config.has_option('base', 'project'):
342         project_str = config.get('base', 'project')
343
344     for name, value in opts:
345         if name == '-s':
346             state_str = value
347         elif name == '-p':
348             project_str = value
349         elif name == '-w':
350             submitter_str = value
351         elif name == '-d':
352             delegate_str = value
353         elif name == '-c':
354             commit_str = value
355         elif name == '-h':
356             hash_str = value
357         elif name == '-n':
358             try:
359                 filt.add("max_count", int(value))
360             except:
361                 sys.stderr.write("Invalid maximum count '%s'\n" % value)
362                 usage()
363         else:
364             sys.stderr.write("Unknown option '%s'\n" % name)
365             usage()
366
367     if len(args) > 1:
368         sys.stderr.write("Too many arguments specified\n")
369         usage()
370
371     (username, password) = (None, None)
372     transport = None
373     if action in auth_actions:
374         if config.has_option('auth', 'username') and \
375                 config.has_option('auth', 'password'):
376
377             use_https = url.startswith('https')
378
379             transport = BasicHTTPAuthTransport( \
380                     config.get('auth', 'username'),
381                     config.get('auth', 'password'),
382                     use_https)
383
384         else:
385             sys.stderr.write(("The %s action requires authentication, "
386                     "but no username or password\nis configured\n") % action)
387             sys.exit(1)
388
389     if project_str:
390         filt.add("project", project_str)
391
392     if state_str:
393         filt.add("state", state_str)
394
395     try:
396         rpc = xmlrpclib.Server(url, transport = transport)
397     except:
398         sys.stderr.write("Unable to connect to %s\n" % url)
399         sys.exit(1)
400
401     patch_id = None
402     if hash_str:
403         patch_id = patch_id_from_hash(rpc, project_str, hash_str)
404         if patch_id is None:
405             sys.stderr.write("No patch has the hash provided\n")
406             sys.exit(1)
407
408
409     if action == 'list' or action == 'search':
410         if len(args) > 0:
411             filt.add("name__icontains", args[0])
412         action_list(rpc, filt, submitter_str, delegate_str)
413
414     elif action.startswith('project'):
415         action_projects(rpc)
416
417     elif action.startswith('state'):
418         action_states(rpc)
419
420     elif action == 'view':
421         try:
422             patch_id = patch_id or int(args[0])
423         except:
424             sys.stderr.write("Invalid patch ID given\n")
425             sys.exit(1)
426
427         s = rpc.patch_get_mbox(patch_id)
428         if len(s) > 0:
429             print unicode(s).encode("utf-8")
430
431     elif action == 'get' or action == 'save':
432         try:
433             patch_id = patch_id or int(args[0])
434         except:
435             sys.stderr.write("Invalid patch ID given\n")
436             sys.exit(1)
437
438         action_get(rpc, patch_id)
439
440     elif action == 'apply':
441         try:
442             patch_id = patch_id or int(args[0])
443         except:
444             sys.stderr.write("Invalid patch ID given\n")
445             sys.exit(1)
446
447         action_apply(rpc, patch_id)
448
449     elif action == 'update':
450         try:
451             patch_id = patch_id or int(args[0])
452         except:
453             sys.stderr.write("Invalid patch ID given\n")
454             sys.exit(1)
455
456         action_update_patch(rpc, patch_id, state = state_str,
457                 commit = commit_str)
458
459     else:
460         sys.stderr.write("Unknown action '%s'\n" % action)
461         usage()
462
463 if __name__ == "__main__":
464     main()