wintest: s3 moved smb.conf to /etc
[kai/samba.git] / script / land.py
index 67564a18b7a829f4294d14f06978f383e1f0bdb6..72bdd4b8408cdd6093673c088485ec1e03180d88 100755 (executable)
@@ -4,18 +4,29 @@
 # Copyright Jelmer Vernooij 2010
 # released under GNU GPL v3 or later
 
+from cStringIO import StringIO
+import fcntl
 from subprocess import call, check_call, Popen, PIPE
 import os, tarfile, sys, time
 from optparse import OptionParser
 import smtplib
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../selftest"))
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../lib/testtools"))
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../lib/subunit/python"))
+import subunit
+import testtools
+import subunithelper
+import tempfile
+from email.mime.application import MIMEApplication
 from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
 
 samba_master = os.getenv('SAMBA_MASTER', 'git://git.samba.org/samba.git')
 samba_master_ssh = os.getenv('SAMBA_MASTER_SSH', 'git+ssh://git.samba.org/data/git/samba.git')
 
 cleanup_list = []
 
-os.putenv('CC', "ccache gcc")
+os.environ['CC'] = "ccache gcc"
 
 tasks = {
     "source3" : [ ("autogen", "./autogen.sh", "text/plain"),
@@ -23,14 +34,14 @@ tasks = {
                   ("make basics", "make basics", "text/plain"),
                   ("make", "make -j 4 everything", "text/plain"), # don't use too many processes
                   ("install", "make install", "text/plain"),
-                  ("test", "TDB_NO_FSYNC=1 make subunit-test FAIL_IMMEDIATELY=1", "text/x-subunit") ],
+                  ("test", "TDB_NO_FSYNC=1 make subunit-test", "text/x-subunit") ],
 
     "source4" : [ ("configure", "./configure.developer ${PREFIX}", "text/plain"),
                   ("make", "make -j", "text/plain"),
                   ("install", "make install", "text/plain"),
-                  ("test", "TDB_NO_FSYNC=1 make subunit-test FAIL_IMMEDIATELY=1", "text/x-subunit") ],
+                  ("test", "TDB_NO_FSYNC=1 make subunit-test", "text/x-subunit") ],
 
-    "source4/lib/ldb" : [ ("configure", "./configure --enable-developer -C ${PREFIX}", "text/plain"),
+    "lib/ldb" : [ ("configure", "./configure --enable-developer -C ${PREFIX}", "text/plain"),
                           ("make", "make -j", "text/plain"),
                           ("install", "make install", "text/plain"),
                           ("test", "make test", "text/plain") ],
@@ -45,7 +56,7 @@ tasks = {
                      ("configure", "./configure --enable-developer -C ${PREFIX}", "text/plain"),
                      ("make", "make -j", "text/plain"),
                      ("install", "make install", "text/plain"),
-                     ("test", "make test", "text/plain"), ],
+                     ("test", "make test", "text/x-subunit"), ],
 
     "lib/replace" : [ ("autogen", "./autogen-waf.sh", "text/plain"),
                       ("configure", "./configure --enable-developer -C ${PREFIX}", "text/plain"),
@@ -59,10 +70,33 @@ tasks = {
                      ("test", "make test", "text/plain"), ],
 }
 
-retry_task = [ ( "retry",
-                 '''set -e
-                git remote add -t master master %s
-                git fetch master
+
+def run_cmd(cmd, dir=None, show=None, output=False, checkfail=True, shell=False):
+    if show is None:
+        show = options.verbose
+    if show:
+        print("Running: '%s' in '%s'" % (cmd, dir))
+    if output:
+        return Popen(cmd, stdout=PIPE, cwd=dir, shell=shell).communicate()[0]
+    elif checkfail:
+        return check_call(cmd, cwd=dir, shell=shell)
+    else:
+        return call(cmd, cwd=dir, shell=shell)
+
+
+def clone_gitroot(test_master, revision="HEAD"):
+    run_cmd(["git", "clone", "--shared", gitroot, test_master])
+    if revision != "HEAD":
+        run_cmd(["git", "checkout", revision])
+
+
+class RetryChecker(object):
+    """Check whether it is necessary to retry."""
+
+    def __init__(self, dir):
+        run_cmd(["git", "remote", "add", "-t", "master", "master", samba_master])
+        run_cmd(["git", "fetch", "master"])
+        cmd = '''set -e
                 while :; do
                   sleep 60
                   git describe master/master > old_master.desc
@@ -70,91 +104,248 @@ retry_task = [ ( "retry",
                   git describe master/master > master.desc
                   diff old_master.desc master.desc
                 done
-               ''' % samba_master, "test/plain" ) ]
+               '''
+        self.proc = Popen(cmd, shell=True, cwd=self.dir)
 
+    def poll(self):
+        return self.proc.poll()
 
-def run_cmd(cmd, dir=".", show=None, output=False, checkfail=True):
-    if show is None:
-        show = options.verbose
-    if show:
-        print("Running: '%s' in '%s'" % (cmd, dir))
-    if output:
-        return Popen(cmd, stdout=PIPE, cwd=dir).communicate()[0]
-    elif checkfail:
-        return check_call(cmd, cwd=dir)
-    else:
-        return call(cmd, cwd=dir)
+    def kill(self):
+        self.proc.terminate()
+        self.proc.wait()
+        self.retry.proc = None
 
 
-class Builder(object):
-    '''handle build of one directory'''
+class TreeStageBuilder(object):
+    """Handle building of a particular stage for a tree.
+    """
 
-    def __init__(self, name, sequence):
+    def __init__(self, tree, name, command, fail_quickly=False):
+        self.tree = tree
         self.name = name
+        self.command = command
+        self.fail_quickly = fail_quickly
+        self.exitcode = None
+        self.stdin = open(os.devnull, 'r')
 
-        if name in ['pass', 'fail', 'retry']:
-            self.dir = "."
+    def start(self):
+        raise NotImplementedError(self.start)
+
+    def poll(self):
+        self.exitcode = self.proc.poll()
+        return self.exitcode
+
+    def kill(self):
+        if self.proc is not None:
+            try:
+                run_cmd(["killbysubdir", self.tree.sdir], checkfail=False)
+            except OSError:
+                # killbysubdir doesn't exist ?
+                pass
+            self.proc.terminate()
+            self.proc.wait()
+            self.proc = None
+
+    @property
+    def failure_reason(self):
+        raise NotImplementedError(self.failure_reason)
+
+    @property
+    def failed(self):
+        return (self.exitcode != 0)
+
+
+class PlainTreeStageBuilder(TreeStageBuilder):
+
+    def start(self):
+        print '%s: [%s] Running %s' % (self.name, self.name, self.command)
+        self.proc = Popen(self.command, shell=True, cwd=self.tree.dir,
+                          stdout=self.tree.stdout, stderr=self.tree.stderr,
+                          stdin=self.stdin)
+
+    @property
+    def failure_reason(self):
+        return "failed '%s' with exit code %d" % (self.command, self.exitcode)
+
+
+class AbortingTestResult(subunithelper.TestsuiteEnabledTestResult):
+
+    def __init__(self, stage):
+        super(AbortingTestResult, self).__init__()
+        self.stage = stage
+
+    def addError(self, test, details=None):
+        self.stage.proc.terminate()
+
+    def addFailure(self, test, details=None):
+        self.stage.proc.terminate()
+
+
+class FailureTrackingTestResult(subunithelper.TestsuiteEnabledTestResult):
+
+    def __init__(self, stage):
+        super(FailureTrackingTestResult, self).__init__()
+        self.stage = stage
+
+    def addError(self, test, details=None):
+        if self.stage.failed_test is None:
+            self.stage.failed_test = ("error", test)
+
+    def addFailure(self, test, details=None):
+        if self.stage.failed_test is None:
+            self.stage.failed_test = ("failure", test)
+
+
+class SubunitTreeStageBuilder(TreeStageBuilder):
+
+    def __init__(self, tree, name, command, fail_quickly=False):
+        super(SubunitTreeStageBuilder, self).__init__(tree, name, command,
+                fail_quickly)
+        self.failed_test = None
+        self.subunit_path = os.path.join(gitroot,
+            "%s.%s.subunit" % (self.tree.tag, self.name))
+        self.tree.logfiles.append(
+            (self.subunit_path, os.path.basename(self.subunit_path),
+             "text/x-subunit"))
+        self.subunit = open(self.subunit_path, 'w')
+
+        formatter = subunithelper.PlainFormatter(False, True, {})
+        clients = [formatter, subunit.TestProtocolClient(self.subunit),
+                   FailureTrackingTestResult(self)]
+        if fail_quickly:
+            clients.append(AbortingTestResult(self))
+        self.subunit_server = subunit.TestProtocolServer(
+            testtools.MultiTestResult(*clients),
+            self.subunit)
+        self.buffered = ""
+
+    def start(self):
+        print '%s: [%s] Running' % (self.tree.name, self.name)
+        self.proc = Popen(self.command, shell=True, cwd=self.tree.dir,
+            stdout=PIPE, stderr=self.tree.stderr, stdin=self.stdin)
+        fd = self.proc.stdout.fileno()
+        fl = fcntl.fcntl(fd, fcntl.F_GETFL)
+        fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
+
+    def poll(self):
+        try:
+            data = self.proc.stdout.read()
+        except IOError:
+            return None
+        else:
+            self.buffered += data
+            buffered = ""
+            for l in self.buffered.splitlines(True):
+                if l[-1] == "\n":
+                    self.subunit_server.lineReceived(l)
+                else:
+                    buffered += l
+            self.buffered = buffered
+            self.exitcode = self.proc.poll()
+            if self.exitcode is not None:
+                self.subunit.close()
+            return self.exitcode
+
+    @property
+    def failure_reason(self):
+        if self.failed_test:
+            return "failed '%s' with %s in test %s" (self.command, self.failed_test[0], self.failed_test[1])
         else:
-            self.dir = self.name
+            return "failed '%s' with exit code %d in unknown test" % (self.command, self.exitcode)
+
+
+class TreeBuilder(object):
+    '''handle build of one directory'''
+
+    def __init__(self, name, sequence, fail_quickly=False):
+        self.name = name
+        self.fail_quickly = fail_quickly
 
         self.tag = self.name.replace('/', '_')
         self.sequence = sequence
         self.next = 0
-        self.stdout_path = "%s/%s.stdout" % (gitroot, self.tag)
-        self.stderr_path = "%s/%s.stderr" % (gitroot, self.tag)
+        self.stages = []
+        self.stdout_path = os.path.join(gitroot, "%s.stdout" % (self.tag, ))
+        self.stderr_path = os.path.join(gitroot, "%s.stderr" % (self.tag, ))
+        self.logfiles = [
+            (self.stdout_path, os.path.basename(self.stdout_path), "text/plain"),
+            (self.stderr_path, os.path.basename(self.stderr_path), "text/plain"),
+            ]
         if options.verbose:
             print("stdout for %s in %s" % (self.name, self.stdout_path))
             print("stderr for %s in %s" % (self.name, self.stderr_path))
-        run_cmd("rm -f %s %s" % (self.stdout_path, self.stderr_path))
+        if os.path.exists(self.stdout_path):
+            os.unlink(self.stdout_path)
+        if os.path.exists(self.stderr_path):
+            os.unlink(self.stderr_path)
         self.stdout = open(self.stdout_path, 'w')
         self.stderr = open(self.stderr_path, 'w')
-        self.stdin  = open("/dev/null", 'r')
-        self.sdir = "%s/%s" % (testbase, self.tag)
-        self.prefix = "%s/prefix/%s" % (testbase, self.tag)
-        run_cmd("rm -rf %s" % self.sdir)
+        self.sdir = os.path.join(testbase, self.tag)
+        if name in ['pass', 'fail', 'retry']:
+            self.dir = self.sdir
+        else:
+            self.dir = os.path.join(self.sdir, self.name)
+        self.prefix = os.path.join(testbase, "prefix", self.tag)
+        run_cmd(["rm", "-rf", self.sdir])
         cleanup_list.append(self.sdir)
         cleanup_list.append(self.prefix)
         os.makedirs(self.sdir)
-        run_cmd("rm -rf %s" % self.sdir)
-        run_cmd(["git", "clone", "--shared", gitroot, self.sdir])
+        run_cmd(["rm",  "-rf", self.sdir])
+        clone_gitroot(self.sdir, revision)
         self.start_next()
+        self.exitcode = None
 
     def start_next(self):
         if self.next == len(self.sequence):
             print '%s: Completed OK' % self.name
             self.done = True
+            self.stdout.close()
+            self.stderr.close()
             return
-        (self.stage, self.cmd, self.output_mime_type) = self.sequence[self.next]
-        self.cmd = self.cmd.replace("${PREFIX}", "--prefix=%s" % self.prefix)
-        if self.output_mime_type == "text/x-subunit":
-            self.cmd += " | %s --immediate" % (os.path.join(os.path.dirname(__file__), "selftest/format-subunit"))
-        print '%s: [%s] Running %s' % (self.name, self.stage, self.cmd)
-        self.proc = Popen(self.cmd, shell=True, cwd="%s/%s" % (self.sdir, self.dir),
-                          stdout=self.stdout, stderr=self.stderr, stdin=self.stdin)
+        (stage_name, cmd, output_mime_type) = self.sequence[self.next]
+        cmd = cmd.replace("${PREFIX}", "--prefix=%s" % self.prefix)
+        if output_mime_type == "text/plain":
+            self.current_stage = PlainTreeStageBuilder(self, stage_name, cmd,
+                self.fail_quickly)
+        elif output_mime_type == "text/x-subunit":
+            self.current_stage = SubunitTreeStageBuilder(self, stage_name, cmd,
+                self.fail_quickly)
+        else:
+            raise Exception("Unknown output mime type %s" % output_mime_type)
+        self.stages.append(self.current_stage)
+        self.current_stage.start()
         self.next += 1
 
+    def remove_logs(self):
+        for path, name, mime_type in self.logfiles:
+            os.unlink(path)
+
     def poll(self):
-        self.status = self.proc.poll()
-        return self.status
+        self.exitcode = self.current_stage.poll()
+        if self.exitcode is not None:
+            self.current_stage = None
+        return self.exitcode
 
     def kill(self):
-        if self.proc is not None:
-            try:
-                run_cmd(["killbysubdir", self.sdir], checkfail=False)
-            except OSError:
-                # killbysubdir doesn't exist ?
-                pass
-            self.proc.terminate()
-            self.proc.wait()
-            self.proc = None
+        if self.current_stage is not None:
+            self.current_stage.kill()
+            self.current_stage = None
 
     @property
     def failed(self):
-        return (os.WIFSIGNALED(self.status) or os.WEXITSTATUS(self.status) != 0)
+        return any([s.failed for s in self.stages])
+
+    @property
+    def failed_stage(self):
+        for s in self.stages:
+            if s.failed:
+                return s
+        return s
 
     @property
     def failure_reason(self):
-        return "%s: [%s] failed '%s' with status %d" % (self.name, self.stage, self.cmd, self.status)
+        return "%s: [%s] %s" % (self.name, self.failed_stage.name,
+            self.failed_stage.failure_reason)
 
 
 class BuildList(object):
@@ -172,10 +363,10 @@ class BuildList(object):
         if tasknames == []:
             tasknames = tasklist
         for n in tasknames:
-            b = Builder(n, tasks[n])
+            b = TreeBuilder(n, tasks[n], not options.fail_slowly)
             self.tlist.append(b)
         if options.retry:
-            self.retry = Builder('retry', retry_task)
+            self.retry = RetryChecker(self.sdir)
             self.need_retry = False
 
     def kill_kids(self):
@@ -184,9 +375,7 @@ class BuildList(object):
             self.tail_proc.wait()
             self.tail_proc = None
         if self.retry is not None:
-            self.retry.proc.terminate()
-            self.retry.proc.wait()
-            self.retry = None
+            self.retry.kill()
         for b in self.tlist:
             b.kill()
 
@@ -194,16 +383,15 @@ class BuildList(object):
         while True:
             none_running = True
             for b in self.tlist:
-                if b.proc is None:
+                if b.current_stage is None:
                     continue
                 none_running = False
                 if b.poll() is None:
                     continue
-                b.proc = None
                 return b
             if options.retry:
-                ret = self.retry.proc.poll()
-                if ret is not None:
+                ret = self.retry.poll()
+                if ret:
                     self.need_retry = True
                     self.retry = None
                     return None
@@ -217,27 +405,36 @@ class BuildList(object):
             if options.retry and self.need_retry:
                 self.kill_kids()
                 print("retry needed")
-                return (0, "retry")
+                return (0, None, None, None, "retry")
             if b is None:
                 break
             if b.failed:
                 self.kill_kids()
-                return (b.status, b.name, b.stage, b.tag, b.failure_reason)
+                return (b.exitcode, b.name, b.failed_stage, b.tag, b.failure_reason)
             b.start_next()
         self.kill_kids()
         return (0, None, None, None, "All OK")
 
-    def tarlogs(self, fname):
-        tar = tarfile.open(fname, "w:gz")
+    def tarlogs(self, name=None, fileobj=None):
+        tar = tarfile.open(name=name, fileobj=fileobj, mode="w:gz")
         for b in self.tlist:
-            tar.add(b.stdout_path, arcname="%s.stdout" % b.tag)
-            tar.add(b.stderr_path, arcname="%s.stderr" % b.tag)
+            for (path, name, mime_type) in b.logfiles:
+                tar.add(path, arcname=name)
+        if os.path.exists("autobuild.log"):
+            tar.add("autobuild.log")
         tar.close()
 
+    def attach_logs(self, outer):
+        f = StringIO()
+        self.tarlogs(fileobj=f)
+        msg = MIMEApplication(f.getvalue(), "x-gzip")
+        msg.add_header('Content-Disposition', 'attachment',
+                       filename="logs.tar.gz")
+        outer.attach(msg)
+
     def remove_logs(self):
         for b in self.tlist:
-            os.unlink(b.stdout_path)
-            os.unlink(b.stderr_path)
+            b.remove_logs()
 
     def start_tail(self):
         cmd = "tail -f *.stdout *.stderr"
@@ -249,7 +446,7 @@ def cleanup():
         return
     print("Cleaning up ....")
     for d in cleanup_list:
-        run_cmd("rm -rf %s" % d)
+        run_cmd(["rm", "-rf", d])
 
 
 def find_git_root(p):
@@ -287,13 +484,16 @@ def daemonize(logfile):
 
 def rebase_tree(url):
     print("Rebasing on %s" % url)
-    run_cmd("git remote add -t master master %s" % url, show=True, dir=test_master)
-    run_cmd("git fetch master", show=True, dir=test_master)
+    run_cmd(["git", "remote", "add", "-t", "master", "master", url], show=True,
+            dir=test_master)
+    run_cmd(["git", "fetch", "master"], show=True, dir=test_master)
     if options.fix_whitespace:
-        run_cmd("git rebase --whitespace=fix master/master", show=True, dir=test_master)
+        run_cmd(["git", "rebase", "--whitespace=fix", "master/master"],
+                show=True, dir=test_master)
     else:
-        run_cmd("git rebase master/master", show=True, dir=test_master)
-    diff = run_cmd("git --no-pager diff HEAD master/master", dir=test_master, output=True)
+        run_cmd(["git", "rebase", "master/master"], show=True, dir=test_master)
+    diff = run_cmd(["git", "--no-pager", "diff", "HEAD", "master/master"],
+        dir=test_master, output=True)
     if diff == '':
         print("No differences between HEAD and master/master - exiting")
         sys.exit(0)
@@ -301,47 +501,60 @@ def rebase_tree(url):
 def push_to(url):
     print("Pushing to %s" % url)
     if options.mark:
-        run_cmd("EDITOR=script/commit_mark.sh git commit --amend -c HEAD", dir=test_master)
-        # the notes method doesn't work yet, as metze hasn't allowed refs/notes/* in master
-        # run_cmd("EDITOR=script/commit_mark.sh git notes edit HEAD", dir=test_master)
-    run_cmd("git remote add -t master pushto %s" % url, show=True, dir=test_master)
-    run_cmd("git push pushto +HEAD:master", show=True, dir=test_master)
-
-def_testbase = os.getenv("AUTOBUILD_TESTBASE", "/memdisk/%s" % os.getenv('USER'))
+        run_cmd("EDITOR=script/commit_mark.sh git commit --amend -c HEAD",
+            dir=test_master, shell=True)
+        # the notes method doesn't work yet, as metze hasn't allowed
+        # refs/notes/* in master
+        # run_cmd("EDITOR=script/commit_mark.sh git notes edit HEAD",
+        #     dir=test_master)
+    run_cmd(["git", "remote", "add", "-t", "master", "pushto", url], show=True,
+        dir=test_master)
+    run_cmd(["git", "push", "pushto", "+HEAD:master"], show=True,
+        dir=test_master)
+
+def_testbase = os.getenv("AUTOBUILD_TESTBASE")
+if def_testbase is None:
+    if os.path.exists("/memdisk"):
+        def_testbase = "/memdisk/%s" % os.getenv('USER')
+    else:
+        def_testbase = os.path.join(tempfile.gettempdir(), "autobuild-%s" % os.getenv("USER"))
 
 parser = OptionParser()
-parser.add_option("", "--repository", help="repository to run tests for", default=None, type=str)
-parser.add_option("", "--tail", help="show output while running", default=False, action="store_true")
-parser.add_option("", "--keeplogs", help="keep logs", default=False, action="store_true")
-parser.add_option("", "--nocleanup", help="don't remove test tree", default=False, action="store_true")
-parser.add_option("", "--testbase", help="base directory to run tests in (default %s)" % def_testbase,
+parser.add_option("--repository", help="repository to run tests for", default=None, type=str)
+parser.add_option("--revision", help="revision to compile if not HEAD", default=None, type=str)
+parser.add_option("--tail", help="show output while running", default=False, action="store_true")
+parser.add_option("--keeplogs", help="keep logs", default=False, action="store_true")
+parser.add_option("--nocleanup", help="don't remove test tree", default=False, action="store_true")
+parser.add_option("--testbase", help="base directory to run tests in (default %s)" % def_testbase,
                   default=def_testbase)
-parser.add_option("", "--passcmd", help="command to run on success", default=None)
-parser.add_option("", "--verbose", help="show all commands as they are run",
+parser.add_option("--passcmd", help="command to run on success", default=None)
+parser.add_option("--verbose", help="show all commands as they are run",
                   default=False, action="store_true")
-parser.add_option("", "--rebase", help="rebase on the given tree before testing",
+parser.add_option("--rebase", help="rebase on the given tree before testing",
                   default=None, type='str')
-parser.add_option("", "--rebase-master", help="rebase on %s before testing" % samba_master,
+parser.add_option("--rebase-master", help="rebase on %s before testing" % samba_master,
                   default=False, action='store_true')
-parser.add_option("", "--pushto", help="push to a git url on success",
+parser.add_option("--pushto", help="push to a git url on success",
                   default=None, type='str')
-parser.add_option("", "--push-master", help="push to %s on success" % samba_master_ssh,
+parser.add_option("--push-master", help="push to %s on success" % samba_master_ssh,
                   default=False, action='store_true')
-parser.add_option("", "--mark", help="add a Tested-By signoff before pushing",
+parser.add_option("--mark", help="add a Tested-By signoff before pushing",
                   default=False, action="store_true")
-parser.add_option("", "--fix-whitespace", help="fix whitespace on rebase",
+parser.add_option("--fix-whitespace", help="fix whitespace on rebase",
                   default=False, action="store_true")
-parser.add_option("", "--retry", help="automatically retry if master changes",
+parser.add_option("--retry", help="automatically retry if master changes",
                   default=False, action="store_true")
-parser.add_option("", "--email", help="send email to the given address on failure",
+parser.add_option("--email", help="send email to the given address on failure",
                   type='str', default=None)
-parser.add_option("", "--always-email", help="always send email, even on success",
+parser.add_option("--always-email", help="always send email, even on success",
                   action="store_true")
-parser.add_option("", "--daemon", help="daemonize after initial setup",
+parser.add_option("--daemon", help="daemonize after initial setup",
+                  action="store_true")
+parser.add_option("--fail-slowly", help="continue running tests even after one has already failed",
                   action="store_true")
 
 
-def email_failure(status, failed_task, failed_stage, failed_tag, errstr):
+def email_failure(blist, exitcode, failed_task, failed_stage, failed_tag, errstr):
     '''send an email to options.email about the failure'''
     user = os.getenv("USER")
     text = '''
@@ -357,22 +570,38 @@ You can see logs of the failed task here:
   http://git.samba.org/%s/samba-autobuild/%s.stdout
   http://git.samba.org/%s/samba-autobuild/%s.stderr
 
+A summary of the autobuild process is here:
+
+  http://git.samba.org/%s/samba-autobuild/autobuild.log
+
 or you can get full logs of all tasks in this job here:
 
   http://git.samba.org/%s/samba-autobuild/logs.tar.gz
 
-''' % (failed_task, errstr, user, failed_tag, user, failed_tag, user)
-    msg = MIMEText(text)
-    msg['Subject'] = 'autobuild failure for task %s during %s' % (failed_task, failed_stage)
+The top commit for the tree that was built was:
+
+%s
+
+''' % (failed_task, errstr, user, failed_tag, user, failed_tag, user, user,
+       get_top_commit_msg(test_master))
+
+    msg = MIMEMultipart()
+    msg['Subject'] = 'autobuild failure for task %s during %s' % (
+        failed_task, failed_stage.name)
     msg['From'] = 'autobuild@samba.org'
     msg['To'] = options.email
 
+    main = MIMEText(text)
+    msg.attach(main)
+
+    blist.attach_logs(msg)
+
     s = smtplib.SMTP()
     s.connect()
     s.sendmail(msg['From'], [msg['To']], msg.as_string())
     s.quit()
 
-def email_success():
+def email_success(blist):
     '''send an email to options.email about a successful build'''
     user = os.getenv("USER")
     text = '''
@@ -389,12 +618,24 @@ you can get full logs of all tasks in this job here:
 
   http://git.samba.org/%s/samba-autobuild/logs.tar.gz
 
-''' % (user,)
-    msg = MIMEText(text)
+''' % user
+
+    text += '''
+The top commit for the tree that was built was:
+
+%s
+''' % (get_top_commit_msg(test_master),)
+
+    msg = MIMEMultipart()
     msg['Subject'] = 'autobuild success'
     msg['From'] = 'autobuild@samba.org'
     msg['To'] = options.email
 
+    main = MIMEText(text, 'plain')
+    msg.attach(main)
+
+    blist.attach_logs(msg)
+
     s = smtplib.SMTP()
     s.connect()
     s.sendmail(msg['From'], [msg['To']], msg.as_string())
@@ -407,7 +648,7 @@ if options.retry:
     if not options.rebase_master and options.rebase is None:
         raise Exception('You can only use --retry if you also rebase')
 
-testbase = "%s/b%u" % (options.testbase, os.getpid())
+testbase = os.path.join(options.testbase, "b%u" % (os.getpid(),))
 test_master = os.path.join(testbase, "master")
 
 if options.repository is not None:
@@ -419,6 +660,15 @@ gitroot = find_git_root(repository)
 if gitroot is None:
     raise Exception("Failed to find git root under %s" % repository)
 
+# get the top commit message, for emails
+if options.revision is not None:
+    revision = options.revision
+else:
+    revision = "HEAD"
+
+def get_top_commit_msg(reporoot):
+    return run_cmd(["git", "log", "-1"], dir=reporoot, output=True)
+
 try:
     os.makedirs(testbase)
 except Exception, reason:
@@ -432,9 +682,9 @@ if options.daemon:
 
 while True:
     try:
-        run_cmd("rm -rf %s" % test_master)
+        run_cmd(["rm", "-rf", test_master])
         cleanup_list.append(test_master)
-        run_cmd("git clone --shared %s %s" % (gitroot, test_master))
+        clone_gitroot(test_master, revision)
     except:
         cleanup()
         raise
@@ -447,8 +697,8 @@ while True:
         blist = BuildList(tasks, args)
         if options.tail:
             blist.start_tail()
-        (status, failed_task, failed_stage, failed_tag, errstr) = blist.run()
-        if status != 0 or errstr != "retry":
+        (exitcode, failed_task, failed_stage, failed_tag, errstr) = blist.run()
+        if exitcode != 0 or errstr != "retry":
             break
         cleanup()
     except:
@@ -460,11 +710,11 @@ if options.tail:
     print("waiting for tail to flush")
     time.sleep(1)
 
-if status == 0:
+if exitcode == 0:
     print errstr
     if options.passcmd is not None:
         print("Running passcmd: %s" % options.passcmd)
-        run_cmd(options.passcmd, dir=test_master)
+        run_cmd(options.passcmd, dir=test_master, shell=True)
     if options.pushto is not None:
         push_to(options.pushto)
     elif options.push_master:
@@ -473,7 +723,7 @@ if status == 0:
         blist.tarlogs("logs.tar.gz")
         print("Logs in logs.tar.gz")
     if options.always_email:
-        email_success()
+        email_success(blist)
     blist.remove_logs()
     cleanup()
     print(errstr)
@@ -482,9 +732,10 @@ else:
     blist.tarlogs("logs.tar.gz")
 
     if options.email is not None:
-        email_failure(status, failed_task, failed_stage, failed_tag, errstr)
+        email_failure(blist, exitcode, failed_task, failed_stage, failed_tag,
+            errstr)
 
     cleanup()
     print(errstr)
     print("Logs in logs.tar.gz")
-sys.exit(status)
+sys.exit(exitcode)