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