Fix bug in pw
[org-mode/org-jambu.git] / UTILITIES / pw
blob76aa2f91713f619ed8fed3ab11cca31ef905b8cb
1 #!/usr/bin/env python
3 # Patchwork command line client
4 # Copyright (C) 2008 Nate Case <ncase@xes-inc.com>
6 # This file is part of the Patchwork package.
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.
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.
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
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 import datetime
32 import smtplib
33 import urllib
34 import re
36 from email.mime.text import MIMEText
38 notify_on_state_change = {
39 'Accepted': 'emacs-orgmode@gnu.org',
40 'RFC': 'emacs-orgmode@gnu.org'
43 # Default Patchwork remote XML-RPC server URL
44 # This script will check the PW_XMLRPC_URL environment variable
45 # for the URL to access. If that is unspecified, it will fallback to
46 # the hardcoded default value specified here.
47 DEFAULT_URL = "http://patchwork.newartisans.com/xmlrpc/"
48 CONFIG_FILES = [os.path.expanduser('~/.pwclientrc')]
50 class Filter:
51 """Filter for selecting patches."""
52 def __init__(self):
53 # These fields refer to specific objects, so they are special
54 # because we have to resolve them to IDs before passing the
55 # filter to the server
56 self.state = ""
57 self.project = ""
59 # The dictionary that gets passed to via XML-RPC
60 self.d = {}
62 def add(self, field, value):
63 if field == 'state':
64 self.state = value
65 elif field == 'project':
66 self.project = value
67 else:
68 # OK to add directly
69 self.d[field] = value
71 def resolve_ids(self, rpc):
72 """Resolve State, Project, and Person IDs based on filter strings."""
73 if self.state != "":
74 id = state_id_by_name(rpc, self.state)
75 if id == 0:
76 sys.stderr.write("Note: No State found matching %s*, " \
77 "ignoring filter\n" % self.state)
78 else:
79 self.d['state_id'] = id
81 if self.project != "":
82 id = project_id_by_name(rpc, self.project)
83 if id == 0:
84 sys.stderr.write("Note: No Project found matching %s, " \
85 "ignoring filter\n" % self.project)
86 else:
87 self.d['project_id'] = id
89 def __str__(self):
90 """Return human-readable description of the filter."""
91 return str(self.d)
93 class BasicHTTPAuthTransport(xmlrpclib.SafeTransport):
95 def __init__(self, username = None, password = None, use_https = False):
96 self.username = username
97 self.password = password
98 self.use_https = use_https
99 xmlrpclib.SafeTransport.__init__(self)
101 def authenticated(self):
102 return self.username != None and self.password != None
104 def send_host(self, connection, host):
105 xmlrpclib.Transport.send_host(self, connection, host)
106 if not self.authenticated():
107 return
108 credentials = '%s:%s' % (self.username, self.password)
109 auth = 'Basic ' + base64.encodestring(credentials).strip()
110 connection.putheader('Authorization', auth)
112 def make_connection(self, host):
113 if self.use_https:
114 fn = xmlrpclib.SafeTransport.make_connection
115 else:
116 fn = xmlrpclib.Transport.make_connection
117 return fn(self, host)
119 def usage():
120 sys.stderr.write("Usage: %s <action> [options]\n\n" % \
121 (os.path.basename(sys.argv[0])))
122 sys.stderr.write("Where <action> is one of:\n")
123 sys.stderr.write(
124 """ apply <ID> : Apply a patch (in the current dir, using -p1)
125 get <ID> : Download a patch and save it locally
126 projects : List all projects
127 states : Show list of potential patch states
128 list [str] : List patches, using the optional filters specified
129 below and an optional substring to search for patches
130 by name
131 search [str] : Same as 'list'
132 view <ID> : View a patch
133 update [-s state] [-c commit-ref] <ID>
134 : Update patch\n""")
135 sys.stderr.write("""\nFilter options for 'list' and 'search':
136 -s <state> : Filter by patch state (e.g., 'New', 'Accepted', etc.)
137 -p <project> : Filter by project name (see 'projects' for list)
138 -w <who> : Filter by submitter (name, e-mail substring search)
139 -d <who> : Filter by delegate (name, e-mail substring search)
140 -n <max #> : Restrict number of results\n""")
141 sys.stderr.write("""\nActions that take an ID argument can also be \
142 invoked with:
143 -h <hash> : Lookup by patch hash\n""")
144 sys.exit(1)
146 def project_id_by_name(rpc, linkname):
147 """Given a project short name, look up the Project ID."""
148 if len(linkname) == 0:
149 return 0
150 projects = rpc.project_list(linkname, 0)
151 for project in projects:
152 if project['linkname'] == linkname:
153 return project['id']
154 return 0
156 def state_id_by_name(rpc, name):
157 """Given a partial state name, look up the state ID."""
158 if len(name) == 0:
159 return 0
160 states = rpc.state_list(name, 0)
161 for state in states:
162 if state['name'].lower().startswith(name.lower()):
163 return state['id']
164 return 0
166 def person_ids_by_name(rpc, name):
167 """Given a partial name or email address, return a list of the
168 person IDs that match."""
169 if len(name) == 0:
170 return []
171 people = rpc.person_list(name, 0)
172 return map(lambda x: x['id'], people)
174 def list_patches(patches):
175 """Dump a list of patches to stdout."""
176 print("%-5s %-12s %s" % ("ID", "State", "Name"))
177 print("%-5s %-12s %s" % ("--", "-----", "----"))
178 for patch in patches:
179 print("%-5d %-12s %s" % (patch['id'], patch['state'], patch['name']))
181 def action_list(rpc, filter, submitter_str, delegate_str):
182 filter.resolve_ids(rpc)
184 if submitter_str != "":
185 ids = person_ids_by_name(rpc, submitter_str)
186 if len(ids) == 0:
187 sys.stderr.write("Note: Nobody found matching *%s*\n", \
188 submitter_str)
189 else:
190 for id in ids:
191 person = rpc.person_get(id)
192 print "Patches submitted by %s <%s>:" % \
193 (person['name'], person['email'])
194 f = filter
195 f.add("submitter_id", id)
196 patches = rpc.patch_list(f.d)
197 list_patches(patches)
198 return
200 if delegate_str != "":
201 ids = person_ids_by_name(rpc, delegate_str)
202 if len(ids) == 0:
203 sys.stderr.write("Note: Nobody found matching *%s*\n", \
204 delegate_str)
205 else:
206 for id in ids:
207 person = rpc.person_get(id)
208 print "Patches delegated to %s <%s>:" % \
209 (person['name'], person['email'])
210 f = filter
211 f.add("delegate_id", id)
212 patches = rpc.patch_list(f.d)
213 list_patches(patches)
214 return
216 patches = rpc.patch_list(filter.d)
217 list_patches(patches)
219 def action_projects(rpc):
220 projects = rpc.project_list("", 0)
221 print("%-5s %-24s %s" % ("ID", "Name", "Description"))
222 print("%-5s %-24s %s" % ("--", "----", "-----------"))
223 for project in projects:
224 print("%-5d %-24s %s" % (project['id'], \
225 project['linkname'], \
226 project['name']))
228 def action_states(rpc):
229 states = rpc.state_list("", 0)
230 print("%-5s %s" % ("ID", "Name"))
231 print("%-5s %s" % ("--", "----"))
232 for state in states:
233 print("%-5d %s" % (state['id'], state['name']))
235 def action_get(rpc, patch_id):
236 patch = rpc.patch_get(patch_id)
237 s = rpc.patch_get_mbox(patch_id)
239 if patch == {} or len(s) == 0:
240 sys.stderr.write("Unable to get patch %d\n" % patch_id)
241 sys.exit(1)
243 base_fname = fname = os.path.basename(patch['filename'])
244 i = 0
245 while os.path.exists(fname):
246 fname = "%s.%d" % (base_fname, i)
247 i += 1
249 try:
250 f = open(fname, "w")
251 except:
252 sys.stderr.write("Unable to open %s for writing\n" % fname)
253 sys.exit(1)
255 try:
256 f.write(unicode(s).encode("utf-8"))
257 f.close()
258 print "Saved patch to %s" % fname
259 except:
260 sys.stderr.write("Failed to write to %s\n" % fname)
261 sys.exit(1)
263 def action_apply(rpc, patch_id):
264 patch = rpc.patch_get(patch_id)
265 if patch == {}:
266 sys.stderr.write("Error getting information on patch ID %d\n" % \
267 patch_id)
268 sys.exit(1)
269 print "Applying patch #%d to current directory" % patch_id
270 print "Description: %s" % patch['name']
271 s = rpc.patch_get_mbox(patch_id)
272 if len(s) > 0:
273 proc = subprocess.Popen(['patch', '-p1'], stdin = subprocess.PIPE)
274 proc.communicate(s)
275 else:
276 sys.stderr.write("Error: No patch content found\n")
277 sys.exit(1)
279 def action_update_patch(rpc, patch_id, state = None, commit = None,
280 delegate_str = "", archived = False):
281 patch = rpc.patch_get(patch_id)
282 if patch == {}:
283 sys.stderr.write("Error getting information on patch ID %d\n" % \
284 patch_id)
285 sys.exit(1)
287 params = {}
289 delegate_id = None
290 if delegate_str != "":
291 params['delegate'] = delegate_str
292 ids = person_ids_by_name(rpc, delegate_str)
293 if len(ids) == 0:
294 sys.stderr.write("Note: Nobody found matching *%s*\n"% \
295 delegate_str)
296 else:
297 delegate_id = ids[0]
299 if state:
300 state_id = state_id_by_name(rpc, state)
301 if state_id == 0:
302 sys.stderr.write("Error: No State found matching %s*\n" % state)
303 sys.exit(1)
304 params['state'] = state_id
306 if state in notify_on_state_change:
307 if not delegate_id:
308 sys.stderr.write("Error: Delete (-d) required for this update\n")
309 sys.exit(1)
311 person = rpc.person_get(delegate_id)
312 submitter = rpc.person_get(patch['submitter_id'])
314 from_addr = '%s <%s>' % (person['name'], person['email'])
315 cc_addr = '%s <%s>' % (submitter['name'], submitter['email'])
316 to_addr = notify_on_state_change[state]
318 longdesc = '''Patch %s (http://patchwork.newartisans.com/patch/%s/) is now %s.
320 This relates to the following submission:
322 http://mid.gmane.org/%s''' % \
323 (patch['id'], patch['id'], state, urllib.quote(patch['msgid']))
324 shortdesc = 'Patch %s %s' % (patch['id'], state)
326 msg = MIMEText(longdesc)
328 msg['Subject'] = 'Patchwork: ' + shortdesc
329 msg['From'] = from_addr
330 msg['To'] = to_addr
331 #msg['Cc'] = cc_addr
332 msg['References'] = patch['msgid']
334 # Send the message via our own SMTP server, but don't include
335 # the envelope header.
336 try:
337 s = smtplib.SMTP('localhost')
338 print "Sending e-mail to: %s, %s" % (to_addr, cc_addr)
339 s.sendmail(from_addr, [to_addr, cc_addr], msg.as_string())
340 s.quit()
341 except:
342 sys.stderr.write("Warning: Failed to send e-mail " +
343 "(no SMTP server listening at localhost?)\n")
345 if commit:
346 params['commit_ref'] = commit
348 if archived:
349 params['archived'] = archived
351 success = False
352 try:
353 print "Updating patch %d to state '%s', delegate %s" % \
354 (patch_id, state, delegate_str)
355 success = rpc.patch_set(patch_id, params)
356 except xmlrpclib.Fault, f:
357 sys.stderr.write("Error updating patch: %s\n" % f.faultString)
359 if not success:
360 sys.stderr.write("Patch not updated\n")
362 def patch_id_from_hash(rpc, project, hash):
363 try:
364 patch = rpc.patch_get_by_project_hash(project, hash)
365 except xmlrpclib.Fault:
366 # the server may not have the newer patch_get_by_project_hash function,
367 # so fall back to hash-only.
368 patch = rpc.patch_get_by_hash(hash)
370 if patch == {}:
371 return None
373 return patch['id']
375 def branch_with(patch_id, rpc, delegate_str):
376 s = rpc.patch_get_mbox(patch_id)
377 if len(s) > 0:
378 print unicode(s).encode("utf-8")
380 patch = rpc.patch_get(patch_id)
382 # Find the latest commit from the day before the patch
383 proc = subprocess.Popen(['git', 'log', '--until=' + patch['date'],
384 '-1', '--format=%H', 'master'],
385 stdout = subprocess.PIPE)
386 sha = proc.stdout.read()[:-1]
388 # Create a topic branch named after this commit
389 proc = subprocess.Popen(['git', 'checkout', '-b', 't/patch%s' %
390 patch_id, sha])
391 sts = os.waitpid(proc.pid, 0)
392 if sts[1] != 0:
393 sys.stderr.write("Could not create branch for patch\n")
394 return
396 # Apply the patch to the branch
397 fname = '/tmp/patch'
398 try:
399 f = open(fname, "w")
400 except:
401 sys.stderr.write("Unable to open %s for writing\n" % fname)
402 sys.exit(1)
404 try:
405 f.write(unicode(s).encode("utf-8"))
406 f.close()
407 print "Saved patch to %s" % fname
408 except:
409 sys.stderr.write("Failed to write to %s\n" % fname)
410 sys.exit(1)
412 proc = subprocess.Popen(['git', 'am', '/tmp/patch'])
413 sts = os.waitpid(proc.pid, 0)
414 if sts[1] != 0:
415 sys.stderr.write("Failed to apply patch to branch\n")
416 proc = subprocess.Popen(['git', 'checkout', 'master'])
417 os.waitpid(proc.pid, 0)
418 proc = subprocess.Popen(['git', 'branch', '-D', 't/patch%s' %
419 patch_id, sha])
420 os.waitpid(proc.pid, 0)
421 return
423 # If it succeeded this far, mark the patch as "Under Review" by the
424 # invoking user.
425 action_update_patch(rpc, patch_id, state = 'Under Review',
426 delegate_str = delegate_str)
428 proc = subprocess.Popen(['git', 'rebase', 'master'])
429 sts = os.waitpid(proc.pid, 0)
431 print sha
433 def merge_with(patch_id, rpc, delegate_str):
434 s = rpc.patch_get_mbox(patch_id)
435 if len(s) > 0:
436 print unicode(s).encode("utf-8")
438 proc = subprocess.Popen(['git', 'checkout', 'master'])
439 sts = os.waitpid(proc.pid, 0)
440 if sts[1] != 0:
441 sys.stderr.write("Failed to checkout master branch\n")
442 return
444 proc = subprocess.Popen(['git', 'merge', '--ff', 't/patch%s' % patch_id])
445 sts = os.waitpid(proc.pid, 0)
446 if sts[1] != 0:
447 sys.stderr.write("Failed to merge t/patch%s into master\n" % patch_id)
448 return
450 proc = subprocess.Popen(['git', 'rev-parse', 'master'],
451 stdout = subprocess.PIPE)
452 sha = proc.stdout.read()[:-1]
454 # If it succeeded this far, mark the patch as "Accepted" by the invoking
455 # user.
456 action_update_patch(rpc, patch_id, state = 'Accepted', commit = sha,
457 delegate_str = delegate_str, archived = True)
459 print sha
461 auth_actions = ['update', 'branch', 'merge']
463 def main():
464 try:
465 opts, args = getopt.getopt(sys.argv[2:], 's:p:w:d:n:c:h:')
466 except getopt.GetoptError, err:
467 print str(err)
468 usage()
470 if len(sys.argv) < 2:
471 usage()
473 action = sys.argv[1].lower()
475 # set defaults
476 filt = Filter()
477 submitter_str = ""
478 delegate_str = ""
479 project_str = ""
480 commit_str = ""
481 state_str = "New"
482 hash_str = ""
483 url = DEFAULT_URL
485 config = ConfigParser.ConfigParser()
486 config.read(CONFIG_FILES)
488 # grab settings from config files
489 if config.has_option('base', 'url'):
490 url = config.get('base', 'url')
492 if config.has_option('base', 'project'):
493 project_str = config.get('base', 'project')
495 for name, value in opts:
496 if name == '-s':
497 state_str = value
498 elif name == '-p':
499 project_str = value
500 elif name == '-w':
501 submitter_str = value
502 elif name == '-d':
503 delegate_str = value
504 elif name == '-c':
505 commit_str = value
506 elif name == '-h':
507 hash_str = value
508 elif name == '-n':
509 try:
510 filt.add("max_count", int(value))
511 except:
512 sys.stderr.write("Invalid maximum count '%s'\n" % value)
513 usage()
514 else:
515 sys.stderr.write("Unknown option '%s'\n" % name)
516 usage()
518 if len(args) > 1:
519 sys.stderr.write("Too many arguments specified\n")
520 usage()
522 (username, password) = (None, None)
523 transport = None
524 if action in auth_actions:
525 if config.has_option('auth', 'username') and \
526 config.has_option('auth', 'password'):
528 use_https = url.startswith('https')
530 transport = BasicHTTPAuthTransport( \
531 config.get('auth', 'username'),
532 config.get('auth', 'password'),
533 use_https)
535 else:
536 sys.stderr.write(("The %s action requires authentication, "
537 "but no username or password\nis configured\n") % action)
538 sys.exit(1)
540 if project_str:
541 filt.add("project", project_str)
543 if state_str:
544 filt.add("state", state_str)
546 try:
547 rpc = xmlrpclib.Server(url, transport = transport)
548 except:
549 sys.stderr.write("Unable to connect to %s\n" % url)
550 sys.exit(1)
552 patch_id = None
553 if hash_str:
554 patch_id = patch_id_from_hash(rpc, project_str, hash_str)
555 if patch_id is None:
556 sys.stderr.write("No patch has the hash provided\n")
557 sys.exit(1)
560 if action == 'list' or action == 'search':
561 if len(args) > 0:
562 filt.add("name__icontains", args[0])
563 action_list(rpc, filt, submitter_str, delegate_str)
565 elif action.startswith('project'):
566 action_projects(rpc)
568 elif action.startswith('state'):
569 action_states(rpc)
571 elif action == 'branch':
572 try:
573 patch_id = patch_id or int(args[0])
574 except:
575 sys.stderr.write("Invalid patch ID given\n")
576 sys.exit(1)
578 branch_with(patch_id, rpc, config.get('auth', 'username'))
580 elif action == 'merge':
581 try:
582 patch_id = patch_id or int(args[0])
583 except:
584 sys.stderr.write("Invalid patch ID given\n")
585 sys.exit(1)
587 merge_with(patch_id, rpc, config.get('auth', 'username'))
589 elif action == 'view':
590 try:
591 patch_id = patch_id or int(args[0])
592 except:
593 sys.stderr.write("Invalid patch ID given\n")
594 sys.exit(1)
596 s = rpc.patch_get_mbox(patch_id)
597 if len(s) > 0:
598 print unicode(s).encode("utf-8")
600 elif action == 'get' or action == 'save':
601 try:
602 patch_id = patch_id or int(args[0])
603 except:
604 sys.stderr.write("Invalid patch ID given\n")
605 sys.exit(1)
607 action_get(rpc, patch_id)
609 elif action == 'apply':
610 try:
611 patch_id = patch_id or int(args[0])
612 except:
613 sys.stderr.write("Invalid patch ID given\n")
614 sys.exit(1)
616 action_apply(rpc, patch_id)
618 elif action == 'update':
619 try:
620 patch_id = patch_id or int(args[0])
621 except:
622 sys.stderr.write("Invalid patch ID given\n")
623 sys.exit(1)
625 action_update_patch(rpc, patch_id, state = state_str,
626 commit = commit_str, delegate_str = delegate_str)
628 else:
629 sys.stderr.write("Unknown action '%s'\n" % action)
630 usage()
632 if __name__ == "__main__":
633 main()