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