# # Copyright (c) 2009 Martin Decky # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # - Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # - Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # - The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # """Send emails for commits and repository changes.""" # # Inspired by bzr-email plugin (copyright (c) 2005 - 2007 Canonical Ltd., # distributed under GPL), but no code is shared with the original plugin. # # Configuration options: # - post_commit_to (destination email address for the commit emails) # - post_commit_sender (source email address for the commit emails) # import smtplib import time from StringIO import StringIO from email.utils import parseaddr from email.utils import formatdate from email.utils import make_msgid from email.Header import Header from email.Message import Message from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from bzrlib import errors from bzrlib import revision from bzrlib import __version__ as bzrlib_version from bzrlib.branch import Branch from bzrlib.diff import DiffTree def send_smtp(server, sender, to, subject, body): """Send SMTP message""" connection = smtplib.SMTP() try: connection.connect(server) except socket.error, err: raise errors.SocketConnectionError(host = server, msg = "Unable to connect to SMTP server", orig_error = err) sender_user, sender_email = parseaddr(sender) payload = MIMEText(body.encode("utf-8"), "plain", "utf-8") msg = MIMEMultipart() msg["From"] = "%s <%s>" % (Header(unicode(sender_user)), sender_email) msg["User-Agent"] = "bzr/%s" % bzrlib_version msg["Date"] = formatdate(None, True) msg["Message-Id"] = make_msgid("bzr") msg["To"] = to msg["Subject"] = Header(subject) msg.attach(payload) connection.sendmail(sender, [to], msg.as_string()) def config_to(config): """Address the mail should go to""" return config.get_user_option("post_commit_to") def config_sender(config): """Address the email should be sent from""" result = config.get_user_option("post_commit_sender") if (result is None): result = config.username() return result def merge_marker(revision): if (len(revision.parent_ids) > 1): return " [merge]" return "" def revision_sequence(branch, revision_old_id, revision_new_id): """Calculate a sequence of revisions""" for revision_ac_id in branch.repository.iter_reverse_revision_history(revision_new_id): if (revision_ac_id == revision_old_id): break yield revision_ac_id def send_email(branch, revision_old_id, revision_new_id, config): """Send the email""" if (config_to(config) is not None): branch.lock_read() branch.repository.lock_read() try: body = StringIO() for revision_ac_id in revision_sequence(branch, revision_old_id, revision_new_id): revision_ac = branch.repository.get_revision(revision_ac_id) revision_ac_no = branch.revision_id_to_revno(revision_ac_id) committer = revision_ac.committer authors = revision_ac.get_apparent_authors() date = time.strftime("%Y-%m-%d %H:%M:%S %Z (%a, %d %b %Y)", time.localtime(revision_ac.timestamp)) if (authors != [committer]): body.write("Author: %s\n" % ", ".join(authors)) body.write("Committer: %s\n" % committer) body.write("Date: %s\n" % date) body.write("New Revision: %s%s\n" % (revision_ac_no, merge_marker(revision_ac))) body.write("New Id: %s\n" % revision_ac_id) body.write("\n") for parent_id in revision_ac.parent_ids: body.write("Parent: %s\n" % parent_id) tree_old = branch.repository.revision_tree(parent_id) tree_ac = branch.repository.revision_tree(revision_ac_id) delta = tree_ac.changes_from(tree_old) if (len(delta.added) > 0): body.write("Added:\n") for item in delta.added: body.write(" %s\n" % item[0]) if (len(delta.removed) > 0): body.write("Removed:\n") for item in delta.removed: body.write(" %s\n" % item[0]) if (len(delta.renamed) > 0): body.write("Renamed:\n") for item in delta.renamed: body.write(" %s -> %s\n" % (item[0], item[1])) if (len(delta.kind_changed) > 0): body.write("Changed:\n") for item in delta.kind_changed: body.write(" %s\n" % item[0]) if (len(delta.modified) > 0): body.write("Modified:\n") for item in delta.modified: body.write(" %s\n" % item[0]) body.write("\n") body.write("Log:\n") if (not revision_ac.message): body.write("(empty)\n") else: log = revision_ac.message.rstrip("\n\r") for line in log.split("\n"): body.write("%s\n" % line) body.write("\n") tree_old = branch.repository.revision_tree(revision_old_id) tree_new = branch.repository.revision_tree(revision_new_id) tree_old.lock_read() try: tree_new.lock_read() try: diff = DiffTree.from_trees_options(tree_old, tree_new, body, "utf8", None, "", "", None) diff.show_diff(None, None) finally: tree_new.unlock() finally: tree_old.unlock() revision_new_no = branch.revision_id_to_revno(revision_new_id) delta = tree_new.changes_from(tree_old) files = [] for item in delta.added: files.append(item[0]) for item in delta.removed: files.append(item[0]) for item in delta.renamed: files.append(item[0]) for item in delta.kind_changed: files.append(item[0]) for item in delta.modified: files.append(item[0]) subject = "r%d - %s" % (revision_new_no, " ".join(files)) send_smtp("localhost", config_sender(config), config_to(config), subject, body.getvalue()) finally: branch.repository.unlock() branch.unlock() def branch_post_change_hook(params): """post_change_branch_tip hook""" send_email(params.branch, params.old_revid, params.new_revid, params.branch.get_config()) install_named_hook = getattr(Branch.hooks, "install_named_hook", None) install_named_hook("post_change_branch_tip", branch_post_change_hook, "bzreml")