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