bug in org-mode-export-as-latex
[org-mode.git] / UTILITIES / pw
blob3f52dd1f242e946b9335c8910ddab30f5e39831f
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 'rejected': 'emacs-orgmode@gnu.org',
41 'changes requested': 'emacs-orgmode@gnu.org',
42 'rfc': 'emacs-orgmode@gnu.org'
45 # Default Patchwork remote XML-RPC server URL
46 # This script will check the PW_XMLRPC_URL environment variable
47 # for the URL to access. If that is unspecified, it will fallback to
48 # the hardcoded default value specified here.
49 DEFAULT_URL = "http://patchwork.newartisans.com/xmlrpc/"
50 CONFIG_FILES = [os.path.expanduser('~/.pwclientrc')]
52 class Filter:
53 """Filter for selecting patches."""
54 def __init__(self):
55 # These fields refer to specific objects, so they are special
56 # because we have to resolve them to IDs before passing the
57 # filter to the server
58 self.state = ""
59 self.project = ""
61 # The dictionary that gets passed to via XML-RPC
62 self.d = {}
64 def add(self, field, value):
65 if field == 'state':
66 self.state = value
67 elif field == 'project':
68 self.project = value
69 else:
70 # OK to add directly
71 self.d[field] = value
73 def resolve_ids(self, rpc):
74 """Resolve State, Project, and Person IDs based on filter strings."""
75 if self.state != "":
76 id = state_id_by_name(rpc, self.state)
77 if id == 0:
78 sys.stderr.write("Note: No State found matching %s*, " \
79 "ignoring filter\n" % self.state)
80 else:
81 self.d['state_id'] = id
83 if self.project != "":
84 id = project_id_by_name(rpc, self.project)
85 if id == 0:
86 sys.stderr.write("Note: No Project found matching %s, " \
87 "ignoring filter\n" % self.project)
88 else:
89 self.d['project_id'] = id
91 def __str__(self):
92 """Return human-readable description of the filter."""
93 return str(self.d)
95 class BasicHTTPAuthTransport(xmlrpclib.SafeTransport):
97 def __init__(self, username = None, password = None, use_https = False):
98 self.username = username
99 self.password = password
100 self.use_https = use_https
101 xmlrpclib.SafeTransport.__init__(self)
103 def authenticated(self):
104 return self.username != None and self.password != None
106 def send_host(self, connection, host):
107 xmlrpclib.Transport.send_host(self, connection, host)
108 if not self.authenticated():
109 return
110 credentials = '%s:%s' % (self.username, self.password)
111 auth = 'Basic ' + base64.encodestring(credentials).strip()
112 connection.putheader('Authorization', auth)
114 def make_connection(self, host):
115 if self.use_https:
116 fn = xmlrpclib.SafeTransport.make_connection
117 else:
118 fn = xmlrpclib.Transport.make_connection
119 return fn(self, host)
121 def usage():
122 sys.stderr.write("Usage: %s <action> [options]\n\n" % \
123 (os.path.basename(sys.argv[0])))
124 sys.stderr.write("Where <action> is one of:\n")
125 sys.stderr.write(
126 """ apply <ID> : Apply a patch (in the current dir, using -p1)
127 get <ID> : Download a patch and save it locally
128 projects : List all projects
129 states : Show list of potential patch states
130 list [str] : List patches, using the optional filters specified
131 below and an optional substring to search for patches
132 by name
133 search [str] : Same as 'list'
134 view <ID> : View a patch
135 show <ID> : Same as view
136 update [-s state] [-c commit-ref] [-m email-comment] <ID>
137 : Update patch, send mail to mailing list if appropriate
138 branch <ID> : Create a topic branch t/patchNNN for this patch
139 : and update it to the current master
140 merge [-m email-comment] <ID>
141 : Merge the t/patchNNN topic branch into master
142 \n""")
143 sys.stderr.write("""\nFilter options for 'list' and 'search':
144 -s <state> : Filter by patch state (e.g., 'New', 'Accepted', etc.)
145 -p <project> : Filter by project name (see 'projects' for list)
146 -w <who> : Filter by submitter (name, e-mail substring search)
147 -d <who> : Filter by delegate (name, e-mail substring search)
148 -n <max #> : Restrict number of results\n""")
149 sys.stderr.write("""\nActions that take an ID argument can also be \
150 invoked with:
151 -h <hash> : Lookup by patch hash\n""")
152 sys.exit(1)
154 def project_id_by_name(rpc, linkname):
155 """Given a project short name, look up the Project ID."""
156 if len(linkname) == 0:
157 return 0
158 projects = rpc.project_list(linkname, 0)
159 for project in projects:
160 if project['linkname'] == linkname:
161 return project['id']
162 return 0
164 def state_id_by_name(rpc, name):
165 """Given a partial state name, look up the state ID."""
166 if len(name) == 0:
167 return 0
168 states = rpc.state_list(name, 0)
169 for state in states:
170 if state['name'].lower().startswith(name.lower()):
171 return state['id']
172 return 0
174 def person_ids_by_name(rpc, name):
175 """Given a partial name or email address, return a list of the
176 person IDs that match."""
177 if len(name) == 0:
178 return []
179 people = rpc.person_list(name, 0)
180 return map(lambda x: x['id'], people)
182 def list_patches(patches):
183 """Dump a list of patches to stdout."""
184 print("%-5s %-12s %s" % ("ID", "State", "Name"))
185 print("%-5s %-12s %s" % ("--", "-----", "----"))
186 for patch in patches:
187 print("%-5d %-12s %s" % (patch['id'], patch['state'], patch['name']))
189 def action_list(rpc, filter, submitter_str, delegate_str):
190 filter.resolve_ids(rpc)
192 if submitter_str != "":
193 ids = person_ids_by_name(rpc, submitter_str)
194 if len(ids) == 0:
195 sys.stderr.write("Note: Nobody found matching *%s*\n", \
196 submitter_str)
197 else:
198 for id in ids:
199 person = rpc.person_get(id)
200 print "Patches submitted by %s <%s>:" % \
201 (person['name'], person['email'])
202 f = filter
203 f.add("submitter_id", id)
204 patches = rpc.patch_list(f.d)
205 list_patches(patches)
206 return
208 if delegate_str != "":
209 ids = person_ids_by_name(rpc, delegate_str)
210 if len(ids) == 0:
211 sys.stderr.write("Note: Nobody found matching *%s*\n", \
212 delegate_str)
213 else:
214 for id in ids:
215 person = rpc.person_get(id)
216 print "Patches delegated to %s <%s>:" % \
217 (person['name'], person['email'])
218 f = filter
219 f.add("delegate_id", id)
220 patches = rpc.patch_list(f.d)
221 list_patches(patches)
222 return
224 patches = rpc.patch_list(filter.d)
225 list_patches(patches)
227 def action_projects(rpc):
228 projects = rpc.project_list("", 0)
229 print("%-5s %-24s %s" % ("ID", "Name", "Description"))
230 print("%-5s %-24s %s" % ("--", "----", "-----------"))
231 for project in projects:
232 print("%-5d %-24s %s" % (project['id'], \
233 project['linkname'], \
234 project['name']))
236 def action_states(rpc):
237 states = rpc.state_list("", 0)
238 print("%-5s %s" % ("ID", "Name"))
239 print("%-5s %s" % ("--", "----"))
240 for state in states:
241 print("%-5d %s" % (state['id'], state['name']))
243 def action_get(rpc, patch_id):
244 patch = rpc.patch_get(patch_id)
245 s = rpc.patch_get_mbox(patch_id)
247 if patch == {} or len(s) == 0:
248 sys.stderr.write("Unable to get patch %d\n" % patch_id)
249 sys.exit(1)
251 base_fname = fname = os.path.basename(patch['filename'])
252 i = 0
253 while os.path.exists(fname):
254 fname = "%s.%d" % (base_fname, i)
255 i += 1
257 try:
258 f = open(fname, "w")
259 except:
260 sys.stderr.write("Unable to open %s for writing\n" % fname)
261 sys.exit(1)
263 try:
264 f.write(unicode(s).encode("utf-8"))
265 f.close()
266 print "Saved patch to %s" % fname
267 except:
268 sys.stderr.write("Failed to write to %s\n" % fname)
269 sys.exit(1)
271 def action_apply(rpc, patch_id):
272 patch = rpc.patch_get(patch_id)
273 if patch == {}:
274 sys.stderr.write("Error getting information on patch ID %d\n" % \
275 patch_id)
276 sys.exit(1)
277 print "Applying patch #%d to current directory" % patch_id
278 print "Description: %s" % patch['name']
279 s = rpc.patch_get_mbox(patch_id)
280 if len(s) > 0:
281 proc = subprocess.Popen(['patch', '-p1'], stdin = subprocess.PIPE)
282 proc.communicate(s)
283 else:
284 sys.stderr.write("Error: No patch content found\n")
285 sys.exit(1)
287 def action_update_patch(rpc, patch_id, state = None, commit = None,
288 delegate_str = "", comment_str = "No comment", archived = False):
289 patch = rpc.patch_get(patch_id)
290 if patch == {}:
291 sys.stderr.write("Error getting information on patch ID %d\n" % \
292 patch_id)
293 sys.exit(1)
295 params = {}
297 delegate_id = None
298 if delegate_str != "":
299 params['delegate'] = delegate_str
300 ids = person_ids_by_name(rpc, delegate_str)
301 if len(ids) == 0:
302 sys.stderr.write("Note: Nobody found matching *%s*\n"% \
303 delegate_str)
304 else:
305 delegate_id = ids[0]
307 if state:
308 state_id = state_id_by_name(rpc, state)
309 if state_id == 0:
310 sys.stderr.write("Error: No State found matching %s*\n" % state)
311 sys.exit(1)
312 params['state'] = state_id
314 if state.lower() in notify_on_state_change:
315 if not delegate_id:
316 sys.stderr.write("Error: Delete (-d) required for this update\n")
317 sys.exit(1)
319 person = rpc.person_get(delegate_id)
320 submitter = rpc.person_get(patch['submitter_id'])
322 from_addr = '%s <%s>' % (person['name'], person['email'])
323 cc_addr = '%s <%s>' % (submitter['name'], submitter['email'])
324 to_addr = notify_on_state_change[state]
326 orig_mail = rpc.patch_get_mbox(patch_id)
327 orig_mail = '> ' + orig_mail.replace('\n','\n> ')
329 longdesc = '''Patch %s (http://patchwork.newartisans.com/patch/%s/) is now "%s".
331 Maintaner comment: %s
333 This relates to the following submission:
335 http://mid.gmane.org/%s
337 Here is the original message containing the patch:
339 %s''' % \
340 (patch['id'], patch['id'], state, comment_str, urllib.quote(patch['msgid']), orig_mail)
341 shortdesc = '[%s] %s' % (state, patch['name'])
343 msg = MIMEText(longdesc)
345 msg['Subject'] = shortdesc
346 msg['From'] = from_addr
347 msg['To'] = to_addr
348 #msg['Cc'] = cc_addr
349 msg['References'] = patch['msgid']
351 # Send the message via our own SMTP server, but don't include
352 # the envelope header.
353 try:
354 s = smtplib.SMTP('localhost')
355 print "Sending e-mail to: %s, %s" % (to_addr, cc_addr)
356 s.sendmail(from_addr, [to_addr, cc_addr], msg.as_string())
357 s.quit()
358 except:
359 sys.stderr.write("Warning: Failed to send e-mail " +
360 "(no SMTP server listening at localhost?)\n")
362 if commit:
363 params['commit_ref'] = commit
365 if archived:
366 params['archived'] = archived
368 success = False
369 try:
370 print "Updating patch %d to state '%s', delegate %s" % \
371 (patch_id, state, delegate_str)
372 success = rpc.patch_set(patch_id, params)
373 except xmlrpclib.Fault, f:
374 sys.stderr.write("Error updating patch: %s\n" % f.faultString)
376 if not success:
377 sys.stderr.write("Patch not updated\n")
379 def patch_id_from_hash(rpc, project, hash):
380 try:
381 patch = rpc.patch_get_by_project_hash(project, hash)
382 except xmlrpclib.Fault:
383 # the server may not have the newer patch_get_by_project_hash function,
384 # so fall back to hash-only.
385 patch = rpc.patch_get_by_hash(hash)
387 if patch == {}:
388 return None
390 return patch['id']
392 def branch_with(patch_id, rpc, delegate_str):
393 s = rpc.patch_get_mbox(patch_id)
394 if len(s) > 0:
395 print unicode(s).encode("utf-8")
397 patch = rpc.patch_get(patch_id)
399 # Find the latest commit from the day before the patch
400 proc = subprocess.Popen(['git', 'log', '--until=' + patch['date'],
401 '-1', '--format=%H', 'master'],
402 stdout = subprocess.PIPE)
403 sha = proc.stdout.read()[:-1]
405 # Create a topic branch named after this commit
406 proc = subprocess.Popen(['git', 'checkout', '-b', 't/patch%s' %
407 patch_id, sha])
408 sts = os.waitpid(proc.pid, 0)
409 if sts[1] != 0:
410 sys.stderr.write("Could not create branch for patch\n")
411 return
413 # Apply the patch to the branch
414 fname = '/tmp/patch'
415 try:
416 f = open(fname, "w")
417 except:
418 sys.stderr.write("Unable to open %s for writing\n" % fname)
419 sys.exit(1)
421 try:
422 f.write(unicode(s).encode("utf-8"))
423 f.close()
424 print "Saved patch to %s" % fname
425 except:
426 sys.stderr.write("Failed to write to %s\n" % fname)
427 sys.exit(1)
429 proc = subprocess.Popen(['git', 'am', '/tmp/patch'])
430 sts = os.waitpid(proc.pid, 0)
431 if sts[1] != 0:
432 sys.stderr.write("Failed to apply patch to branch\n")
433 proc = subprocess.Popen(['git', 'checkout', 'master'])
434 os.waitpid(proc.pid, 0)
435 proc = subprocess.Popen(['git', 'branch', '-D', 't/patch%s' %
436 patch_id, sha])
437 os.waitpid(proc.pid, 0)
438 return
440 # If it succeeded this far, mark the patch as "Under Review" by the
441 # invoking user.
442 action_update_patch(rpc, patch_id, state = 'Under Review',
443 delegate_str = delegate_str)
445 proc = subprocess.Popen(['git', 'rebase', 'master'])
446 sts = os.waitpid(proc.pid, 0)
448 print sha
450 def merge_with(patch_id, rpc, delegate_str, comment_str):
451 s = rpc.patch_get_mbox(patch_id)
452 if len(s) > 0:
453 print unicode(s).encode("utf-8")
455 proc = subprocess.Popen(['git', 'checkout', 'master'])
456 sts = os.waitpid(proc.pid, 0)
457 if sts[1] != 0:
458 sys.stderr.write("Failed to checkout master branch\n")
459 return
461 proc = subprocess.Popen(['git', 'merge', '--ff', 't/patch%s' % patch_id])
462 sts = os.waitpid(proc.pid, 0)
463 if sts[1] != 0:
464 sys.stderr.write("Failed to merge t/patch%s into master\n" % patch_id)
465 return
467 proc = subprocess.Popen(['git', 'rev-parse', 'master'],
468 stdout = subprocess.PIPE)
469 sha = proc.stdout.read()[:-1]
471 # If it succeeded this far, mark the patch as "Accepted" by the invoking
472 # user.
473 action_update_patch(rpc, patch_id, state = 'Accepted', commit = sha,
474 delegate_str = delegate_str, archived = True)
476 print sha
478 auth_actions = ['update', 'branch', 'merge']
480 def main():
481 try:
482 opts, args = getopt.getopt(sys.argv[2:], 's:p:w:d:n:c:h:m:')
483 except getopt.GetoptError, err:
484 print str(err)
485 usage()
487 if len(sys.argv) < 2:
488 usage()
490 action = sys.argv[1].lower()
492 # set defaults
493 filt = Filter()
494 submitter_str = ""
495 delegate_str = ""
496 project_str = ""
497 commit_str = ""
498 state_str = "New"
499 hash_str = ""
500 url = DEFAULT_URL
502 config = ConfigParser.ConfigParser()
503 config.read(CONFIG_FILES)
505 # grab settings from config files
506 if config.has_option('base', 'url'):
507 url = config.get('base', 'url')
509 if config.has_option('base', 'project'):
510 project_str = config.get('base', 'project')
512 for name, value in opts:
513 if name == '-s':
514 state_str = value
515 elif name == '-p':
516 project_str = value
517 elif name == '-w':
518 submitter_str = value
519 elif name == '-d':
520 delegate_str = value
521 elif name == '-c':
522 commit_str = value
523 elif name == '-h':
524 hash_str = value
525 elif name == '-m':
526 comment_str = value
527 elif name == '-n':
528 try:
529 filt.add("max_count", int(value))
530 except:
531 sys.stderr.write("Invalid maximum count '%s'\n" % value)
532 usage()
533 else:
534 sys.stderr.write("Unknown option '%s'\n" % name)
535 usage()
537 if len(args) > 1:
538 sys.stderr.write("Too many arguments specified\n")
539 usage()
541 (username, password) = (None, None)
542 transport = None
543 if action in auth_actions:
544 if config.has_option('auth', 'username') and \
545 config.has_option('auth', 'password'):
547 use_https = url.startswith('https')
549 transport = BasicHTTPAuthTransport( \
550 config.get('auth', 'username'),
551 config.get('auth', 'password'),
552 use_https)
554 else:
555 sys.stderr.write(("The %s action requires authentication, "
556 "but no username or password\nis configured\n") % action)
557 sys.exit(1)
559 if project_str:
560 filt.add("project", project_str)
562 if state_str:
563 filt.add("state", state_str)
565 try:
566 rpc = xmlrpclib.Server(url, transport = transport)
567 except:
568 sys.stderr.write("Unable to connect to %s\n" % url)
569 sys.exit(1)
571 patch_id = None
572 if hash_str:
573 patch_id = patch_id_from_hash(rpc, project_str, hash_str)
574 if patch_id is None:
575 sys.stderr.write("No patch has the hash provided\n")
576 sys.exit(1)
579 if action == 'list' or action == 'search':
580 if len(args) > 0:
581 filt.add("name__icontains", args[0])
582 action_list(rpc, filt, submitter_str, delegate_str)
584 elif action.startswith('project'):
585 action_projects(rpc)
587 elif action.startswith('state'):
588 action_states(rpc)
590 elif action == 'branch':
591 try:
592 patch_id = patch_id or int(args[0])
593 except:
594 sys.stderr.write("Invalid patch ID given\n")
595 sys.exit(1)
597 branch_with(patch_id, rpc, config.get('auth', 'username'))
599 elif action == 'merge':
600 try:
601 patch_id = patch_id or int(args[0])
602 except:
603 sys.stderr.write("Invalid patch ID given\n")
604 sys.exit(1)
606 merge_with(patch_id, rpc, config.get('auth', 'username'), comment_str)
608 elif action == 'test':
610 patch_id = patch_id or int(args[0])
611 patch = rpc.patch_get(patch_id)
612 s = rpc.patch_get_mbox(patch_id)
613 print "xxx %s xxx"
614 print "xxx %s xxx" % patch['name']
615 print "xxx %s xxx" % rpc.patch_get_mbox(patch_id)
617 elif action == 'view' or action == 'show':
618 try:
619 patch_id = patch_id or int(args[0])
620 except:
621 sys.stderr.write("Invalid patch ID given\n")
622 sys.exit(1)
624 s = rpc.patch_get_mbox(patch_id)
625 if len(s) > 0:
626 print unicode(s).encode("utf-8")
628 elif action == 'get' or action == 'save':
629 try:
630 patch_id = patch_id or int(args[0])
631 except:
632 sys.stderr.write("Invalid patch ID given\n")
633 sys.exit(1)
635 action_get(rpc, patch_id)
637 elif action == 'apply':
638 try:
639 patch_id = patch_id or int(args[0])
640 except:
641 sys.stderr.write("Invalid patch ID given\n")
642 sys.exit(1)
644 action_apply(rpc, patch_id)
646 elif action == 'update':
647 try:
648 patch_id = patch_id or int(args[0])
649 except:
650 sys.stderr.write("Invalid patch ID given\n")
651 sys.exit(1)
653 action_update_patch(rpc, patch_id, state = state_str,
654 commit = commit_str, delegate_str = delegate_str, comment_str = comment_str)
656 else:
657 sys.stderr.write("Unknown action '%s'\n" % action)
658 usage()
660 if __name__ == "__main__":
661 main()