monotone

monotone Mtn Source Tree

Root/contrib/ciabot_monotone.py

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

Archive Download this file

Branches

Tags

Quick Links:     www.monotone.ca    -     Downloads    -     Documentation    -     Wiki    -     Code Forge    -     Build Status