Fixed typo in ob-css.el
[org-mode.git] / UTILITIES / pw
blob5a2e3dd3540538b97923217c0815be672e12c4be
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 show <ID> : Same as view
134 update [-s state] [-c commit-ref] <ID>
135 : Update patch\n""")
136 sys.stderr.write("""\nFilter options for 'list' and 'search':
137 -s <state> : Filter by patch state (e.g., 'New', 'Accepted', etc.)
138 -p <project> : Filter by project name (see 'projects' for list)
139 -w <who> : Filter by submitter (name, e-mail substring search)
140 -d <who> : Filter by delegate (name, e-mail substring search)
141 -n <max #> : Restrict number of results\n""")
142 sys.stderr.write("""\nActions that take an ID argument can also be \
143 invoked with:
144 -h <hash> : Lookup by patch hash\n""")
145 sys.exit(1)
147 def project_id_by_name(rpc, linkname):
148 """Given a project short name, look up the Project ID."""
149 if len(linkname) == 0:
150 return 0
151 projects = rpc.project_list(linkname, 0)
152 for project in projects:
153 if project['linkname'] == linkname:
154 return project['id']
155 return 0
157 def state_id_by_name(rpc, name):
158 """Given a partial state name, look up the state ID."""
159 if len(name) == 0:
160 return 0
161 states = rpc.state_list(name, 0)
162 for state in states:
163 if state['name'].lower().startswith(name.lower()):
164 return state['id']
165 return 0
167 def person_ids_by_name(rpc, name):
168 """Given a partial name or email address, return a list of the
169 person IDs that match."""
170 if len(name) == 0:
171 return []
172 people = rpc.person_list(name, 0)
173 return map(lambda x: x['id'], people)
175 def list_patches(patches):
176 """Dump a list of patches to stdout."""
177 print("%-5s %-12s %s" % ("ID", "State", "Name"))
178 print("%-5s %-12s %s" % ("--", "-----", "----"))
179 for patch in patches:
180 print("%-5d %-12s %s" % (patch['id'], patch['state'], patch['name']))
182 def action_list(rpc, filter, submitter_str, delegate_str):
183 filter.resolve_ids(rpc)
185 if submitter_str != "":
186 ids = person_ids_by_name(rpc, submitter_str)
187 if len(ids) == 0:
188 sys.stderr.write("Note: Nobody found matching *%s*\n", \
189 submitter_str)
190 else:
191 for id in ids:
192 person = rpc.person_get(id)
193 print "Patches submitted by %s <%s>:" % \
194 (person['name'], person['email'])
195 f = filter
196 f.add("submitter_id", id)
197 patches = rpc.patch_list(f.d)
198 list_patches(patches)
199 return
201 if delegate_str != "":
202 ids = person_ids_by_name(rpc, delegate_str)
203 if len(ids) == 0:
204 sys.stderr.write("Note: Nobody found matching *%s*\n", \
205 delegate_str)
206 else:
207 for id in ids:
208 person = rpc.person_get(id)
209 print "Patches delegated to %s <%s>:" % \
210 (person['name'], person['email'])
211 f = filter
212 f.add("delegate_id", id)
213 patches = rpc.patch_list(f.d)
214 list_patches(patches)
215 return
217 patches = rpc.patch_list(filter.d)
218 list_patches(patches)
220 def action_projects(rpc):
221 projects = rpc.project_list("", 0)
222 print("%-5s %-24s %s" % ("ID", "Name", "Description"))
223 print("%-5s %-24s %s" % ("--", "----", "-----------"))
224 for project in projects:
225 print("%-5d %-24s %s" % (project['id'], \
226 project['linkname'], \
227 project['name']))
229 def action_states(rpc):
230 states = rpc.state_list("", 0)
231 print("%-5s %s" % ("ID", "Name"))
232 print("%-5s %s" % ("--", "----"))
233 for state in states:
234 print("%-5d %s" % (state['id'], state['name']))
236 def action_get(rpc, patch_id):
237 patch = rpc.patch_get(patch_id)
238 s = rpc.patch_get_mbox(patch_id)
240 if patch == {} or len(s) == 0:
241 sys.stderr.write("Unable to get patch %d\n" % patch_id)
242 sys.exit(1)
244 base_fname = fname = os.path.basename(patch['filename'])
245 i = 0
246 while os.path.exists(fname):
247 fname = "%s.%d" % (base_fname, i)
248 i += 1
250 try:
251 f = open(fname, "w")
252 except:
253 sys.stderr.write("Unable to open %s for writing\n" % fname)
254 sys.exit(1)
256 try:
257 f.write(unicode(s).encode("utf-8"))
258 f.close()
259 print "Saved patch to %s" % fname
260 except:
261 sys.stderr.write("Failed to write to %s\n" % fname)
262 sys.exit(1)
264 def action_apply(rpc, patch_id):
265 patch = rpc.patch_get(patch_id)
266 if patch == {}:
267 sys.stderr.write("Error getting information on patch ID %d\n" % \
268 patch_id)
269 sys.exit(1)
270 print "Applying patch #%d to current directory" % patch_id
271 print "Description: %s" % patch['name']
272 s = rpc.patch_get_mbox(patch_id)
273 if len(s) > 0:
274 proc = subprocess.Popen(['patch', '-p1'], stdin = subprocess.PIPE)
275 proc.communicate(s)
276 else:
277 sys.stderr.write("Error: No patch content found\n")
278 sys.exit(1)
280 def action_update_patch(rpc, patch_id, state = None, commit = None,
281 delegate_str = "", archived = False):
282 patch = rpc.patch_get(patch_id)
283 if patch == {}:
284 sys.stderr.write("Error getting information on patch ID %d\n" % \
285 patch_id)
286 sys.exit(1)
288 params = {}
290 delegate_id = None
291 if delegate_str != "":
292 params['delegate'] = delegate_str
293 ids = person_ids_by_name(rpc, delegate_str)
294 if len(ids) == 0:
295 sys.stderr.write("Note: Nobody found matching *%s*\n"% \
296 delegate_str)
297 else:
298 delegate_id = ids[0]
300 if state:
301 state_id = state_id_by_name(rpc, state)
302 if state_id == 0:
303 sys.stderr.write("Error: No State found matching %s*\n" % state)
304 sys.exit(1)
305 params['state'] = state_id
307 if state in notify_on_state_change:
308 if not delegate_id:
309 sys.stderr.write("Error: Delete (-d) required for this update\n")
310 sys.exit(1)
312 person = rpc.person_get(delegate_id)
313 submitter = rpc.person_get(patch['submitter_id'])
315 from_addr = '%s <%s>' % (person['name'], person['email'])
316 cc_addr = '%s <%s>' % (submitter['name'], submitter['email'])
317 to_addr = notify_on_state_change[state]
319 longdesc = '''Patch %s (http://patchwork.newartisans.com/patch/%s/) is now %s.
321 This relates to the following submission:
323 http://mid.gmane.org/%s''' % \
324 (patch['id'], patch['id'], state, urllib.quote(patch['msgid']))
325 shortdesc = 'Patch %s %s' % (patch['id'], state)
327 msg = MIMEText(longdesc)
329 msg['Subject'] = 'Patchwork: ' + shortdesc
330 msg['From'] = from_addr
331 msg['To'] = to_addr
332 #msg['Cc'] = cc_addr
333 msg['References'] = patch['msgid']
335 # Send the message via our own SMTP server, but don't include
336 # the envelope header.
337 try:
338 s = smtplib.SMTP('localhost')
339 print "Sending e-mail to: %s, %s" % (to_addr, cc_addr)
340 s.sendmail(from_addr, [to_addr, cc_addr], msg.as_string())
341 s.quit()
342 except:
343 sys.stderr.write("Warning: Failed to send e-mail " +
344 "(no SMTP server listening at localhost?)\n")
346 if commit:
347 params['commit_ref'] = commit
349 if archived:
350 params['archived'] = archived
352 success = False
353 try:
354 print "Updating patch %d to state '%s', delegate %s" % \
355 (patch_id, state, delegate_str)
356 success = rpc.patch_set(patch_id, params)
357 except xmlrpclib.Fault, f:
358 sys.stderr.write("Error updating patch: %s\n" % f.faultString)
360 if not success:
361 sys.stderr.write("Patch not updated\n")
363 def patch_id_from_hash(rpc, project, hash):
364 try:
365 patch = rpc.patch_get_by_project_hash(project, hash)
366 except xmlrpclib.Fault:
367 # the server may not have the newer patch_get_by_project_hash function,
368 # so fall back to hash-only.
369 patch = rpc.patch_get_by_hash(hash)
371 if patch == {}:
372 return None
374 return patch['id']
376 def branch_with(patch_id, rpc, delegate_str):
377 s = rpc.patch_get_mbox(patch_id)
378 if len(s) > 0:
379 print unicode(s).encode("utf-8")
381 patch = rpc.patch_get(patch_id)
383 # Find the latest commit from the day before the patch
384 proc = subprocess.Popen(['git', 'log', '--until=' + patch['date'],
385 '-1', '--format=%H', 'master'],
386 stdout = subprocess.PIPE)
387 sha = proc.stdout.read()[:-1]
389 # Create a topic branch named after this commit
390 proc = subprocess.Popen(['git', 'checkout', '-b', 't/patch%s' %
391 patch_id, sha])
392 sts = os.waitpid(proc.pid, 0)
393 if sts[1] != 0:
394 sys.stderr.write("Could not create branch for patch\n")
395 return
397 # Apply the patch to the branch
398 fname = '/tmp/patch'
399 try:
400 f = open(fname, "w")
401 except:
402 sys.stderr.write("Unable to open %s for writing\n" % fname)
403 sys.exit(1)
405 try:
406 f.write(unicode(s).encode("utf-8"))
407 f.close()
408 print "Saved patch to %s" % fname
409 except:
410 sys.stderr.write("Failed to write to %s\n" % fname)
411 sys.exit(1)
413 proc = subprocess.Popen(['git', 'am', '/tmp/patch'])
414 sts = os.waitpid(proc.pid, 0)
415 if sts[1] != 0:
416 sys.stderr.write("Failed to apply patch to branch\n")
417 proc = subprocess.Popen(['git', 'checkout', 'master'])
418 os.waitpid(proc.pid, 0)
419 proc = subprocess.Popen(['git', 'branch', '-D', 't/patch%s' %
420 patch_id, sha])
421 os.waitpid(proc.pid, 0)
422 return
424 # If it succeeded this far, mark the patch as "Under Review" by the
425 # invoking user.
426 action_update_patch(rpc, patch_id, state = 'Under Review',
427 delegate_str = delegate_str)
429 proc = subprocess.Popen(['git', 'rebase', 'master'])
430 sts = os.waitpid(proc.pid, 0)
432 print sha
434 def merge_with(patch_id, rpc, delegate_str):
435 s = rpc.patch_get_mbox(patch_id)
436 if len(s) > 0:
437 print unicode(s).encode("utf-8")
439 proc = subprocess.Popen(['git', 'checkout', 'master'])
440 sts = os.waitpid(proc.pid, 0)
441 if sts[1] != 0:
442 sys.stderr.write("Failed to checkout master branch\n")
443 return
445 proc = subprocess.Popen(['git', 'merge', '--ff', 't/patch%s' % patch_id])
446 sts = os.waitpid(proc.pid, 0)
447 if sts[1] != 0:
448 sys.stderr.write("Failed to merge t/patch%s into master\n" % patch_id)
449 return
451 proc = subprocess.Popen(['git', 'rev-parse', 'master'],
452 stdout = subprocess.PIPE)
453 sha = proc.stdout.read()[:-1]
455 # If it succeeded this far, mark the patch as "Accepted" by the invoking
456 # user.
457 action_update_patch(rpc, patch_id, state = 'Accepted', commit = sha,
458 delegate_str = delegate_str, archived = True)
460 print sha
462 auth_actions = ['update', 'branch', 'merge']
464 def main():
465 try:
466 opts, args = getopt.getopt(sys.argv[2:], 's:p:w:d:n:c:h:')
467 except getopt.GetoptError, err:
468 print str(err)
469 usage()
471 if len(sys.argv) < 2:
472 usage()
474 action = sys.argv[1].lower()
476 # set defaults
477 filt = Filter()
478 submitter_str = ""
479 delegate_str = ""
480 project_str = ""
481 commit_str = ""
482 state_str = "New"
483 hash_str = ""
484 url = DEFAULT_URL
486 config = ConfigParser.ConfigParser()
487 config.read(CONFIG_FILES)
489 # grab settings from config files
490 if config.has_option('base', 'url'):
491 url = config.get('base', 'url')
493 if config.has_option('base', 'project'):
494 project_str = config.get('base', 'project')
496 for name, value in opts:
497 if name == '-s':
498 state_str = value
499 elif name == '-p':
500 project_str = value
501 elif name == '-w':
502 submitter_str = value
503 elif name == '-d':
504 delegate_str = value
505 elif name == '-c':
506 commit_str = value
507 elif name == '-h':
508 hash_str = value
509 elif name == '-n':
510 try:
511 filt.add("max_count", int(value))
512 except:
513 sys.stderr.write("Invalid maximum count '%s'\n" % value)
514 usage()
515 else:
516 sys.stderr.write("Unknown option '%s'\n" % name)
517 usage()
519 if len(args) > 1:
520 sys.stderr.write("Too many arguments specified\n")
521 usage()
523 (username, password) = (None, None)
524 transport = None
525 if action in auth_actions:
526 if config.has_option('auth', 'username') and \
527 config.has_option('auth', 'password'):
529 use_https = url.startswith('https')
531 transport = BasicHTTPAuthTransport( \
532 config.get('auth', 'username'),
533 config.get('auth', 'password'),
534 use_https)
536 else:
537 sys.stderr.write(("The %s action requires authentication, "
538 "but no username or password\nis configured\n") % action)
539 sys.exit(1)
541 if project_str:
542 filt.add("project", project_str)
544 if state_str:
545 filt.add("state", state_str)
547 try:
548 rpc = xmlrpclib.Server(url, transport = transport)
549 except:
550 sys.stderr.write("Unable to connect to %s\n" % url)
551 sys.exit(1)
553 patch_id = None
554 if hash_str:
555 patch_id = patch_id_from_hash(rpc, project_str, hash_str)
556 if patch_id is None:
557 sys.stderr.write("No patch has the hash provided\n")
558 sys.exit(1)
561 if action == 'list' or action == 'search':
562 if len(args) > 0:
563 filt.add("name__icontains", args[0])
564 action_list(rpc, filt, submitter_str, delegate_str)
566 elif action.startswith('project'):
567 action_projects(rpc)
569 elif action.startswith('state'):
570 action_states(rpc)
572 elif action == 'branch':
573 try:
574 patch_id = patch_id or int(args[0])
575 except:
576 sys.stderr.write("Invalid patch ID given\n")
577 sys.exit(1)
579 branch_with(patch_id, rpc, config.get('auth', 'username'))
581 elif action == 'merge':
582 try:
583 patch_id = patch_id or int(args[0])
584 except:
585 sys.stderr.write("Invalid patch ID given\n")
586 sys.exit(1)
588 merge_with(patch_id, rpc, config.get('auth', 'username'))
590 elif action == 'view' or action == 'show':
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 s = rpc.patch_get_mbox(patch_id)
598 if len(s) > 0:
599 print unicode(s).encode("utf-8")
601 elif action == 'get' or action == 'save':
602 try:
603 patch_id = patch_id or int(args[0])
604 except:
605 sys.stderr.write("Invalid patch ID given\n")
606 sys.exit(1)
608 action_get(rpc, patch_id)
610 elif action == 'apply':
611 try:
612 patch_id = patch_id or int(args[0])
613 except:
614 sys.stderr.write("Invalid patch ID given\n")
615 sys.exit(1)
617 action_apply(rpc, patch_id)
619 elif action == 'update':
620 try:
621 patch_id = patch_id or int(args[0])
622 except:
623 sys.stderr.write("Invalid patch ID given\n")
624 sys.exit(1)
626 action_update_patch(rpc, patch_id, state = state_str,
627 commit = commit_str, delegate_str = delegate_str)
629 else:
630 sys.stderr.write("Unknown action '%s'\n" % action)
631 usage()
633 if __name__ == "__main__":
634 main()