#! /usr/bin/env python import os import socket import sys import subprocess import tempfile import time Name = "git-notifier" Version = "0.2" CacheFile = ".%s.dat" % Name Separator = "\n>---------------------------------------------------------------\n" NoDiff = "[nodiff]" NoMail = "[nomail]" class State: def __init__(self): self.clear() def clear(self): self.heads = {} self.tags = {} self.revs = {} def writeTo(self, file): out = open(file, "w") for (head, ref) in self.heads.items(): print >>out, "head", head, ref for (tag, ref) in self.tags.items(): print >>out, "tag", tag, ref for (rev, ref) in self.revs.items(): print >>out, "rev", rev, ",".join(ref) def readFrom(self, file): self.clear() for line in open(file): line = line.strip() if not line or line.startswith("#"): continue m = line.split() if len(m) >= 3: (type, key, val) = (m[0], m[1], m[2]) else: (type, key, val) = (m[0], m[1], '') if type == "head": self.heads[key] = val elif type == "tag": self.tags[key] = val elif type == "rev": self.revs[key] = val.split(",") else: error("unknown type %s in cache file" % type) class GitConfig: def __init__(self, args): whoami = os.environ["LOGNAME"] sender = os.environ["GL_USER"] if "GL_USER" in os.environ else whoami self.hostname = self._git_config("hooks.hostname", socket.gethostname()) self.sender = self._git_config("hooks.sender", sender) self.emailprefix = self._git_config("hooks.emailprefix", "[git]") self.mailinglist = self._git_config("hooks.mailinglist", whoami) self.users = self._git_config("hooks.users", None) self.log = self._git_config("log", "%s.log" % Name) self.noupdate = False self.updateonly = False self.debug = False self.maxdiffsize = int(self._git_config("hooks.maxdiffsize", 50)) self.maxdiffsize *= 1024 repo = "ssh://%s/%s" % (self.hostname, os.getcwd()) if repo.endswith(".git"): repo = repo[0:-4] self.repo = self._git_config("repository_uri", repo) self.parseArgs(args) if not self.debug: self.log = open(self.log, "a") else: self.log = sys.stderr if not self.users and "GL_LOG" in os.environ: users = os.path.join(os.path.dirname(os.environ["GL_LOG"]), "../conf/sender.cfg") if os.path.exists(users): self.users = users self.readUsers() def parseArgs(self, args): def noVal(key, i): if i < len(args) and args[i] == "--%s" % key: self.__dict__[key] = True i += 1 return i def withVal(key, i): if i < len(args) and args[i] == "--%s" % key: if i == len(args) - 1: error("command line argument %s missing value" % args[i]) self.__dict__[key] = args[i+1] i += 2 return i i = 0 while i < len(args): j = i arg = args[i] i = withVal("sender", i) i = withVal("emailprefix", i) i = withVal("mailinglist", i) i = withVal("hostname", i) i = withVal("users", i) i = withVal("log", i) i = noVal("noupdate", i) i = noVal("debug", i) i = noVal("updateonly", i) if i == j: error("unknown parameter %s" % arg) def readUsers(self): if self.users and os.path.exists(self.users): for line in open(self.users): line = line.strip() if not line or line.startswith("#"): continue m = line.split() if self.sender == m[0]: self.sender = " ".join(m[1:]) break def _git_config(self, key, default): cfg = git(["config %s" % key]) return cfg[0] if cfg else default def log(msg): print >>Config.log, "%s - %s" % (time.asctime(), msg) def error(msg): log("Error: %s" % msg) sys.exit(1) def git(args, stdout_to=subprocess.PIPE, all=False): if isinstance(args, tuple) or isinstance(args, list): args = " ".join(args) try: if Config.debug: print >>sys.stderr, "> git " + args except NameError: # Config may not be defined yet. pass try: child = subprocess.Popen("git " + args, shell=True, stdin=None, stdout=stdout_to, stderr=subprocess.PIPE) (stdout, stderr) = child.communicate() except OSError, e: error("cannot start git: %s" % str(e)) if child.returncode != 0 and stderr: msg = ": %s" % stderr if stderr else "" error("git child failed with exit code %d%s" % (child.returncode, msg)) if stdout_to != subprocess.PIPE: return [] if not all: return [line.strip() for line in stdout.split("\n") if line] else: return stdout.split("\n") def getHeads(state): for (rev, head) in [head.split() for head in git("show-ref --heads")]: if head.startswith("refs/heads/"): head = head[11:] state.heads[head] = rev def getTags(state): for (rev, tag) in [head.split() for head in git("show-ref --tags")]: # We are only interested in annotaged tags. type = git("cat-file -t %s" % rev)[0] if type == "tag": if tag.startswith("refs/tags/"): tag= tag[10:] state.tags[tag] = rev def getReachableRefs(state): for rev in git(["rev-list"] + state.heads.keys() + state.tags.keys()): state.revs[rev] = [] def getCurrent(): state = State() getHeads(state) getTags(state) getReachableRefs(state) return state Tmps = [] def makeTmp(): global Tmps (fd, fname) = tempfile.mkstemp(prefix="%s-" % Name, suffix=".tmp") Tmps += [fname] return (os.fdopen(fd, "w"), fname) def deleteTmps(): for tmp in Tmps: os.unlink(tmp) def mailTag(key, value): return "%-11s: %s" % (key, value) def generateMailHeader(subject): (out, fname) = makeTmp() print >>out, """From: %s To: %s Subject: %s %s X-Git-Repository: %s X-Mailer: %s %s %s """ % (Config.sender, Config.mailinglist, Config.emailprefix, subject, Config.repo, Name, Version, mailTag("Repository", Config.repo)), return (out, fname) def sendMail(out, fname): out.close() if Config.debug: for line in open(fname): print " |", line, print "" else: stdin = subprocess.Popen("/usr/sbin/sendmail -t", shell=True, stdin=subprocess.PIPE).stdin for line in open(fname): print >>stdin, line, stdin.close() # Wait a bit in case we're going to send more mails. Otherwise, the mails # get sent back-to-back and are likely to end up with identical timestamps, # which may then make them appear to have arrived in the wrong order. if not Config.debug: time.sleep(2) def entryAdded(key, value, rev): log("New %s %s" % (key, value)) (out, fname) = generateMailHeader("%s '%s' created" % (key, value)) print >>out, mailTag("New %s" % key, value) print >>out, mailTag("Referencing", rev) sendMail(out, fname) def entryDeleted(key, value): log("Deleted %s %s" % (key, value)) (out, fname) = generateMailHeader("%s '%s' deleted" % (key, value)) print >>out, mailTag("Deleted %s" % key, value) sendMail(out, fname) def commit(current, rev): log("New revision %s" % rev) branches = [] for branch in current.heads: revs = git(["rev-list", rev, "^%s^" % rev, "^" + branch]) if (rev not in revs): branches += [branch] # branches = current.revs[rev] multi = "es" if len(branches) > 1 else "" branches = ",".join(branches) subject = git("show '--pretty=format:%%s (%%h)' -s %s" % rev) (out, fname) = generateMailHeader("%s: %s" % (branches, subject[0])) print >>out, mailTag("On branch%s" % multi, branches) print >>out, "\nhttp://hackage.haskell.org/trac/ghc/changeset/%s" % rev show_flags="--stat --no-color --find-copies-harder --pretty=medium --ignore-space-at-eol" footer = "" show = git("show %s -s %s" % (show_flags, rev)) for line in show: if NoDiff in line: break if NoMail in line: return else: (tmp, tname) = makeTmp() git("show -p %s" % rev, stdout_to=tmp) tmp.close() size = os.path.getsize(tname) if size > Config.maxdiffsize: footer = "\nDiff suppressed because of size. To see it, use:\n\n git show %s" % rev else: show_flags += " -p" show = git("show %s %s" % (show_flags, rev), all=True) print >>out, Separator for line in show: if line == "---": print >>out, Separator else: print >>out, line print >>out, footer sendMail(out, fname) def headMoved(head, path): log("Head moved: %s -> %s" % (head, path[-1])) subject = git("show '--pretty=format:%%s (%%h)' -s %s" % path[-1]) (out, fname) = generateMailHeader("%s's head updated: %s" % (head, subject[0])) print >>out, "Branch '%s' now includes:" % head print >>out, "" for rev in path: print >>out, " ", git("show -s --oneline --abbrev-commit %s" % rev)[0] sendMail(out, fname) if "-h" in sys.argv or "--help" in sys.argv: print >>sys.stderr, "Usage: %s " % sys.argv[0] print >>sys.stderr, "" print >>sys.stderr, "See README for options." sys.exit(1) Config = GitConfig(sys.argv[1:]) log("Running for %s" % os.getcwd()) cache = State() if os.path.exists(CacheFile): cache.readFrom(CacheFile) report = (not Config.updateonly) else: log("Initial run. Not generating any mails, just recording current state.") report = False current = getCurrent() if report: # Check heads. old = set(cache.heads.keys()) new = set(current.heads.keys()) for head in (new - old): entryAdded("branch", head, current.heads[head]) for head in (old - new): entryDeleted("branch", head) stable_heads = new & old # Check Tags. old = set(cache.tags.keys()) new = set(current.tags.keys()) for tag in (new - old): entryAdded("tag", tag, current.tags[tag]) for tag in (old - new): entryDeleted("tag", tag) # Check commits. old = set(cache.revs.keys()) new = set(current.revs.keys()) # Sort updates by time. def _key(rev): ts = git("show -s '--pretty=format:%%ct' %s" % rev) return int(ts[0]) new_revs = new - old for rev in sorted(new_revs, key=_key): commit(current, rev) # See if heads have moved to include already reported revisions. for head in stable_heads: old_rev = cache.heads[head] new_rev = current.heads[head] path = git(["rev-list", "--reverse", "--date-order", new_rev, "^%s" % old_rev]) if len(set(path) - new_revs): headMoved(head, path) if not Config.noupdate: current.writeTo(CacheFile) deleteTmps()