From: Stephen Haberman Date: Thu, 19 Jun 2008 17:47:02 +0000 (-0500) Subject: Initial hooks. X-Git-Url: http://git.droids-corp.org/?a=commitdiff_plain;h=2e5fa40dba316e274fd6d47c25fd78da350f2525;p=git-central.git Initial hooks. --- 2e5fa40dba316e274fd6d47c25fd78da350f2525 diff --git a/post-receive-email b/post-receive-email new file mode 100644 index 0000000..ec547af --- /dev/null +++ b/post-receive-email @@ -0,0 +1,649 @@ +#!/bin/sh +# +# Copyright (c) 2007 Andy Parkins +# +# An example hook script to mail out commit update information. This hook +# sends emails listing new revisions to the repository introduced by the +# change being reported. The rule is that (for branch updates) each commit +# will appear on one email and one email only. +# +# This hook is stored in the contrib/hooks directory. Your distribution +# will have put this somewhere standard. You should make this script +# executable then link to it in the repository you would like to use it in. +# For example, on debian the hook is stored in +# /usr/share/doc/git-core/contrib/hooks/post-receive-email: +# +# chmod a+x post-receive-email +# cd /path/to/your/repository.git +# ln -sf /usr/share/doc/git-core/contrib/hooks/post-receive-email hooks/post-receive +# +# This hook script assumes it is enabled on the central repository of a +# project, with all users pushing only to it and not between each other. It +# will still work if you don't operate in that style, but it would become +# possible for the email to be from someone other than the person doing the +# push. +# +# Config +# ------ +# hooks.mailinglist +# This is the list that all pushes will go to; leave it blank to not send +# emails for every ref update. +# hooks.announcelist +# This is the list that all pushes of annotated tags will go to. Leave it +# blank to default to the mailinglist field. The announce emails lists +# the short log summary of the changes since the last annotated tag. +# hooks.envelopesender +# If set then the -f option is passed to sendmail to allow the envelope +# sender address to be set +# hooks.emailprefix +# All emails have their subjects prefixed with this prefix, or "[SCM]" +# if emailprefix is unset, to aid filtering +# +# Notes +# ----- +# All emails include the headers "X-Git-Refname", "X-Git-Oldrev", +# "X-Git-Newrev", and "X-Git-Reftype" to enable fine tuned filtering and +# give information for debugging. +# + +# ---------------------------- Functions + +# +# Top level email generation function. This decides what type of update +# this is and calls the appropriate body-generation routine after outputting +# the common header +# +# Note this function doesn't actually generate any email output, that is +# taken care of by the functions it calls: +# - generate_email_header +# - generate_create_XXXX_email +# - generate_update_XXXX_email +# - generate_delete_XXXX_email +# - generate_email_footer +# +generate_email() +{ + # --- Arguments + oldrev=$(git rev-parse $1) + newrev=$(git rev-parse $2) + refname="$3" + + # --- Interpret + # 0000->1234 (create) + # 1234->2345 (update) + # 2345->0000 (delete) + if expr "$oldrev" : '0*$' >/dev/null + then + change_type="create" + else + if expr "$newrev" : '0*$' >/dev/null + then + change_type="delete" + else + change_type="update" + fi + fi + + # --- Get the revision types + newrev_type=$(git cat-file -t $newrev 2> /dev/null) + oldrev_type=$(git cat-file -t "$oldrev" 2> /dev/null) + case "$change_type" in + create|update) + rev="$newrev" + rev_type="$newrev_type" + ;; + delete) + rev="$oldrev" + rev_type="$oldrev_type" + ;; + esac + + # The revision type tells us what type the commit is, combined with + # the location of the ref we can decide between + # - working branch + # - tracking branch + # - unannoted tag + # - annotated tag + case "$refname","$rev_type" in + refs/tags/*,commit) + # un-annotated tag + refname_type="tag" + short_refname=${refname##refs/tags/} + ;; + refs/tags/*,tag) + # annotated tag + refname_type="annotated tag" + short_refname=${refname##refs/tags/} + # change recipients + if [ -n "$announcerecipients" ]; then + recipients="$announcerecipients" + fi + ;; + refs/heads/*,commit) + # branch + refname_type="branch" + short_refname=${refname##refs/heads/} + ;; + refs/remotes/*,commit) + # tracking branch + refname_type="tracking branch" + short_refname=${refname##refs/remotes/} + echo >&2 "*** Push-update of tracking branch, $refname" + echo >&2 "*** - no email generated." + exit 0 + ;; + *) + # Anything else (is there anything else?) + echo >&2 "*** Unknown type of update to $refname ($rev_type)" + echo >&2 "*** - no email generated" + exit 1 + ;; + esac + + # Check if we've got anyone to send to + if [ -z "$recipients" ]; then + case "$refname_type" in + "annotated tag") + config_name="hooks.announcelist" + ;; + *) + config_name="hooks.mailinglist" + ;; + esac + echo >&2 "*** $config_name is not set so no email will be sent" + echo >&2 "*** for $refname update $oldrev->$newrev" + exit 0 + fi + + # Email parameters + # The email subject will contain the best description of the ref + # that we can build from the parameters + describe=$(git describe $rev 2>/dev/null) + if [ -z "$describe" ]; then + describe=$rev + fi + + generate_email_header + + # Call the correct body generation function + fn_name=general + case "$refname_type" in + "tracking branch"|branch) + fn_name=branch + ;; + "annotated tag") + fn_name=atag + ;; + esac + generate_${change_type}_${fn_name}_email + + generate_email_footer +} + +generate_email_header() +{ + # --- Email (all stdout will be the email) + # Generate header + cat <<-EOF + To: $recipients + Subject: ${emailprefix}$projectdesc $refname_type, $short_refname, ${change_type}d. $describe + X-Git-Refname: $refname + X-Git-Reftype: $refname_type + X-Git-Oldrev: $oldrev + X-Git-Newrev: $newrev + + This is an automated email from the git hooks/post-receive script. It was + generated because a ref change was pushed to the repository containing + the project "$projectdesc". + + The $refname_type, $short_refname has been ${change_type}d + EOF +} + +generate_email_footer() +{ + SPACE=" " + cat <<-EOF + + + hooks/post-receive + --${SPACE} + $projectdesc + EOF +} + +# --------------- Branches + +# +# Called for the creation of a branch +# +generate_create_branch_email() +{ + # This is a new branch and so oldrev is not valid + echo " at $newrev ($newrev_type)" + echo "" + + echo $LOGBEGIN + # This shows all log entries that are not already covered by + # another ref - i.e. commits that are now accessible from this + # ref that were previously not accessible + # (see generate_update_branch_email for the explanation of this + # command) + git rev-parse --not --branches | grep -v $(git rev-parse $refname) | + git rev-list --pretty --reverse --stdin $newrev + echo $LOGEND + + echo "" + echo "Summary of changes:" + git diff-tree --stat -p $newrev +} + +# +# Called for the change of a pre-existing branch +# +generate_update_branch_email() +{ + # Consider this: + # 1 --- 2 --- O --- X --- 3 --- 4 --- N + # + # O is $oldrev for $refname + # N is $newrev for $refname + # X is a revision pointed to by some other ref, for which we may + # assume that an email has already been generated. + # In this case we want to issue an email containing only revisions + # 3, 4, and N. Given (almost) by + # + # git rev-list N ^O --not --all + # + # The reason for the "almost", is that the "--not --all" will take + # precedence over the "N", and effectively will translate to + # + # git rev-list N ^O ^X ^N + # + # So, we need to build up the list more carefully. git rev-parse + # will generate a list of revs that may be fed into git rev-list. + # We can get it to make the "--not --all" part and then filter out + # the "^N" with: + # + # git rev-parse --not --all | grep -v N + # + # Then, using the --stdin switch to git rev-list we have effectively + # manufactured + # + # git rev-list N ^O ^X + # + # This leaves a problem when someone else updates the repository + # while this script is running. Their new value of the ref we're + # working on would be included in the "--not --all" output; and as + # our $newrev would be an ancestor of that commit, it would exclude + # all of our commits. What we really want is to exclude the current + # value of $refname from the --not list, rather than N itself. So: + # + # git rev-parse --not --all | grep -v $(git rev-parse $refname) + # + # Get's us to something pretty safe (apart from the small time + # between refname being read, and git rev-parse running - for that, + # I give up) + # + # + # Next problem, consider this: + # * --- B --- * --- O ($oldrev) + # \ + # * --- X --- * --- N ($newrev) + # + # That is to say, there is no guarantee that oldrev is a strict + # subset of newrev (it would have required a --force, but that's + # allowed). So, we can't simply say rev-list $oldrev..$newrev. + # Instead we find the common base of the two revs and list from + # there. + # + # As above, we need to take into account the presence of X; if + # another branch is already in the repository and points at some of + # the revisions that we are about to output - we don't want them. + # The solution is as before: git rev-parse output filtered. + # + # Finally, tags: 1 --- 2 --- O --- T --- 3 --- 4 --- N + # + # Tags pushed into the repository generate nice shortlog emails that + # summarise the commits between them and the previous tag. However, + # those emails don't include the full commit messages that we output + # for a branch update. Therefore we still want to output revisions + # that have been output on a tag email. + # + # Luckily, git rev-parse includes just the tool. Instead of using + # "--all" we use "--branches"; this has the added benefit that + # "remotes/" will be ignored as well. + + # List all of the revisions that were removed by this update, in a + # fast forward update, this list will be empty, because rev-list O + # ^N is empty. For a non fast forward, O ^N is the list of removed + # revisions + fast_forward="" + rev="" + for rev in $(git rev-list $newrev..$oldrev) + do + revtype=$(git cat-file -t "$rev") + echo " discards $rev ($revtype)" + done + if [ -z "$rev" ]; then + fast_forward=1 + fi + + # List all the revisions from baserev to newrev in a kind of + # "table-of-contents"; note this list can include revisions that + # have already had notification emails and is present to show the + # full detail of the change from rolling back the old revision to + # the base revision and then forward to the new revision + for rev in $(git rev-list $oldrev..$newrev) + do + revtype=$(git cat-file -t "$rev") + echo " via $rev ($revtype)" + done + + if [ "$fast_forward" ]; then + echo " from $oldrev ($oldrev_type)" + else + # 1. Existing revisions were removed. In this case newrev + # is a subset of oldrev - this is the reverse of a + # fast-forward, a rewind + # 2. New revisions were added on top of an old revision, + # this is a rewind and addition. + + # (1) certainly happened, (2) possibly. When (2) hasn't + # happened, we set a flag to indicate that no log printout + # is required. + + echo "" + + # Find the common ancestor of the old and new revisions and + # compare it with newrev + baserev=$(git merge-base $oldrev $newrev) + rewind_only="" + if [ "$baserev" = "$newrev" ]; then + echo "This update discarded existing revisions and left the branch pointing at" + echo "a previous point in the repository history." + echo "" + echo " * -- * -- N ($newrev)" + echo " \\" + echo " O -- O -- O ($oldrev)" + echo "" + echo "The removed revisions are not necessarilly gone - if another reference" + echo "still refers to them they will stay in the repository." + rewind_only=1 + else + echo "This update added new revisions after undoing existing revisions. That is" + echo "to say, the old revision is not a strict subset of the new revision. This" + echo "situation occurs when you --force push a change and generate a repository" + echo "containing something like this:" + echo "" + echo " * -- * -- B -- O -- O -- O ($oldrev)" + echo " \\" + echo " N -- N -- N ($newrev)" + echo "" + echo "When this happens we assume that you've already had alert emails for all" + echo "of the O revisions, and so we here report only the revisions in the N" + echo "branch from the common base, B." + fi + fi + + echo "" + if [ -z "$rewind_only" ]; then + echo "Those revisions listed above that are new to this repository have" + echo "not appeared on any other notification email; so we list those" + echo "revisions in full, below." + + echo "" + echo $LOGBEGIN + git rev-parse --not --branches | grep -v $(git rev-parse $refname) | + git rev-list --reverse --pretty --stdin $oldrev..$newrev + + # XXX: Need a way of detecting whether git rev-list actually + # outputted anything, so that we can issue a "no new + # revisions added by this update" message + + echo $LOGEND + else + echo "No new revisions were added by this update." + fi + + # The diffstat is shown from the old revision to the new revision. + # This is to show the truth of what happened in this change. + # There's no point showing the stat from the base to the new + # revision because the base is effectively a random revision at this + # point - the user will be interested in what this revision changed + # - including the undoing of previous revisions in the case of + # non-fast forward updates. + echo "" + echo "Summary of changes:" + git diff-tree --stat -p --find-copies-harder $oldrev..$newrev +} + +# +# Called for the deletion of a branch +# +generate_delete_branch_email() +{ + echo " was $oldrev" + echo "" + echo $LOGEND + git show -s --pretty=oneline $oldrev + echo $LOGEND +} + +# --------------- Annotated tags + +# +# Called for the creation of an annotated tag +# +generate_create_atag_email() +{ + echo " at $newrev ($newrev_type)" + + generate_atag_email +} + +# +# Called for the update of an annotated tag (this is probably a rare event +# and may not even be allowed) +# +generate_update_atag_email() +{ + echo " to $newrev ($newrev_type)" + echo " from $oldrev (which is now obsolete)" + + generate_atag_email +} + +# +# Called when an annotated tag is created or changed +# +generate_atag_email() +{ + # Use git for-each-ref to pull out the individual fields from the + # tag + eval $(git for-each-ref --shell --format=' + tagobject=%(*objectname) + tagtype=%(*objecttype) + tagger=%(taggername) + tagged=%(taggerdate)' $refname + ) + + echo " tagging $tagobject ($tagtype)" + case "$tagtype" in + commit) + + # If the tagged object is a commit, then we assume this is a + # release, and so we calculate which tag this tag is + # replacing + prevtag=$(git describe --abbrev=0 $newrev^ 2>/dev/null) + + if [ -n "$prevtag" ]; then + echo " replaces $prevtag" + fi + ;; + *) + echo " length $(git cat-file -s $tagobject) bytes" + ;; + esac + echo " tagged by $tagger" + echo " on $tagged" + + echo "" + echo $LOGBEGIN + + # Show the content of the tag message; this might contain a change + # log or release notes so is worth displaying. + git cat-file tag $newrev | sed -e '1,/^$/d' + + echo "" + case "$tagtype" in + commit) + # Only commit tags make sense to have rev-list operations + # performed on them + if [ -n "$prevtag" ]; then + # Show changes since the previous release + git rev-list --pretty=short "$prevtag..$newrev" | git shortlog + else + # No previous tag, show all the changes since time + # began + git rev-list --pretty=short $newrev | git shortlog + fi + ;; + *) + # XXX: Is there anything useful we can do for non-commit + # objects? + ;; + esac + + echo $LOGEND +} + +# +# Called for the deletion of an annotated tag +# +generate_delete_atag_email() +{ + echo " was $oldrev" + echo "" + echo $LOGEND + git show -s --pretty=oneline $oldrev + echo $LOGEND +} + +# --------------- General references + +# +# Called when any other type of reference is created (most likely a +# non-annotated tag) +# +generate_create_general_email() +{ + echo " at $newrev ($newrev_type)" + + generate_general_email +} + +# +# Called when any other type of reference is updated (most likely a +# non-annotated tag) +# +generate_update_general_email() +{ + echo " to $newrev ($newrev_type)" + echo " from $oldrev" + + generate_general_email +} + +# +# Called for creation or update of any other type of reference +# +generate_general_email() +{ + # Unannotated tags are more about marking a point than releasing a + # version; therefore we don't do the shortlog summary that we do for + # annotated tags above - we simply show that the point has been + # marked, and print the log message for the marked point for + # reference purposes + # + # Note this section also catches any other reference type (although + # there aren't any) and deals with them in the same way. + + echo "" + if [ "$newrev_type" = "commit" ]; then + echo $LOGBEGIN + git show --no-color --root -s --pretty=medium $newrev + echo $LOGEND + else + # What can we do here? The tag marks an object that is not + # a commit, so there is no log for us to display. It's + # probably not wise to output git cat-file as it could be a + # binary blob. We'll just say how big it is + echo "$newrev is a $newrev_type, and is $(git cat-file -s $newrev) bytes long." + fi +} + +# +# Called for the deletion of any other type of reference +# +generate_delete_general_email() +{ + echo " was $oldrev" + echo "" + echo $LOGEND + git show -s --pretty=oneline $oldrev + echo $LOGEND +} + +send_mail() +{ + if [ -n "$envelopesender" ]; then + /usr/sbin/sendmail -t -f "$envelopesender" + else + # /usr/sbin/sendmail -t + /home/BIPFS/shaberman/local/bin/msmtp --file=/srv/git/hooks/msmtp.conf -t + fi +} + +# ---------------------------- main() + +# --- Constants +LOGBEGIN="- Log -----------------------------------------------------------------" +LOGEND="-----------------------------------------------------------------------" + +# --- Config +# Set GIT_DIR either from the working directory, or from the environment +# variable. +GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) +if [ -z "$GIT_DIR" ]; then + echo >&2 "fatal: post-receive: GIT_DIR not set" + exit 1 +fi + +projectdesc=$(sed -ne '1p' "$GIT_DIR/description") +# Check if the description is unchanged from it's default, and shorten it to +# a more manageable length if it is +if expr "$projectdesc" : "Unnamed repository.*$" >/dev/null +then + projectdesc="UNNAMED PROJECT" +fi + +recipients=$(git config hooks.mailinglist) +announcerecipients=$(git config hooks.announcelist) +envelopesender=$(git config hooks.envelopesender) +emailprefix=$(git config hooks.emailprefix || echo '[SCM] ') + +# --- Main loop +# Allow dual mode: run from the command line just like the update hook, or +# if no arguments are given then run as a hook script +if [ -n "$1" -a -n "$2" -a -n "$3" ]; then + # Output to the terminal in command line mode - if someone wanted to + # resend an email; they could redirect the output to sendmail + # themselves + PAGER= generate_email $2 $3 $1 +else + while read oldrev newrev refname + do + generate_email $oldrev $newrev $refname | send_mail + done +fi diff --git a/trac-post-commit-hook.py b/trac-post-commit-hook.py new file mode 100644 index 0000000..ce23af5 --- /dev/null +++ b/trac-post-commit-hook.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python + +# trac-post-commit-hook +# ---------------------------------------------------------------------------- +# Copyright (c) 2004 Stephen Hansen +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +# ---------------------------------------------------------------------------- + +# This Subversion post-commit hook script is meant to interface to the +# Trac (http://www.edgewall.com/products/trac/) issue tracking/wiki/etc +# system. +# +# It should be called from the 'post-commit' script in Subversion, such as +# via: +# +# REPOS="$1" +# REV="$2" +# TRAC_ENV='/somewhere/trac/project/' +# +# /usr/bin/python /usr/local/src/trac/contrib/trac-post-commit-hook \ +# -p "$TRAC_ENV" \ +# -r "$REV" +# +# It searches commit messages for text in the form of: +# command #1 +# command #1, #2 +# command #1 & #2 +# command #1 and #2 +# +# You can have more then one command in a message. The following commands +# are supported. There is more then one spelling for each command, to make +# this as user-friendly as possible. +# +# closes, fixes +# The specified issue numbers are closed with the contents of this +# commit message being added to it. +# references, refs, addresses, re +# The specified issue numbers are left in their current status, but +# the contents of this commit message are added to their notes. +# +# A fairly complicated example of what you can do is with a commit message +# of: +# +# Changed blah and foo to do this or that. Fixes #10 and #12, and refs #12. +# +# This will close #10 and #12, and add a note to #12. + +import re +import os +import sys +from datetime import datetime + +from trac.env import open_environment +from trac.ticket.notification import TicketNotifyEmail +from trac.ticket import Ticket +from trac.ticket.web_ui import TicketModule +# TODO: move grouped_changelog_entries to model.py +from trac.util.datefmt import utc +from trac.versioncontrol.api import NoSuchChangeset + +from optparse import OptionParser + +parser = OptionParser() +parser.add_option('-p', '--project', dest='project', help='Path to the Trac project.') +parser.add_option('-r', '--revision', dest='rev', help='Repository revision number.') + +(options, args) = parser.parse_args(sys.argv[1:]) + +leftEnv = '' +rghtEnv = '' +commandPattern = re.compile(leftEnv + r'(?P[A-Za-z]*).?(?P#[0-9]+(?:(?:[, &]*|[ ]?and[ ]?)#[0-9]+)*)' + rghtEnv) +ticketPattern = re.compile(r'#([0-9]*)') + +# Old commands: +# 'close': '_cmdClose', +# 'closed': '_cmdClose', +# 'closes': '_cmdClose', +# 'fix': '_cmdClose', +# 'fixed': '_cmdClose', +# 'fixes': '_cmdClose', +# 'addresses': '_cmdRefs', +# 'references': '_cmdRefs', +# 'see': '_cmdRefs'} + +class CommitHook: + + _supported_cmds = { + 're': '_cmdRefs', + 'refs': '_cmdRefs', + 'qa': '_cmdQa' + } + + def __init__(self, project=options.project, rev=options.rev): + self.env = open_environment(project) + repos = self.env.get_repository() + repos.sync() + + # Instead of bothering with the encoding, we'll use unicode data + # as provided by the Trac versioncontrol API (#1310). + try: + chgset = repos.get_changeset(rev) + except NoSuchChangeset: + return # out of scope changesets are not cached + self.author = chgset.author + self.rev = rev + self.msg = "(In [%s]) %s" % (rev, chgset.message) + self.now = datetime.now(utc) + + cmdGroups = commandPattern.findall(self.msg) + + tickets = {} + for cmd, tkts in cmdGroups: + funcname = CommitHook._supported_cmds.get(cmd.lower(), '') + if funcname: + for tkt_id in ticketPattern.findall(tkts): + func = getattr(self, funcname) + tickets.setdefault(tkt_id, []).append(func) + + for tkt_id, cmds in tickets.iteritems(): + try: + db = self.env.get_db_cnx() + + ticket = Ticket(self.env, int(tkt_id), db) + for cmd in cmds: + cmd(ticket) + + # determine sequence number... + cnum = 0 + tm = TicketModule(self.env) + for change in tm.grouped_changelog_entries(ticket, db): + if change['permanent']: + cnum += 1 + + ticket.save_changes(self.author, self.msg, self.now, db, cnum+1) + db.commit() + + tn = TicketNotifyEmail(self.env) + tn.notify(ticket, newticket=0, modtime=self.now) + except Exception, e: + # import traceback + # traceback.print_exc(file=sys.stderr) + print>>sys.stderr, 'Unexpected error while processing ticket ' \ + 'ID %s: %s' % (tkt_id, e) + + def _cmdClose(self, ticket): + ticket['status'] = 'closed' + ticket['resolution'] = 'fixed' + + def _cmdRefs(self, ticket): + pass + + def _cmdQa(self, ticket): + ticket['phase'] = 'QA' + ticket['owner'] = '' + ticket['status'] = 'new' + +if __name__ == "__main__": + if len(sys.argv) < 5: + print "For usage: %s --help" % (sys.argv[0]) + print + print "Note that the deprecated options will be removed in Trac 0.12." + else: + CommitHook() diff --git a/trac-post-commit-hook.sh b/trac-post-commit-hook.sh new file mode 100644 index 0000000..0adb086 --- /dev/null +++ b/trac-post-commit-hook.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +export LD_LIBRARY_PATH=/home/BIPFS/shaberman/local/lib +TRAC_ENV=/srv/trac/cbas + +while read oldrev newrev refname ; do + if expr "$oldrev" : '0*$' >/dev/null + then + # git-rev-list "$newrev" + git rev-parse --not --branches | grep -v $(git rev-parse $refname) | git rev-list --stdin $newrev + else + # git-rev-list "$newrev" "^$oldrev" + git rev-parse --not --branches | grep -v $(git rev-parse $refname) | git rev-list --stdin $oldrev..$newrev + fi | while read com ; do + /home/BIPFS/shaberman/local/bin/python /srv/git/hooks/trac-post-commit-hook.py -p "$TRAC_ENV" -r "$com" + done +done + +exit 0 diff --git a/trac-pre-commit-hook.py b/trac-pre-commit-hook.py new file mode 100644 index 0000000..3d5a79d --- /dev/null +++ b/trac-pre-commit-hook.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# -*- coding: iso8859-1 -*- +# +# Author: Jonas Borgström +# +# This script will enforce the following policy: +# +# "A checkin must reference an open ticket." +# +# This script should be invoked from the subversion pre-commit hook like this: +# +# REPOS="$1" +# TXN="$2" +# TRAC_ENV="/somewhere/trac/project/" +# LOG=`/usr/bin/svnlook log -t "$TXN" "$REPOS"` +# /usr/bin/python /some/path/trac-pre-commit-hook "$TRAC_ENV" "$LOG" || exit 1 +# +import os +import re +import sys + +from trac.env import open_environment + +def main(): + if len(sys.argv) != 3: + print >> sys.stderr, 'Usage: %s ' % sys.argv[0] + sys.exit(1) + + env_path = sys.argv[1] + log = sys.argv[2].lower() + + # Stephen: exempt 'No ticket' + if re.search('no ticket', log) or re.search('initialized merge tracking', log): + sys.exit(0) + + tickets = [] + for tmp in re.findall('(?:refs|re|qa).?(#[0-9]+(?:(?:[, &]+| *and *)#[0-9]+)*)', log): + tickets += re.findall('#([0-9]+)', tmp) + + # At least one ticket has to be mentioned in the log message + if tickets == []: + print >> sys.stderr, 'At least one open ticket must be mentioned in the log message.' + sys.exit(1) + + env = open_environment(env_path) + db = env.get_db_cnx() + + cursor = db.cursor() + # Stephen: let the tickets be closed for now + # cursor.execute("SELECT COUNT(id) FROM ticket WHERE status <> 'closed' AND id IN (%s)" % ','.join(tickets)) + cursor.execute("SELECT COUNT(id) FROM ticket WHERE id IN (%s)" % ','.join(tickets)) + row = cursor.fetchone() + if not row or row[0] < 1: + print >> sys.stderr, 'At least one open ticket must be mentioned in the log message.' + sys.exit(1) + else: + sys.exit(0) + +if __name__ == '__main__': + main() + + + diff --git a/wine-git-notify b/wine-git-notify new file mode 100644 index 0000000..04934ff --- /dev/null +++ b/wine-git-notify @@ -0,0 +1,403 @@ +#!/bin/env perl +# +# Tool to send git commit notifications +# +# Copyright 2005 Alexandre Julliard +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# +# +# This script is meant to be called from .git/hooks/update. +# +# Usage: git-notify [options] [--] refname old-sha1 new-sha1 +# +# -c name Send CIA notifications under specified project name +# -m addr Send mail notifications to specified address +# -n max Set max number of individual mails to send +# -r name Set the git repository name +# -s bytes Set the maximum diff size in bytes (-1 for no limit) +# -u url Set the URL to the gitweb browser +# -x branch Exclude changes to the specified branch from reports +# + +use strict; +use open ':utf8'; +use Encode 'encode'; +use Cwd 'realpath'; + +binmode STDIN, ':utf8'; +binmode STDOUT, ':utf8'; + +# some parameters you may want to change + +# base URL of the gitweb repository browser (can be set with the -u option) +my $gitweb_url = "http://source.winehq.org/git"; + +# set this to something that takes "-s" +my $mailer = "/usr/bin/mail"; + +# default repository name (can be changed with the -r option) +my $repos_name = ""; + +# max size of diffs in bytes (can be changed with the -s option) +my $max_diff_size = 10000; + +# address for mail notices (can be set with -m option) +my $commitlist_address; + +# project name for CIA notices (can be set with -c option) +my $cia_project_name; + +# CIA notification address +my $cia_address = "cia\@cia.navi.cx"; + +# max number of individual notices before falling back to a single global notice (can be set with -n option) +my $max_individual_notices = 100; + +# debug mode +my $debug = 0; + +# branches to exclude +my @exclude_list = (); + +sub usage() +{ + print "Usage: $0 [options] [--] refname old-sha1 new-sha1\n"; + print " -c name Send CIA notifications under specified project name\n"; + print " -m addr Send mail notifications to specified address\n"; + print " -n max Set max number of individual mails to send\n"; + print " -r name Set the git repository name\n"; + print " -s bytes Set the maximum diff size in bytes (-1 for no limit)\n"; + print " -u url Set the URL to the gitweb browser\n"; + print " -x branch Exclude changes to the specified branch from reports\n"; + exit 1; +} + +sub xml_escape($) +{ + my $str = shift; + $str =~ s/&/&/g; + $str =~ s//>/g; + my @chars = unpack "U*", $str; + $str = join "", map { ($_ > 127) ? sprintf "&#%u;", $_ : chr($_); } @chars; + return $str; +} + +# format an integer date + timezone as string +# algorithm taken from git's date.c +sub format_date($$) +{ + my ($time,$tz) = @_; + + if ($tz < 0) + { + my $minutes = (-$tz / 100) * 60 + (-$tz % 100); + $time -= $minutes * 60; + } + else + { + my $minutes = ($tz / 100) * 60 + ($tz % 100); + $time += $minutes * 60; + } + return gmtime($time) . sprintf " %+05d", $tz; +} + +# parse command line options +sub parse_options() +{ + while (@ARGV && $ARGV[0] =~ /^-/) + { + my $arg = shift @ARGV; + + if ($arg eq '--') { last; } + elsif ($arg eq '-c') { $cia_project_name = shift @ARGV; } + elsif ($arg eq '-m') { $commitlist_address = shift @ARGV; } + elsif ($arg eq '-n') { $max_individual_notices = shift @ARGV; } + elsif ($arg eq '-r') { $repos_name = shift @ARGV; } + elsif ($arg eq '-s') { $max_diff_size = shift @ARGV; } + elsif ($arg eq '-u') { $gitweb_url = shift @ARGV; } + elsif ($arg eq '-x') { push @exclude_list, "^" . shift @ARGV; } + elsif ($arg eq '-d') { $debug++; } + else { usage(); } + } + if (@ARGV && $#ARGV != 2) { usage(); } +} + +# send an email notification +sub mail_notification($$$@) +{ + my ($name, $subject, $content_type, @text) = @_; + $subject = encode("MIME-Q",$subject); + if ($debug) + { + print "---------------------\n"; + print "To: $name\n"; + print "Subject: $subject\n"; + print "Content-Type: $content_type\n"; + print "\n", join("\n", @text), "\n"; + } + else + { + my $pid = open MAIL, "|-"; + return unless defined $pid; + if (!$pid) + { + # exec $mailer, "-s", $subject, "-a", "Content-Type: $content_type", $name or die "Cannot exec $mailer"; + exec "/home/BIPFS/shaberman/local/bin/msmtp", "--file=/srv/git/hooks/msmtp.conf", "-t", "shaberman@exigencecorp.com", "-s", $subject, "-a", "Content-Type: $content_type", $name or die "Cannot exec $mailer"; + } + print MAIL join("\n", @text), "\n"; + close MAIL; + } +} + +# get the default repository name +sub get_repos_name() +{ + my $dir = `git rev-parse --git-dir`; + chomp $dir; + my $repos = realpath($dir); + $repos =~ s/(.*?)((\.git\/)?\.git)$/\1/; + $repos =~ s/(.*)\/([^\/]+)\/?$/\2/; + return $repos; +} + +# extract the information from a commit or tag object and return a hash containing the various fields +sub get_object_info($) +{ + my $obj = shift; + my %info = (); + my @log = (); + my $do_log = 0; + + open TYPE, "-|" or exec "git", "cat-file", "-t", $obj or die "cannot run git-cat-file"; + my $type = ; + chomp $type; + close TYPE; + + open OBJ, "-|" or exec "git", "cat-file", $type, $obj or die "cannot run git-cat-file"; + while () + { + chomp; + if ($do_log) + { + last if /^-----BEGIN PGP SIGNATURE-----/; + push @log, $_; + } + elsif (/^(author|committer|tagger) ((.*)(<.*>)) (\d+) ([+-]\d+)$/) + { + $info{$1} = $2; + $info{$1 . "_name"} = $3; + $info{$1 . "_email"} = $4; + $info{$1 . "_date"} = $5; + $info{$1 . "_tz"} = $6; + } + elsif (/^tag (.*)$/) + { + $info{"tag"} = $1; + } + elsif (/^$/) { $do_log = 1; } + } + close OBJ; + + $info{"type"} = $type; + $info{"log"} = \@log; + return %info; +} + +# send a commit notice to a mailing list +sub send_commit_notice($$) +{ + my ($ref,$obj) = @_; + my %info = get_object_info($obj); + my @notice = (); + my $subject; + + if ($info{"type"} eq "tag") + { + push @notice, + "Module: $repos_name", + "Branch: $ref", + "Tag: $obj", + "URL: $gitweb_url/?a=tag;h=$obj", + "", + "Tagger: " . $info{"tagger"}, + "Date: " . format_date($info{"tagger_date"},$info{"tagger_tz"}), + "", + join "\n", @{$info{"log"}}; + $subject = "Tag " . $info{"tag"} . " : " . $info{"tagger_name"} . ": " . ${$info{"log"}}[0]; + } + else + { + push @notice, + "Module: $repos_name", + "Branch: $ref", + "Commit: $obj", + "URL: $gitweb_url/?a=commit;h=$obj", + "", + "Author: " . $info{"author"}, + "Date: " . format_date($info{"author_date"},$info{"author_tz"}), + "", + join "\n", @{$info{"log"}}, + "", + "---", + ""; + + open STAT, "-|" or exec "git", "diff-tree", "--stat", "-M", "--no-commit-id", $obj or die "cannot exec git-diff-tree"; + push @notice, join("", ); + close STAT; + + open DIFF, "-|" or exec "git", "diff-tree", "-p", "-M", "--no-commit-id", $obj or die "cannot exec git-diff-tree"; + my $diff = join( "", ); + close DIFF; + + if (($max_diff_size == -1) || (length($diff) < $max_diff_size)) + { + push @notice, $diff; + } + else + { + push @notice, "Diff: $gitweb_url/?a=commitdiff;h=$obj", + } + + $subject = $info{"author_name"} . ": " . ${$info{"log"}}[0]; + } + + mail_notification($commitlist_address, $subject, "text/plain; charset=UTF-8", @notice); +} + +# send a commit notice to the CIA server +sub send_cia_notice($$) +{ + my ($ref,$commit) = @_; + my %info = get_object_info($commit); + my @cia_text = (); + + return if $info{"type"} ne "commit"; + + push @cia_text, + "", + " ", + " git-notify script for CIA", + " ", + " ", + " " . xml_escape($cia_project_name) . "", + " " . xml_escape($repos_name) . "", + " " . xml_escape($ref). "", + " ", + " ", + " ", + " " . substr($commit,0,10) . "", + " " . xml_escape($info{"author"}) . "", + " " . xml_escape(join "\n", @{$info{"log"}}) . "", + " "; + + open COMMIT, "-|" or exec "git", "diff-tree", "--name-status", "-r", "-M", $commit or die "cannot run git-diff-tree"; + while () + { + chomp; + if (/^([AMD])\t(.*)$/) + { + my ($action, $file) = ($1, $2); + my %actions = ( "A" => "add", "M" => "modify", "D" => "remove" ); + next unless defined $actions{$action}; + push @cia_text, " " . xml_escape($file) . ""; + } + elsif (/^R\d+\t(.*)\t(.*)$/) + { + my ($old, $new) = ($1, $2); + push @cia_text, " " . xml_escape($old) . ""; + } + } + close COMMIT; + + push @cia_text, + " ", + " " . xml_escape("$gitweb_url/?a=commit;h=$commit") . "", + " ", + " ", + " " . $info{"author_date"} . "", + ""; + + mail_notification($cia_address, "DeliverXML", "text/xml", @cia_text); +} + +# send a global commit notice when there are too many commits for individual mails +sub send_global_notice($$$) +{ + my ($ref, $old_sha1, $new_sha1) = @_; + my @notice = (); + + open LIST, "-|" or exec "git", "rev-list", "--pretty", "^$old_sha1", "$new_sha1", @exclude_list or die "cannot exec git-rev-list"; + while () + { + chomp; + s/^commit /URL: $gitweb_url\/?a=commit;h=/; + push @notice, $_; + } + close LIST; + + mail_notification($commitlist_address, "New commits on branch $ref", "text/plain; charset=UTF-8", @notice); +} + +# send all the notices +sub send_all_notices($$$) +{ + my ($ref, $old_sha1, $new_sha1) = @_; + + $ref =~ s/^refs\/heads\///; + + if ($old_sha1 eq '0' x 40) # new ref + { + send_commit_notice( $ref, $new_sha1 ) if $commitlist_address; + return; + } + + my @commits = (); + + open LIST, "-|" or exec "git", "rev-list", "^$old_sha1", "$new_sha1", @exclude_list or die "cannot exec git-rev-list"; + while () + { + chomp; + die "invalid commit $_" unless /^[0-9a-f]{40}$/; + unshift @commits, $_; + } + close LIST; + + if (@commits > $max_individual_notices) + { + send_global_notice( $ref, $old_sha1, $new_sha1 ) if $commitlist_address; + return; + } + + foreach my $commit (@commits) + { + send_commit_notice( $ref, $commit ) if $commitlist_address; + send_cia_notice( $ref, $commit ) if $cia_project_name; + } +} + +$repos_name = get_repos_name(); +parse_options(); + +# append repository path to URL +$gitweb_url .= "/$repos_name.git"; + +if (@ARGV) +{ + send_all_notices( $ARGV[0], $ARGV[1], $ARGV[2] ); +} +else # read them from stdin +{ + while (<>) + { + chomp; + if (/^([0-9a-f]{40}) ([0-9a-f]{40}) (.*)$/) { send_all_notices( $3, $1, $2 ); } + } +} + +exit 0; +