1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
|
#!/usr/bin/env python
#
# Copyright (C) Nathaniel Smith <njs@pobox.com>
# Licensed under the MIT license:
# http://www.opensource.org/licenses/mit-license.html
# I.e., do what you like, but keep copyright and there's NO WARRANTY.
#
# CIA bot client script for Monotone repositories, written in python. This
# generates commit messages using CIA's XML commit format, and can deliver
# them using either XML-RPC or email. Based on the script 'ciabot_svn.py' by
# Micah Dowty <micah@navi.cx>.
# This script is normally run from a cron job. It periodically does a 'pull'
# from a given server, finds new revisions, filters them for "interesting"
# ones, and reports them to CIA.
# It needs a working directory, where it will store the database and some
# state of its own.
# To use:
# -- make a copy of it somewhere
# -- edit the configuration values below
# -- set up a cron job to run every ten minutes (or whatever), running the
# command "ciabot_monotone.py <path to scratch dir>". The scratch dir is
# used to store state between runs. It will be automatically created,
# but do not delete it.
class config:
def project_for_branch(self, branchname):
# Customize this to return your project name(s). If changes to the
# given branch are uninteresting -- i.e., changes to them should be
# ignored entirely -- then return the python constant None (which is
# distinct from the string "None", a valid but poor project name!).
#if branchname.startswith("net.venge.monotone-viz"):
# return "monotone-viz"
#elif branchname.startswith("net.venge.monotone.contrib.monotree"):
# return "monotree"
#else:
# return "monotone"
return "FIXME"
# Add entries of the form ("server address", "pattern") to get
# this script to watch the given collections at the given monotone
# servers.
watch_list = [
#("monotone.ca", "net.venge.monotone"),
]
# If this is non-None, then the web interface will make any file 'foo' a
# link to 'repository_uri/foo'.
repository_uri = None
# The server to deliver XML-RPC messages to, if using XML-RPC delivery.
xmlrpc_server = "http://cia.navi.cx"
# The email address to deliver messages to, if using email delivery.
smtp_address = "cia@cia.navi.cx"
# The SMTP server to connect to, if using email delivery.
smtp_server = "localhost"
# The 'from' address to put on email, if using email delivery.
from_address = "cia-user@FIXME"
# Set to one of "xmlrpc", "email", "debug".
delivery = "debug"
# Path to monotone executable.
monotone_exec = "monotone"
################################################################################
import sys, os, os.path
class Monotone:
def __init__(self, bin, db):
self.bin = bin
self.db = db
def _run_monotone(self, args):
args_str = " ".join(args)
# Yay lack of quoting
fd = os.popen("%s --db=%s --quiet %s" % (self.bin, self.db, args_str))
output = fd.read()
if fd.close():
sys.exit("monotone exited with error")
return output
def _split_revs(self, output):
if output:
return output.strip().split("\n")
else:
return []
def get_interface_version(self):
iv_str = self._run_monotone(["automate", "interface_version"])
return tuple(map(int, iv_str.strip().split(".")))
def db_init(self):
self._run_monotone(["db", "init"])
def db_migrate(self):
self._run_monotone(["db", "migrate"])
def ensure_db_exists(self):
if not os.path.exists(self.db):
self.db_init()
def pull(self, server, collection):
self._run_monotone(["pull", server, collection])
def leaves(self):
return self._split_revs(self._run_monotone(["automate", "leaves"]))
def ancestry_difference(self, new_rev, old_revs):
args = ["automate", "ancestry_difference", new_rev] + old_revs
return self._split_revs(self._run_monotone(args))
def log(self, rev, xlast=None):
if xlast is not None:
last_arg = ["--last=%i" % (xlast,)]
else:
last_arg = []
return self._run_monotone(["log", "-r", rev] + last_arg)
def toposort(self, revs):
args = ["automate", "toposort"] + revs
return self._split_revs(self._run_monotone(args))
def get_revision(self, rid):
return self._run_monotone(["automate", "get_revision", rid])
class LeafFile:
def __init__(self, path):
self.path = path
def get_leaves(self):
if os.path.exists(self.path):
f = open(self.path, "r")
lines = []
for line in f:
lines.append(line.strip())
return lines
else:
return []
def set_leaves(self, leaves):
f = open(self.path + ".new", "w")
for leaf in leaves:
f.write(leaf + "\n")
f.close()
os.rename(self.path + ".new", self.path)
def escape_for_xml(text, is_attrib=0):
text = text.replace("&", "&")
text = text.replace("<", "<")
text = text.replace(">", ">")
if is_attrib:
text = text.replace("'", "'")
text = text.replace("\"", """)
return text
def send_message(message, c):
if c.delivery == "debug":
print message
elif c.delivery == "xmlrpc":
import xmlrpclib
xmlrpclib.ServerProxy(c.xmlrpc_server).hub.deliver(message)
elif c.delivery == "email":
import smtplib
smtp = smtplib.SMTP(c.smtp_server)
smtp.sendmail(c.from_address, c.smtp_address,
"From: %s\r\nTo: %s\r\n"
"Subject: DeliverXML\r\n\r\n%s"
% (c.from_address, c.smtp_address, message))
else:
sys.exit("delivery option must be one of 'debug', 'xmlrpc', 'email'")
def send_change_for(rid, m, c):
message_tmpl = """<message>
<generator>
<name>Monotone CIA Bot client python script</name>
<version>0.1</version>
</generator>
<source>
<project>%(project)s</project>
<branch>%(branch)s</branch>
</source>
<body>
<commit>
<revision>%(rid)s</revision>
<author>%(author)s</author>
<files>%(files)s</files>
<log>%(log)s</log>
</commit>
</body>
</message>"""
substs = {}
log = m.log(rid, 1)
rev = m.get_revision(rid)
# Stupid way to pull out everything inside quotes (which currently
# uniquely identifies filenames inside a changeset).
pieces = rev.split('"')
files = []
for i in range(len(pieces)):
if (i % 2) == 1:
if pieces[i] not in files:
files.append(pieces[i])
substs["files"] = "\n".join(["<file>%s</file>" % escape_for_xml(f) for f in files])
branch = None
author = None
changelog_pieces = []
started_changelog = 0
pieces = log.split("\n")
for p in pieces:
if p.startswith("Author:"):
author = p.split(None, 1)[1].strip()
if p.startswith("Branch:"):
branch = p.split()[1]
if p.startswith("ChangeLog:"):
started_changelog = 1
elif started_changelog:
changelog_pieces.append(p)
changelog = "\n".join(changelog_pieces).strip()
if branch is None:
return
project = c.project_for_branch(branch)
if project is None:
return
substs["author"] = escape_for_xml(author or "(unknown author)")
substs["project"] = escape_for_xml(project)
substs["branch"] = escape_for_xml(branch)
substs["rid"] = escape_for_xml(rid)
substs["log"] = escape_for_xml(changelog)
message = message_tmpl % substs
send_message(message, c)
def send_changes_between(old_leaves, new_leaves, m, c):
if not old_leaves:
# Special case for initial setup -- don't push thousands of old
# revisions down CIA's throat!
return
new_revs = {}
for leaf in new_leaves:
if leaf in old_leaves:
continue
for new_rev in m.ancestry_difference(leaf, old_leaves):
new_revs[new_rev] = None
new_revs_sorted = m.toposort(new_revs.keys())
for new_rev in new_revs_sorted:
send_change_for(new_rev, m, c)
def main(progname, args):
if len(args) != 1:
sys.exit("Usage: %s STATE-DIR" % (progname,))
(state_dir,) = args
if not os.path.isdir(state_dir):
os.makedirs(state_dir)
lockfile = os.path.join(state_dir, "lock")
# Small race condition, oh well.
if os.path.exists(lockfile):
sys.exit("script already running, exiting")
try:
open(lockfile, "w").close()
c = config()
m = Monotone(c.monotone_exec, os.path.join(state_dir, "database.db"))
m.ensure_db_exists()
m.db_migrate()
for server, collection in c.watch_list:
m.pull(server, collection)
lf = LeafFile(os.path.join(state_dir, "leaves"))
new_leaves = m.leaves()
send_changes_between(lf.get_leaves(), new_leaves, m, c)
lf.set_leaves(new_leaves)
finally:
os.unlink(lockfile)
if __name__ == "__main__":
main(sys.argv[0], sys.argv[1:])
|