initial commit, origins explained in README
This commit is contained in:
commit
4724203731
|
@ -0,0 +1,5 @@
|
|||
This comes from the ancient GNOME gitadmin-bin that can't be found anywhere,
|
||||
that was later copied without history to the current GNOME sysadmin-bin module.
|
||||
|
||||
This module is an older history-less fork, with a few entr'ouvert specific
|
||||
changes from the start.
|
|
@ -0,0 +1,211 @@
|
|||
# Utility functions for git
|
||||
#
|
||||
# Copyright (C) 2008 Owen Taylor
|
||||
# Copyright (C) 2009 Red Hat, Inc
|
||||
#
|
||||
# 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 program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, If not, see
|
||||
# http://www.gnu.org/licenses/.
|
||||
#
|
||||
# (These are adapted from git-bz)
|
||||
|
||||
import os
|
||||
import re
|
||||
from subprocess import Popen, PIPE
|
||||
import sys
|
||||
|
||||
from util import die
|
||||
|
||||
# Clone of subprocess.CalledProcessError (not in Python 2.4)
|
||||
class CalledProcessError(Exception):
|
||||
def __init__(self, returncode, cmd):
|
||||
self.returncode = returncode
|
||||
self.cmd = cmd
|
||||
|
||||
def __str__(self):
|
||||
return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
|
||||
|
||||
NULL_REVISION = "0000000000000000000000000000000000000000"
|
||||
|
||||
# Run a git command
|
||||
# Non-keyword arguments are passed verbatim as command line arguments
|
||||
# Keyword arguments are turned into command line options
|
||||
# <name>=True => --<name>
|
||||
# <name>='<str>' => --<name>=<str>
|
||||
# Special keyword arguments:
|
||||
# _quiet: Discard all output even if an error occurs
|
||||
# _interactive: Don't capture stdout and stderr
|
||||
# _input=<str>: Feed <str> to stdinin of the command
|
||||
# _outfile=<file): Use <file> as the output file descriptor
|
||||
# _split_lines: Return an array with one string per returned line
|
||||
#
|
||||
def git_run(command, *args, **kwargs):
|
||||
to_run = ['git', command.replace("_", "-")]
|
||||
|
||||
interactive = False
|
||||
quiet = False
|
||||
input = None
|
||||
interactive = False
|
||||
outfile = None
|
||||
do_split_lines = False
|
||||
for (k,v) in kwargs.iteritems():
|
||||
if k == '_quiet':
|
||||
quiet = True
|
||||
elif k == '_interactive':
|
||||
interactive = True
|
||||
elif k == '_input':
|
||||
input = v
|
||||
elif k == '_outfile':
|
||||
outfile = v
|
||||
elif k == '_split_lines':
|
||||
do_split_lines = True
|
||||
elif v is True:
|
||||
if len(k) == 1:
|
||||
to_run.append("-" + k)
|
||||
else:
|
||||
to_run.append("--" + k.replace("_", "-"))
|
||||
else:
|
||||
to_run.append("--" + k.replace("_", "-") + "=" + v)
|
||||
|
||||
to_run.extend(args)
|
||||
|
||||
if outfile:
|
||||
stdout = outfile
|
||||
else:
|
||||
if interactive:
|
||||
stdout = None
|
||||
else:
|
||||
stdout = PIPE
|
||||
|
||||
if interactive:
|
||||
stderr = None
|
||||
else:
|
||||
stderr = PIPE
|
||||
|
||||
if input != None:
|
||||
stdin = PIPE
|
||||
else:
|
||||
stdin = None
|
||||
|
||||
process = Popen(to_run,
|
||||
stdout=stdout, stderr=stderr, stdin=stdin)
|
||||
output, error = process.communicate(input)
|
||||
if process.returncode != 0:
|
||||
if not quiet and not interactive:
|
||||
print >>sys.stderr, error,
|
||||
print output,
|
||||
raise CalledProcessError(process.returncode, " ".join(to_run))
|
||||
|
||||
if interactive or outfile:
|
||||
return None
|
||||
else:
|
||||
if do_split_lines:
|
||||
return output.strip().splitlines()
|
||||
else:
|
||||
return output.strip()
|
||||
|
||||
# Wrapper to allow us to do git.<command>(...) instead of git_run()
|
||||
class Git:
|
||||
def __getattr__(self, command):
|
||||
def f(*args, **kwargs):
|
||||
return git_run(command, *args, **kwargs)
|
||||
return f
|
||||
|
||||
git = Git()
|
||||
|
||||
class GitCommit:
|
||||
def __init__(self, id, subject):
|
||||
self.id = id
|
||||
self.subject = subject
|
||||
|
||||
# Takes argument like 'git.rev_list()' and returns a list of commit objects
|
||||
def rev_list_commits(*args, **kwargs):
|
||||
kwargs_copy = dict(kwargs)
|
||||
kwargs_copy['pretty'] = 'format:%s'
|
||||
kwargs_copy['_split_lines'] = True
|
||||
lines = git.rev_list(*args, **kwargs_copy)
|
||||
if (len(lines) % 2 != 0):
|
||||
raise RuntimeException("git rev-list didn't return an even number of lines")
|
||||
|
||||
result = []
|
||||
for i in xrange(0, len(lines), 2):
|
||||
m = re.match("commit\s+([A-Fa-f0-9]+)", lines[i])
|
||||
if not m:
|
||||
raise RuntimeException("Can't parse commit it '%s'", lines[i])
|
||||
commit_id = m.group(1)
|
||||
subject = lines[i + 1]
|
||||
result.append(GitCommit(commit_id, subject))
|
||||
|
||||
return result
|
||||
|
||||
# Loads a single commit object by ID
|
||||
def load_commit(commit_id):
|
||||
return rev_list_commits(commit_id + "^!")[0]
|
||||
|
||||
# Return True if the commit has multiple parents
|
||||
def commit_is_merge(commit):
|
||||
if isinstance(commit, basestring):
|
||||
commit = load_commit(commit)
|
||||
|
||||
parent_count = 0
|
||||
for line in git.cat_file("commit", commit.id, _split_lines=True):
|
||||
if line == "":
|
||||
break
|
||||
if line.startswith("parent "):
|
||||
parent_count += 1
|
||||
|
||||
return parent_count > 1
|
||||
|
||||
# Return a short one-line summary of the commit
|
||||
def commit_oneline(commit):
|
||||
if isinstance(commit, basestring):
|
||||
commit = load_commit(commit)
|
||||
|
||||
return commit.id[0:7]+"... " + commit.subject[0:59]
|
||||
|
||||
# Return the directory name with .git stripped as a short identifier
|
||||
# for the module
|
||||
def get_module_name():
|
||||
try:
|
||||
git_dir = git.rev_parse(git_dir=True, _quiet=True)
|
||||
except CalledProcessError:
|
||||
die("GIT_DIR not set")
|
||||
|
||||
# Use the directory name with .git stripped as a short identifier
|
||||
absdir = os.path.abspath(git_dir)
|
||||
if absdir.endswith(os.sep + '.git'):
|
||||
absdir = os.path.dirname(absdir)
|
||||
projectshort = os.path.basename(absdir)
|
||||
if projectshort.endswith(".git"):
|
||||
projectshort = projectshort[:-4]
|
||||
|
||||
return projectshort
|
||||
|
||||
# Return the project description or '' if it is 'Unnamed repository;'
|
||||
def get_project_description():
|
||||
try:
|
||||
git_dir = git.rev_parse(git_dir=True, _quiet=True)
|
||||
except CalledProcessError:
|
||||
die("GIT_DIR not set")
|
||||
|
||||
projectdesc = ''
|
||||
description = os.path.join(git_dir, 'description')
|
||||
if os.path.exists(description):
|
||||
try:
|
||||
projectdesc = open(description).read().strip()
|
||||
except:
|
||||
pass
|
||||
if projectdesc.startswith('Unnamed repository;'):
|
||||
projectdesc = ''
|
||||
|
||||
return projectdesc
|
|
@ -0,0 +1,59 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Standard GNOME pre-receive hook.
|
||||
#
|
||||
# The "pre-receive" hook is invoked just before receive-pack starts to
|
||||
# update refs on the remote repository. Its exit status determines the
|
||||
# success or failure of the update.
|
||||
|
||||
BINDIR=/usr/local/bin/git-bin
|
||||
|
||||
# If the committing user has a homedir with a .gitconfig in it, it we
|
||||
# don't want that to affect our operation. (Should this just be handled
|
||||
# in run-git-or-special-cmd?)
|
||||
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
|
||||
GIT_CONFIG="${GIT_DIR}/config"
|
||||
export GIT_CONFIG
|
||||
|
||||
# Use the directory name with .git stripped as a short identifier
|
||||
absdir=$(cd $GIT_DIR && pwd)
|
||||
projectshort=$(basename ${absdir%.git})
|
||||
|
||||
# Make sure that the user used --exec=import for importing repositories,
|
||||
# and not otherwise. This forces people to be aware of the 'pending'
|
||||
# state.
|
||||
if [ -e "$GIT_DIR/pending" -a -z "$GNOME_GIT_IMPORT" ] ; then
|
||||
cat <<EOF 1>&2
|
||||
---
|
||||
$projectshort is still in the process of being imported. To import
|
||||
into $projectshort use 'git push --exec=import'. If you are done
|
||||
importing, do:
|
||||
|
||||
ssh $USER@git.gnome.org finish-import $projectshort
|
||||
|
||||
---
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ \! -e "$GIT_DIR/pending" -a -n "$GNOME_GIT_IMPORT" ] ; then
|
||||
cat <<EOF 1>&2
|
||||
---
|
||||
$projectshort is no longer in the process of being imported. You
|
||||
can push to $projectshort normally. If you accidentally ran
|
||||
finish-import too early, please contact gitmaster@gnome.org
|
||||
for assistance.
|
||||
----
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
while read oldrev newrev refname; do
|
||||
# Unlike the gnome-post-receive script, where we play fancy games
|
||||
# with 'tee', we invoke the different pre-receive hooks separately
|
||||
# for each ref that is updated. This keeps things simple and
|
||||
# reliable and none of the scripts need all the refs at once.
|
||||
|
||||
$BINDIR/pre-receive-check-policy $oldrev $newrev $refname || exit 1
|
||||
#$BINDIR/pre-receive-check-po $oldrev $newrev $refname || exit 1
|
||||
done
|
|
@ -0,0 +1,26 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Standard GNOME post-receive hook.
|
||||
#
|
||||
# The "post-receive" script is run after receive-pack has accepted a pack
|
||||
# and the repository has been updated. It is passed arguments in through
|
||||
# stdin in the form
|
||||
# <oldrev> <newrev> <refname>
|
||||
# For example:
|
||||
# aa453216d1b3e49e7f6f98441fa56946ddcd6a20 68f7abf4e6f922807889f52bc043ecd31b79f814 refs/heads/master
|
||||
#
|
||||
# git-config options affecting the operation of this script:
|
||||
# hook.emailprefix - should always be empty
|
||||
# hooks.mailinglist - should always be svn-commits-list@gnome.org
|
||||
|
||||
BINDIR=/usr/local/bin/git-bin
|
||||
|
||||
# If the committing user has a homedir with a .gitconfig in it, it we
|
||||
# don't want that to affect our operation. (Should this just be handled
|
||||
# in run-git-or-special-cmd?)
|
||||
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
|
||||
GIT_CONFIG="${GIT_DIR}/config"
|
||||
export GIT_CONFIG
|
||||
|
||||
tee >($BINDIR/post-receive-notify-updates 1>&2) \
|
||||
| $BINDIR/post-receive-email 1>&2
|
|
@ -0,0 +1,896 @@
|
|||
#! /usr/bin/env python
|
||||
#
|
||||
# post-receive-email - Post receive email hook for the Labs Git repository
|
||||
#
|
||||
# Copyright (C) 2008 Owen Taylor
|
||||
# Copyright (C) 2009 Red Hat, Inc
|
||||
# Copyright (C) 2009 Frederic Peters
|
||||
#
|
||||
# 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 program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, If not, see
|
||||
# http://www.gnu.org/licenses/.
|
||||
#
|
||||
# About
|
||||
# =====
|
||||
# This script is used to generate mail to the project list when change
|
||||
# are pushed to the project git repository. It accepts input in the form of
|
||||
# a Git post-receive hook, and generates appropriate emails.
|
||||
#
|
||||
# The attempt here is to provide a maximimally useful and robust output
|
||||
# with as little clutter as possible.
|
||||
#
|
||||
|
||||
import re
|
||||
import os
|
||||
import pwd
|
||||
import sys
|
||||
from email import Header
|
||||
|
||||
script_path = os.path.realpath(os.path.abspath(sys.argv[0]))
|
||||
script_dir = os.path.dirname(script_path)
|
||||
|
||||
sys.path.insert(0, script_dir)
|
||||
|
||||
from git import *
|
||||
from util import die, strip_string as s, start_email, end_email
|
||||
|
||||
# When we put a git subject into the Subject: line, where to truncate
|
||||
SUBJECT_MAX_SUBJECT_CHARS = 100
|
||||
|
||||
CREATE = 0
|
||||
UPDATE = 1
|
||||
DELETE = 2
|
||||
INVALID_TAG = 3
|
||||
|
||||
# Short name for project
|
||||
projectshort = None
|
||||
|
||||
# Human readable name for user, might be None
|
||||
user_fullname = None
|
||||
|
||||
# Who gets the emails
|
||||
recipients = None
|
||||
|
||||
# map of ref_name => Change object; this is used when computing whether
|
||||
# we've previously generated a detailed diff for a commit in the push
|
||||
all_changes = {}
|
||||
processed_changes = {}
|
||||
|
||||
class RefChange(object):
|
||||
def __init__(self, refname, oldrev, newrev):
|
||||
self.refname = refname
|
||||
self.oldrev = oldrev
|
||||
self.newrev = newrev
|
||||
self.cc = set()
|
||||
|
||||
if oldrev == None and newrev != None:
|
||||
self.change_type = CREATE
|
||||
elif oldrev != None and newrev == None:
|
||||
self.change_type = DELETE
|
||||
elif oldrev != None and newrev != None:
|
||||
self.change_type = UPDATE
|
||||
else:
|
||||
self.change_type = INVALID_TAG
|
||||
|
||||
m = re.match(r"refs/[^/]*/(.*)", refname)
|
||||
if m:
|
||||
self.short_refname = m.group(1)
|
||||
else:
|
||||
self.short_refname = refname
|
||||
|
||||
# Do any setup before sending email. The __init__ function should generally
|
||||
# just record the parameters passed in and not do git work. (The main reason
|
||||
# for the split is to let the prepare stage do different things based on
|
||||
# whether other ref updates have been processed or not.)
|
||||
def prepare(self):
|
||||
pass
|
||||
|
||||
# Whether we should generate the normal 'main' email. For simple branch
|
||||
# updates we only generate 'extra' emails
|
||||
def get_needs_main_email(self):
|
||||
return True
|
||||
|
||||
# The XXX in [projectname/XXX], usually a branch
|
||||
def get_project_extra(self):
|
||||
return None
|
||||
|
||||
# Return the subject for the main email, without the leading [projectname]
|
||||
def get_subject(self):
|
||||
raise NotImplemenetedError()
|
||||
|
||||
# Write the body of the main email to the given file object
|
||||
def generate_body(self, out):
|
||||
raise NotImplemenetedError()
|
||||
|
||||
def generate_header(self, out, subject, include_revs=True, oldrev=None, newrev=None, cc=None):
|
||||
user = os.environ['USER']
|
||||
if user_fullname:
|
||||
from_address = "%s <%s@entrouvert.com>" % (user_fullname, user)
|
||||
else:
|
||||
from_address = "%s@entrouvert.com" % (user)
|
||||
|
||||
if cc is None:
|
||||
cc = self.cc
|
||||
|
||||
print >>out, s("""
|
||||
To: %(recipients)s
|
||||
Cc: %(cc)s
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
From: %(from_address)s
|
||||
Subject: %(subject)s
|
||||
Keywords: %(projectshort)s
|
||||
X-Git-Refname: %(refname)s
|
||||
""") % {
|
||||
'recipients': recipients,
|
||||
'cc': ','.join(cc),
|
||||
'from_address': from_address,
|
||||
'subject': subject,
|
||||
'projectshort': projectshort,
|
||||
'refname': self.refname
|
||||
}
|
||||
|
||||
if include_revs:
|
||||
if oldrev:
|
||||
oldrev = oldrev
|
||||
else:
|
||||
oldrev = NULL_REVISION
|
||||
if newrev:
|
||||
newrev = newrev
|
||||
else:
|
||||
newrev = NULL_REVISION
|
||||
|
||||
print >>out, s("""
|
||||
X-Git-Oldrev: %(oldrev)s
|
||||
X-Git-Newrev: %(newrev)s
|
||||
""") % {
|
||||
'oldrev': oldrev,
|
||||
'newrev': newrev,
|
||||
}
|
||||
|
||||
# Trailing newline to signal the end of the header
|
||||
print >>out
|
||||
|
||||
def send_main_email(self):
|
||||
if not self.get_needs_main_email():
|
||||
return
|
||||
|
||||
extra = self.get_project_extra()
|
||||
if extra:
|
||||
extra = "/" + extra
|
||||
else:
|
||||
extra = ""
|
||||
subject = "[" + projectshort + extra + "] " + self.get_subject()
|
||||
|
||||
email_out = start_email()
|
||||
|
||||
self.generate_header(email_out, subject, include_revs=True, oldrev=self.oldrev, newrev=self.newrev)
|
||||
self.generate_body(email_out)
|
||||
|
||||
end_email()
|
||||
|
||||
# Allow multiple emails to be sent - used for branch updates
|
||||
def send_extra_emails(self):
|
||||
pass
|
||||
|
||||
def send_emails(self):
|
||||
self.send_main_email()
|
||||
self.send_extra_emails()
|
||||
|
||||
# ========================
|
||||
|
||||
# Common baseclass for BranchCreation and BranchUpdate (but not BranchDeletion)
|
||||
class BranchChange(RefChange):
|
||||
def __init__(self, *args):
|
||||
RefChange.__init__(self, *args)
|
||||
|
||||
def prepare(self):
|
||||
# We need to figure out what commits are referenced in this commit thta
|
||||
# weren't previously referenced in the repository by another branch.
|
||||
# "Previously" here means either before this push, or by branch updates
|
||||
# we've already done in this push. These are the commits we'll send
|
||||
# out individual mails for.
|
||||
#
|
||||
# Note that "Before this push" can't be gotten exactly right since an
|
||||
# push is only atomic per-branch and there is no locking across branches.
|
||||
# But new commits will always show up in a cover mail in any case; even
|
||||
# someone who maliciously is trying to fool us can't hide all trace.
|
||||
|
||||
# Ordering matters here, so we can't rely on kwargs
|
||||
branches = git.rev_parse('--symbolic-full-name', '--branches', _split_lines=True)
|
||||
detailed_commit_args = [ self.newrev ]
|
||||
|
||||
for branch in branches:
|
||||
if branch == self.refname:
|
||||
# For this branch, exclude commits before 'oldrev'
|
||||
if self.change_type != CREATE:
|
||||
detailed_commit_args.append("^" + self.oldrev)
|
||||
elif branch in all_changes and not branch in processed_changes:
|
||||
# For branches that were updated in this push but we haven't processed
|
||||
# yet, exclude commits before their old revisions
|
||||
detailed_commit_args.append("^" + all_changes[branch].oldrev)
|
||||
else:
|
||||
# Exclude commits that are ancestors of all other branches
|
||||
detailed_commit_args.append("^" + branch)
|
||||
|
||||
detailed_commits = git.rev_list(*detailed_commit_args).splitlines()
|
||||
|
||||
self.detailed_commits = set()
|
||||
for id in detailed_commits:
|
||||
self.detailed_commits.add(id)
|
||||
|
||||
# Find the commits that were added and removed, reverse() to get
|
||||
# chronological order
|
||||
if self.change_type == CREATE:
|
||||
# If someone creates a branch of GTK+, we don't want to list (or even walk through)
|
||||
# all 30,000 commits in the history as "new commits" on the branch. So we start
|
||||
# the commit listing from the first commit we are going to send a mail out about.
|
||||
#
|
||||
# This does mean that if someone creates a branch, merges it, and then pushes
|
||||
# both the branch and what was merged into at once, then the resulting mails will
|
||||
# be a bit strange (depending on ordering) - the mail for the creation of the
|
||||
# branch may look like it was created in the finished state because all the commits
|
||||
# have been already mailed out for the other branch. I don't think this is a big
|
||||
# problem, and the best way to fix it would be to sort the ref updates so that the
|
||||
# branch creation was processed first.
|
||||
#
|
||||
if len(detailed_commits) > 0:
|
||||
# Verify parent of first detailed commit is valid. On initial push, it is not.
|
||||
parent = detailed_commits[-1] + "^"
|
||||
try:
|
||||
validref = git.rev_parse(parent, _quiet=True)
|
||||
except CalledProcessError:
|
||||
self.added_commits = []
|
||||
else:
|
||||
self.added_commits = rev_list_commits(parent + ".." + self.newrev)
|
||||
self.added_commits.reverse()
|
||||
else:
|
||||
self.added_commits = []
|
||||
self.removed_commits = []
|
||||
else:
|
||||
self.added_commits = rev_list_commits(self.oldrev + ".." + self.newrev)
|
||||
self.added_commits.reverse()
|
||||
self.removed_commits = rev_list_commits(self.newrev + ".." + self.oldrev)
|
||||
self.removed_commits.reverse()
|
||||
|
||||
# In some cases we'll send a cover email that describes the overall
|
||||
# change to the branch before ending individual mails for commits. In other
|
||||
# cases, we just send the individual emails. We generate a cover mail:
|
||||
#
|
||||
# - If it's a branch creation
|
||||
# - If it's not a fast forward
|
||||
# - If there are any merge commits
|
||||
# - If there are any commits we won't send separately (already in repo)
|
||||
|
||||
have_merge_commits = False
|
||||
for commit in self.added_commits:
|
||||
if commit_is_merge(commit):
|
||||
have_merge_commits = True
|
||||
|
||||
self.needs_cover_email = (self.change_type == CREATE or
|
||||
len(self.removed_commits) > 0 or
|
||||
have_merge_commits or
|
||||
len(self.detailed_commits) < len(self.added_commits))
|
||||
|
||||
def get_needs_main_email(self):
|
||||
return self.needs_cover_email
|
||||
|
||||
# A prefix for the cover letter summary with the number of added commits
|
||||
def get_count_string(self):
|
||||
if len(self.added_commits) > 1:
|
||||
return "(%d commits) " % len(self.added_commits)
|
||||
else:
|
||||
return ""
|
||||
|
||||
# Generate a short listing for a series of commits
|
||||
# show_details - whether we should mark commit where we aren't going to send
|
||||
# a detailed email. (Set the False when listing removed commits)
|
||||
def generate_commit_summary(self, out, commits, show_details=True):
|
||||
detail_note = False
|
||||
for commit in commits:
|
||||
if show_details and not commit.id in self.detailed_commits:
|
||||
detail = " (*)"
|
||||
detail_note = True
|
||||
else:
|
||||
detail = ""
|
||||
print >>out, " " + commit_oneline(commit) + detail
|
||||
|
||||
if detail_note:
|
||||
print >>out
|
||||
print >>out, "(*) This commit already existed in another branch; no separate mail sent"
|
||||
|
||||
def send_extra_emails(self):
|
||||
total = len(self.added_commits)
|
||||
|
||||
for i, commit in enumerate(self.added_commits):
|
||||
if not commit.id in self.detailed_commits:
|
||||
continue
|
||||
|
||||
email_out = start_email()
|
||||
|
||||
if self.short_refname == 'master':
|
||||
branch = ""
|
||||
else:
|
||||
branch = "/" + self.short_refname
|
||||
|
||||
total = len(self.added_commits)
|
||||
if total > 1 and self.needs_cover_email:
|
||||
count_string = ": %(index)s/%(total)s" % {
|
||||
'index' : i + 1,
|
||||
'total' : total
|
||||
}
|
||||
else:
|
||||
count_string = ""
|
||||
|
||||
subject = "[%(projectshort)s%(branch)s%(count_string)s] %(subject)s" % {
|
||||
'projectshort' : projectshort,
|
||||
'branch' : branch,
|
||||
'count_string' : count_string,
|
||||
'subject' : commit.subject[0:SUBJECT_MAX_SUBJECT_CHARS]
|
||||
}
|
||||
|
||||
# If there is a cover email, it has the X-Git-OldRev/X-Git-NewRev in it
|
||||
# for the total branch update. Without a cover email, we are conceptually
|
||||
# breaking up the update into individual updates for each commit
|
||||
if self.needs_cover_email:
|
||||
self.generate_header(email_out, subject, include_revs=False, cc=[])
|
||||
else:
|
||||
parent = git.rev_parse(commit.id + "^")
|
||||
self.generate_header(email_out, subject,
|
||||
include_revs=True,
|
||||
oldrev=parent, newrev=commit.id)
|
||||
|
||||
email_out.flush()
|
||||
git.show(commit.id, M=True, stat=True, _outfile=email_out)
|
||||
email_out.flush()
|
||||
git.show(commit.id, p=True, M=True, diff_filter="ACMRTUXB", pretty="format:---", _outfile=email_out)
|
||||
end_email()
|
||||
|
||||
class BranchCreation(BranchChange):
|
||||
def __init__(self, *args):
|
||||
BranchChange.__init__(self, *args)
|
||||
|
||||
def get_subject(self):
|
||||
return self.get_count_string() + "Created branch " + self.short_refname
|
||||
|
||||
def generate_body(self, out):
|
||||
if len(self.added_commits) > 0:
|
||||
print >>out, s("""
|
||||
The branch '%(short_refname)s' was created.
|
||||
|
||||
Summary of new commits:
|
||||
|
||||
""") % {
|
||||
'short_refname': self.short_refname,
|
||||
}
|
||||
|
||||
self.generate_commit_summary(out, self.added_commits)
|
||||
else:
|
||||
print >>out, s("""
|
||||
The branch '%(short_refname)s' was created pointing to:
|
||||
|
||||
%(commit_oneline)s
|
||||
|
||||
""") % {
|
||||
'short_refname': self.short_refname,
|
||||
'commit_oneline': commit_oneline(self.newrev)
|
||||
}
|
||||
|
||||
class BranchUpdate(BranchChange):
|
||||
def get_project_extra(self):
|
||||
if len(self.removed_commits) > 0:
|
||||
# In the non-fast-forward-case, the branch name is in the subject
|
||||
return None
|
||||
else:
|
||||
if self.short_refname == 'master':
|
||||
# Not saying 'master' all over the place reduces clutter
|
||||
return None
|
||||
else:
|
||||
return self.short_refname
|
||||
|
||||
def get_subject(self):
|
||||
if len(self.removed_commits) > 0:
|
||||
return self.get_count_string() + "Non-fast-forward update to branch " + self.short_refname
|
||||
else:
|
||||
# We want something for useful for the subject than "Updates to branch spiffy-stuff".
|
||||
# The common case where we have a cover-letter for a fast-forward branch
|
||||
# update is a merge. So we try to get:
|
||||
#
|
||||
# [myproject/spiffy-stuff] (18 commits) ...Merge branch master
|
||||
#
|
||||
last_commit = self.added_commits[-1]
|
||||
if len(self.added_commits) > 1:
|
||||
return self.get_count_string() + "..." + last_commit.subject[0:SUBJECT_MAX_SUBJECT_CHARS]
|
||||
else:
|
||||
# The ... indicates we are only showing one of many, don't need it for a single commit
|
||||
return last_commit.subject[0:SUBJECT_MAX_SUBJECT_CHARS]
|
||||
|
||||
def generate_body_normal(self, out):
|
||||
print >>out, s("""
|
||||
Summary of changes:
|
||||
|
||||
""")
|
||||
|
||||
self.generate_commit_summary(out, self.added_commits)
|
||||
|
||||
def generate_body_non_fast_forward(self, out):
|
||||
print >>out, s("""
|
||||
The branch '%(short_refname)s' was changed in a way that was not a fast-forward update.
|
||||
NOTE: This may cause problems for people pulling from the branch. For more information,
|
||||
please see:
|
||||
|
||||
http://live.gnome.org/Git/Help/NonFastForward
|
||||
|
||||
Commits removed from the branch:
|
||||
|
||||
""") % {
|
||||
'short_refname': self.short_refname,
|
||||
}
|
||||
|
||||
self.generate_commit_summary(out, self.removed_commits, show_details=False)
|
||||
|
||||
print >>out, s("""
|
||||
|
||||
Commits added to the branch:
|
||||
|
||||
""")
|
||||
self.generate_commit_summary(out, self.added_commits)
|
||||
|
||||
def generate_body(self, out):
|
||||
if len(self.removed_commits) == 0:
|
||||
self.generate_body_normal(out)
|
||||
else:
|
||||
self.generate_body_non_fast_forward(out)
|
||||
|
||||
class BranchDeletion(RefChange):
|
||||
def get_subject(self):
|
||||
return "Deleted branch " + self.short_refname
|
||||
|
||||
def generate_body(self, out):
|
||||
print >>out, s("""
|
||||
The branch '%(short_refname)s' was deleted.
|
||||
""") % {
|
||||
'short_refname': self.short_refname,
|
||||
}
|
||||
|
||||
# ========================
|
||||
|
||||
class AnnotatedTagChange(RefChange):
|
||||
def __init__(self, *args):
|
||||
RefChange.__init__(self, *args)
|
||||
|
||||
def prepare(self):
|
||||
# Resolve tag to commit
|
||||
if self.oldrev:
|
||||
self.old_commit_id = git.rev_parse(self.oldrev + "^{commit}")
|
||||
|
||||
if self.newrev:
|
||||
self.parse_tag_object(self.newrev)
|
||||
else:
|
||||
self.parse_tag_object(self.oldrev)
|
||||
|
||||
# Parse information out of the tag object
|
||||
def parse_tag_object(self, revision):
|
||||
message_lines = []
|
||||
in_message = False
|
||||
|
||||
# A bit of paranoia if we fail at parsing; better to make the failure
|
||||
# visible than just silently skip Tagger:/Date:.
|
||||
self.tagger = "unknown <unknown@example.com>"
|
||||
self.date = "at an unknown time"
|
||||
|
||||
self.have_signature = False
|
||||
for line in git.cat_file(revision, p=True, _split_lines=True):
|
||||
if in_message:
|
||||
# Nobody is going to verify the signature by extracting it
|
||||
# from the email, so strip it, and remember that we saw it
|
||||
# by saying 'signed tag'
|
||||
if re.match(r'-----BEGIN PGP SIGNATURE-----', line):
|
||||
self.have_signature = True
|
||||
break
|
||||
message_lines.append(line)
|
||||
else:
|
||||
if line.strip() == "":
|
||||
in_message = True
|
||||
continue
|
||||
# I don't know what a more robust rule is for dividing the
|
||||
# name and date, other than maybe looking explicitly for a
|
||||
# RFC 822 date. This seems to work pretty well
|
||||
m = re.match(r"tagger\s+([^>]*>)\s*(.*)", line)
|
||||
if m:
|
||||
self.tagger = m.group(1)
|
||||
self.date = m.group(2)
|
||||
continue
|
||||
self.message = "\n".join([" " + line for line in message_lines])
|
||||
|
||||
# Outputs information about the new tag
|
||||
def generate_tag_info(self, out):
|
||||
|
||||
print >>out, s("""
|
||||
Tagger: %(tagger)s
|
||||
Date: %(date)s
|
||||
|
||||
%(message)s
|
||||
|
||||
""") % {
|
||||
'tagger': self.tagger,
|
||||
'date': self.date,
|
||||
'message': self.message,
|
||||
}
|
||||
|
||||
# We take the creation of an annotated tag as being a "mini-release-announcement"
|
||||
# and show a 'git shortlog' of the changes since the last tag that was an
|
||||
# ancestor of the new tag.
|
||||
last_tag = None
|
||||
try:
|
||||
# A bit of a hack to get that previous tag
|
||||
last_tag = git.describe(self.newrev+"^", abbrev='0', _quiet=True)
|
||||
except CalledProcessError:
|
||||
# Assume that this means no older tag
|
||||
pass
|
||||
|
||||
if last_tag:
|
||||
revision_range = last_tag + ".." + self.newrev
|
||||
print >>out, s("""
|
||||
Changes since the last tag '%(last_tag)s':
|
||||
|
||||
""") % {
|
||||
'last_tag': last_tag
|
||||
}
|
||||
else:
|
||||
revision_range = self.newrev
|
||||
print >>out, s("""
|
||||
Changes:
|
||||
|
||||
""")
|
||||
out.write(git.shortlog(revision_range))
|
||||
out.write("\n")
|
||||
|
||||
def get_tag_type(self):
|
||||
if self.have_signature:
|
||||
return 'signed tag'
|
||||
else:
|
||||
return 'unsigned tag'
|
||||
|
||||
class AnnotatedTagCreation(AnnotatedTagChange):
|
||||
def get_subject(self):
|
||||
return "Created tag " + self.short_refname
|
||||
|
||||
def generate_body(self, out):
|
||||
print >>out, s("""
|
||||
The %(tag_type)s '%(short_refname)s' was created.
|
||||
|
||||
""") % {
|
||||
'tag_type': self.get_tag_type(),
|
||||
'short_refname': self.short_refname,
|
||||
}
|
||||
self.generate_tag_info(out)
|
||||
|
||||
class AnnotatedTagDeletion(AnnotatedTagChange):
|
||||
def get_subject(self):
|
||||
return "Deleted tag " + self.short_refname
|
||||
|
||||
def generate_body(self, out):
|
||||
print >>out, s("""
|
||||
The %(tag_type)s '%(short_refname)s' was deleted. It previously pointed to:
|
||||
|
||||
%(old_commit_oneline)s
|
||||
""") % {
|
||||
'tag_type': self.get_tag_type(),
|
||||
'short_refname': self.short_refname,
|
||||
'old_commit_oneline': commit_oneline(self.old_commit_id)
|
||||
}
|
||||
|
||||
class AnnotatedTagUpdate(AnnotatedTagChange):
|
||||
def get_subject(self):
|
||||
return "Updated tag " + self.short_refname
|
||||
|
||||
def generate_body(self, out):
|
||||
print >>out, s("""
|
||||
The tag '%(short_refname)s' was replaced with a new tag. It previously
|
||||
pointed to:
|
||||
|
||||
%(old_commit_oneline)s
|
||||
|
||||
NOTE: People pulling from the repository will not get the new tag.
|
||||
For more information, please see:
|
||||
|
||||
http://live.gnome.org/Git/Help/TagUpdates
|
||||
|
||||
New tag information:
|
||||
|
||||
""") % {
|
||||
'short_refname': self.short_refname,
|
||||
'old_commit_oneline': commit_oneline(self.old_commit_id),
|
||||
}
|
||||
self.generate_tag_info(out)
|
||||
|
||||
# ========================
|
||||
|
||||
class LightweightTagCreation(RefChange):
|
||||
def get_subject(self):
|
||||
return "Created tag " + self.short_refname
|
||||
|
||||
def generate_body(self, out):
|
||||
print >>out, s("""
|
||||
The lightweight tag '%(short_refname)s' was created pointing to:
|
||||
|
||||
%(commit_oneline)s
|
||||
""") % {
|
||||
'short_refname': self.short_refname,
|
||||
'commit_oneline': commit_oneline(self.newrev)
|
||||
}
|
||||
|
||||
class LightweightTagDeletion(RefChange):
|
||||
def get_subject(self):
|
||||
return "Deleted tag " + self.short_refname
|
||||
|
||||
def generate_body(self, out):
|
||||
print >>out, s("""
|
||||
The lighweight tag '%(short_refname)s' was deleted. It previously pointed to:
|
||||
|
||||
%(commit_oneline)s
|
||||
""") % {
|
||||
'short_refname': self.short_refname,
|
||||
'commit_oneline': commit_oneline(self.oldrev)
|
||||
}
|
||||
|
||||
class LightweightTagUpdate(RefChange):
|
||||
def get_subject(self):
|
||||
return "Updated tag " + self.short_refname
|
||||
|
||||
def generate_body(self, out):
|
||||
print >>out, s("""
|
||||
The lightweight tag '%(short_refname)s' was updated to point to:
|
||||
|
||||
%(commit_oneline)s
|
||||
|
||||
It previously pointed to:
|
||||
|
||||
%(old_commit_oneline)s
|
||||
|
||||
NOTE: People pulling from the repository will not get the new tag.
|
||||
For more information, please see:
|
||||
|
||||
http://live.gnome.org/Git/Help/TagUpdates
|
||||
""") % {
|
||||
'short_refname': self.short_refname,
|
||||
'commit_oneline': commit_oneline(self.newrev),
|
||||
'old_commit_oneline': commit_oneline(self.oldrev)
|
||||
}
|
||||
|
||||
# ========================
|
||||
|
||||
class InvalidRefDeletion(RefChange):
|
||||
def get_subject(self):
|
||||
return "Deleted invalid ref " + self.refname
|
||||
|
||||
def generate_body(self, out):
|
||||
print >>out, s("""
|
||||
The ref '%(refname)s' was deleted. It previously pointed nowhere.
|
||||
""") % {
|
||||
'refname': self.refname,
|
||||
}
|
||||
|
||||
# ========================
|
||||
|
||||
class MiscChange(RefChange):
|
||||
def __init__(self, refname, oldrev, newrev, message):
|
||||
RefChange.__init__(self, refname, oldrev, newrev)
|
||||
self.message = message
|
||||
|
||||
class MiscCreation(MiscChange):
|
||||
def get_subject(self):
|
||||
return "Unexpected: Created " + self.refname
|
||||
|
||||
def generate_body(self, out):
|
||||
print >>out, s("""
|
||||
The ref '%(refname)s' was created pointing to:
|
||||
|
||||
%(newrev)s
|
||||
|
||||
This is unexpected because:
|
||||
|
||||
%(message)s
|
||||
""") % {
|
||||
'refname': self.refname,
|
||||
'newrev': self.newrev,
|
||||
'message': self.message
|
||||
}
|
||||
|
||||
class MiscDeletion(MiscChange):
|
||||
def get_subject(self):
|
||||
return "Unexpected: Deleted " + self.refname
|
||||
|
||||
def generate_body(self, out):
|
||||
print >>out, s("""
|
||||
The ref '%(refname)s' was deleted. It previously pointed to:
|
||||
|
||||
%(oldrev)s
|
||||
|
||||
This is unexpected because:
|
||||
|
||||
%(message)s
|
||||
""") % {
|
||||
'refname': self.refname,
|
||||
'oldrev': self.oldrev,
|
||||
'message': self.message
|
||||
}
|
||||
|
||||
class MiscUpdate(MiscChange):
|
||||
def get_subject(self):
|
||||
return "Unexpected: Updated " + self.refname
|
||||
|
||||
def generate_body(self, out):
|
||||
print >>out, s("""
|
||||
The ref '%(refname)s' was updated from:
|
||||
|
||||
%(newrev)s
|
||||
|
||||
To:
|
||||
|
||||
%(oldrev)s
|
||||
|
||||
This is unexpected because:
|
||||
|
||||
%(message)s
|
||||
""") % {
|
||||
'refname': self.refname,
|
||||
'oldrev': self.oldrev,
|
||||
'newrev': self.newrev,
|
||||
'message': self.message
|
||||
}
|
||||
|
||||
# ========================
|
||||
|
||||
def make_change(oldrev, newrev, refname):
|
||||
refname = refname
|
||||
|
||||
# Canonicalize
|
||||
oldrev = git.rev_parse(oldrev)
|
||||
newrev = git.rev_parse(newrev)
|
||||
|
||||
# Replacing the null revision with None makes it easier for us to test
|
||||
# in subsequent code
|
||||
|
||||
if re.match(r'^0+$', oldrev):
|
||||
oldrev = None
|
||||
else:
|
||||
oldrev = oldrev
|
||||
|
||||
if re.match(r'^0+$', newrev):
|
||||
newrev = None
|
||||
else:
|
||||
newrev = newrev
|
||||
|
||||
# Figure out what we are doing to the ref
|
||||
|
||||
if oldrev == None and newrev != None:
|
||||
change_type = CREATE
|
||||
target = newrev
|
||||
elif oldrev != None and newrev == None:
|
||||
change_type = DELETE
|
||||
target = oldrev
|
||||
elif oldrev != None and newrev != None:
|
||||
change_type = UPDATE
|
||||
target = newrev
|
||||
else:
|
||||
return InvalidRefDeletion(refname, oldrev, newrev)
|
||||
|
||||
object_type = git.cat_file(target, t=True)
|
||||
|
||||
# And then create the right type of change object
|
||||
|
||||
# Closing the arguments like this simplifies the following code
|
||||
def make(cls, *args):
|
||||
return cls(refname, oldrev, newrev, *args)
|
||||
|
||||
def make_misc_change(message):
|
||||
if change_type == CREATE:
|
||||
return make(MiscCreation, message)
|
||||
elif change_type == DELETE:
|
||||
return make(MiscDeletion, message)
|
||||
else:
|
||||
return make(MiscUpdate, message)
|
||||
|
||||
if re.match(r'^refs/tags/.*$', refname):
|
||||
if object_type == 'commit':
|
||||
if change_type == CREATE:
|
||||
return make(LightweightTagCreation)
|
||||
elif change_type == DELETE:
|
||||
return make(LightweightTagDeletion)
|
||||
else:
|
||||
return make(LightweightTagUpdate)
|
||||
elif object_type == 'tag':
|
||||
if change_type == CREATE:
|
||||
return make(AnnotatedTagCreation)
|
||||
elif change_type == DELETE:
|
||||
return make(AnnotatedTagDeletion)
|
||||
else:
|
||||
return make(AnnotatedTagUpdate)
|
||||
else:
|
||||
return make_misc_change("%s is not a commit or tag object" % target)
|
||||
elif re.match(r'^refs/heads/.*$', refname):
|
||||
if object_type == 'commit':
|
||||
if change_type == CREATE:
|
||||
return make(BranchCreation)
|
||||
elif change_type == DELETE:
|
||||
return make(BranchDeletion)
|
||||
else:
|
||||
return make(BranchUpdate)
|
||||
else:
|
||||
return make_misc_change("%s is not a commit object" % target)
|
||||
elif re.match(r'^refs/remotes/.*$', refname):
|
||||
return make_misc_change("'%s' is a tracking branch and doesn't belong on the server" % refname)
|
||||
else:
|
||||
return make_misc_change("'%s' is not in refs/heads/ or refs/tags/" % refname)
|
||||
|
||||
def main():
|
||||
global projectshort
|
||||
global user_fullname
|
||||
global recipients
|
||||
|
||||
# No emails for a repository in the process of being imported
|
||||
git_dir = git.rev_parse(git_dir=True, _quiet=True)
|
||||
if os.path.exists(os.path.join(git_dir, 'pending')):
|
||||
return
|
||||
|
||||
projectshort = get_module_name()
|
||||
|
||||
try:
|
||||
recipients=git.config("hooks.mailinglist", _quiet=True)
|
||||
except CalledProcessError:
|
||||
pass
|
||||
|
||||
if not recipients:
|
||||
sys.exit(0)
|
||||
|
||||
# Figure out a human-readable username
|
||||
try:
|
||||
entry = pwd.getpwuid(os.getuid())
|
||||
gecos = entry.pw_gecos
|
||||
except:
|
||||
gecos = None
|
||||
|
||||
if gecos != None:
|
||||
# Typical GNOME account have John Doe <john.doe@example.com> for the GECOS.
|
||||
# Comma-separated fields are also possible
|
||||
m = re.match("([^,<]+)", gecos)
|
||||
if m:
|
||||
fullname = m.group(1).strip()
|
||||
if fullname != "":
|
||||
user_fullname = fullname
|
||||
|
||||
changes = []
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
# For testing purposes, allow passing in a ref update on the command line
|
||||
if len(sys.argv) != 4:
|
||||
die("Usage: generate-commit-mail OLDREV NEWREV REFNAME")
|
||||
changes.append(make_change(sys.argv[1], sys.argv[2], sys.argv[3]))
|
||||
else:
|
||||
for line in sys.stdin:
|
||||
items = line.strip().split()
|
||||
if len(items) != 3:
|
||||
die("Input line has unexpected number of items")
|
||||
changes.append(make_change(items[0], items[1], items[2]))
|
||||
|
||||
for change in changes:
|
||||
all_changes[change.refname] = change
|
||||
|
||||
for change in changes:
|
||||
change.prepare()
|
||||
change.send_emails()
|
||||
processed_changes[change.refname] = change
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,117 @@
|
|||
#!/usr/bin/python
|
||||
#
|
||||
# post-receive-notify-updates
|
||||
#
|
||||
# Copyright (C) 2008 Owen Taylor
|
||||
# Copyright (C) 2009 Red Hat, Inc
|
||||
# Copyright (C) 2009 Frederic Peters
|
||||
#
|
||||
# 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 program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, If not, see
|
||||
# http://www.gnu.org/licenses/.
|
||||
#
|
||||
# About
|
||||
# =====
|
||||
# This script is used to send out notification mails on branch updates; these
|
||||
# notification mails are typically used to trigger automated rebuilds
|
||||
#
|
||||
|
||||
import re
|
||||
import os
|
||||
import pwd
|
||||
import sys
|
||||
|
||||
script_path = os.path.realpath(os.path.abspath(sys.argv[0]))
|
||||
script_dir = os.path.dirname(script_path)
|
||||
|
||||
sys.path.insert(0, script_dir)
|
||||
|
||||
from git import *
|
||||
from util import die, strip_string as s, start_email, end_email
|
||||
|
||||
# These addresses always get notified
|
||||
ALL_MODULE_RECIPIENTS = []
|
||||
|
||||
BRANCH_RE = re.compile(r'^refs/heads/(.*)$')
|
||||
def get_branch_name(refname):
|
||||
m = BRANCH_RE.match(refname)
|
||||
if m:
|
||||
return m.group(1)
|
||||
|
||||
SPLIT_RE = re.compile("\s*,\s*")
|
||||
|
||||
def main():
|
||||
module_name = get_module_name()
|
||||
|
||||
try:
|
||||
recipients_s = git.config("hooks.update-recipients", _quiet=True)
|
||||
except CalledProcessError:
|
||||
recipients_s = ""
|
||||
|
||||
recipients_s = recipients_s.strip()
|
||||
if recipients_s == "":
|
||||
recipients = []
|
||||
else:
|
||||
recipients = SPLIT_RE.split(recipients_s)
|
||||
|
||||
for recipient in ALL_MODULE_RECIPIENTS:
|
||||
if not recipient in recipients:
|
||||
recipients.append(recipient)
|
||||
|
||||
changes = []
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
# For testing purposes, allow passing in a ref update on the command line
|
||||
if len(sys.argv) != 4:
|
||||
die("Usage: post-receive-notify-updates OLDREV NEWREV REFNAME")
|
||||
branch_name = get_branch_name(sys.argv[3])
|
||||
if branch_name is not None:
|
||||
changes.append((branch_name, sys.argv[1], sys.argv[2], sys.argv[3]))
|
||||
else:
|
||||
for line in sys.stdin:
|
||||
items = line.strip().split()
|
||||
if len(items) != 3:
|
||||
die("Input line has unexpected number of items")
|
||||
branch_name = get_branch_name(items[2])
|
||||
if branch_name is not None:
|
||||
changes.append((branch_name, items[0], items[1], items[2]))
|
||||
|
||||
if len(changes) == 0:
|
||||
# Nothing to mail about
|
||||
return
|
||||
|
||||
branches = [branch_name for branch_name, _, _, _ in changes]
|
||||
subject = module_name + " " + " ".join(branches)
|
||||
|
||||
if recipients:
|
||||
out = start_email()
|
||||
|
||||
print >>out, s("""
|
||||
To: %(recipients)s
|
||||
From: noreply@entrouvert.org
|
||||
Subject: %(subject)s
|
||||
""") % {
|
||||
'recipients': ", ".join(recipients),
|
||||
'subject': subject
|
||||
}
|
||||
|
||||
# Trailing newline to signal the end of the header
|
||||
print >>out
|
||||
|
||||
for _, oldrev, newrev, refname in changes:
|
||||
print >>out, oldrev, newrev, refname
|
||||
|
||||
end_email()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,72 @@
|
|||
#!/bin/bash
|
||||
|
||||
# This checks to see if the module has maintainer information. Maintainer
|
||||
# information can be provided one of two ways:
|
||||
#
|
||||
# - A <modulename>.doap file
|
||||
# - A MAINTAINERS file (deprecated)
|
||||
|
||||
BINDIR=/home/admin/gitadmin-bin
|
||||
|
||||
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
|
||||
|
||||
# Use the directory name with .git stripped as a short identifier
|
||||
absdir=$(cd $GIT_DIR && pwd)
|
||||
projectshort=$(basename ${absdir%.git})
|
||||
|
||||
check_maintainers() {
|
||||
oldrev=$1
|
||||
newrev=$2
|
||||
refname=$3
|
||||
|
||||
branchname=${refname#refs/heads/}
|
||||
if [ "$branchname" = "$refname" ] ; then
|
||||
# not a branch update
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$branchname" != "master" ] ; then
|
||||
# maintainer info only required on the master branch
|
||||
return 0
|
||||
fi
|
||||
|
||||
if expr $oldrev : "^0\+$" > /dev/null 2>&1; then
|
||||
# Don't require maintainer info for initial imports; keeps things simple
|
||||
return 0
|
||||
fi
|
||||
|
||||
if expr $newrev : "^0\+$" > /dev/null 2>&1; then
|
||||
# Branch deletion; (shouldn't really happen for the master branch)
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! git diff-tree --name-only -r $oldrev $newrev | grep -q -v '\(LINGUAS\|ChangeLog\|.po\)$' ; then
|
||||
# Looks like something a translator would do, exempt it from the check
|
||||
return 0
|
||||
fi
|
||||
|
||||
if git cat-file -e $newrev:$projectshort.doap 2>/dev/null ; then
|
||||
# There's a DOAP file. For performance reasons and to allow having fairly
|
||||
# strict validation without being annoying, we only validate the DOAP file
|
||||
# if it changed
|
||||
if git diff-tree --name-only -r $oldrev $newrev | grep -q $projectshort.doap ; then
|
||||
if ! git cat-file blob $newrev:$projectshort.doap | $BINDIR/validate-doap $projectshort ; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# See if there is a old-style MAINTAINERS file
|
||||
if ! git cat-file blob $newrev:MAINTAINERS 2>/dev/null | /bin/grep -q "^Userid:" ; then
|
||||
echo "A valid $projectshort.doap file is required. See http://live.gnome.org/MaintainersCorner#maintainers" >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
if [ $# = 3 ] ; then
|
||||
check_maintainers $@ || exit 1
|
||||
else
|
||||
while read oldrev newrev refname; do
|
||||
check_maintainers $oldrev $newrev $refname || exit 1
|
||||
done
|
||||
fi
|
|
@ -0,0 +1,141 @@
|
|||
#!/bin/bash
|
||||
|
||||
check_po() {
|
||||
rev=$1
|
||||
path=$2
|
||||
mode=$3
|
||||
|
||||
case "$path" in
|
||||
*.po)
|
||||
;;
|
||||
*)
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
basename=`basename $path`
|
||||
|
||||
# Perform extensive tests. This could be disabled if GNOME .po
|
||||
# files used newer features than those available on git.gnome.org.
|
||||
dash_c="-c"
|
||||
|
||||
# Parse the file and check for errors
|
||||
result=`git cat-file blob "$rev:$path" | msgfmt $dash_c -o /dev/null - 2>&1`
|
||||
if [ $? -gt 0 ]; then
|
||||
cat <<EOF >&2
|
||||
---
|
||||
The following translation (.po) file appears to be invalid.$branch_message
|
||||
|
||||
$path
|
||||
|
||||
The results of the validation follow. Please correct the errors on the line numbers mentioned and try to push again.
|
||||
|
||||
$result
|
||||
|
||||
To check this locally before attempting to push again, you can use the following command:
|
||||
|
||||
msgfmt $dash_c $basename
|
||||
|
||||
After making fixes, modify your commit to include them, by doing:
|
||||
|
||||
git add $basename
|
||||
git commit --amend
|
||||
|
||||
If you have any further problems or questions, please contact the GNOME Translation Project mailing list <gnome-i18n@gnome.org>. Thank you.
|
||||
---
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for the absence of an executable flag
|
||||
if expr "$mode" : ".*\([1357]..\|[1357].\|[1357]\)$" > /dev/null 2>& 1; then
|
||||
cat <<EOF >&2
|
||||
---
|
||||
The following translation file appears to have its executable flag set.$branch_message
|
||||
|
||||
$path
|
||||
|
||||
Translation files should not be executable. Please remove the flag and try to push again. The following commands may help:
|
||||
|
||||
chmod a-x $basename
|
||||
git add $basename
|
||||
git commit --amend
|
||||
|
||||
If you have any further problems or questions, please contact the GNOME Translation Project mailing list <gnome-i18n@gnome.org>. Thank you.
|
||||
---
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_pos() {
|
||||
oldrev=$1
|
||||
newrev=$2
|
||||
refname=$3
|
||||
|
||||
branchname=${refname#refs/heads/}
|
||||
if [ "$branchname" = "$refname" ] ; then
|
||||
# not a branch update
|
||||
return
|
||||
fi
|
||||
|
||||
branch_message=
|
||||
if [ "x$branchname" != "master" ] ; then
|
||||
branch_message=" (When updating branch '$branchname'.)"
|
||||
fi
|
||||
|
||||
if expr $newrev : "^0\+$" > /dev/null 2>&1; then
|
||||
# Branch deletion, nothing to check
|
||||
return 0
|
||||
fi
|
||||
|
||||
if expr $oldrev : "^0\+$" > /dev/null 2>&1; then
|
||||
# Branch creation
|
||||
git ls-tree -r $newrev | (
|
||||
while read mode objtype sha path ; do
|
||||
if [ $objtype = blob ] ; then
|
||||
check_po $newrev $path $mode
|
||||
fi
|
||||
done
|
||||
)
|
||||
else
|
||||
# Branch update
|
||||
git diff-tree -r $oldrev $newrev | (
|
||||
while read srcmode destmode srcsha destsha status srcpath destpath ; do
|
||||
if [ $status = 'D' ] ; then
|
||||
continue # deleted
|
||||
fi
|
||||
|
||||
# destpath only present for copies/renames
|
||||
if [ x"$destpath" = x ] ; then
|
||||
destpath=$srcpath
|
||||
fi
|
||||
|
||||
# Strip colon from the source mode
|
||||
srcmode=${srcmode#:}
|
||||
|
||||
check_po $newrev $destpath $destmode
|
||||
done
|
||||
)
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
# Use a newer version of the gettext tools than the ones shipped with RHEL 5
|
||||
PATH=/usr/libexec/gettext17:$PATH
|
||||
|
||||
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
|
||||
|
||||
# Don't check .po's during import; we don't want to enforce correct
|
||||
# .po files for some ancient historical branch
|
||||
if [ -e $GIT_DIR/pending ] ; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ $# = 3 ] ; then
|
||||
check_pos $@ || exit 1
|
||||
else
|
||||
while read oldrev newrev refname; do
|
||||
check_pos $oldrev $newrev $refname || exit 1
|
||||
done
|
||||
fi
|
|
@ -0,0 +1,259 @@
|
|||
#!/bin/bash
|
||||
|
||||
# This script checks gnome.org policy about how people are supposed to
|
||||
# use git; the intent of the policy is to keep people from shooting
|
||||
# themselve in the foot.
|
||||
#
|
||||
# Eventually, we'd like to have an ability to override policy; one way
|
||||
# it could work is that if you did 'git push --exec=force' and you
|
||||
# were a member of the right group, then run-git-or-special-cmd
|
||||
# would set an environment variable that this script would interpret.
|
||||
|
||||
# Used in some of the messages
|
||||
server=git.labs.libre-entreprise.org
|
||||
|
||||
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
|
||||
|
||||
in_import() {
|
||||
test -e "$GIT_DIR/pending"
|
||||
}
|
||||
|
||||
forced() {
|
||||
test -n "$GNOME_GIT_FORCE"
|
||||
}
|
||||
|
||||
check_commit() {
|
||||
commit=$1
|
||||
|
||||
email="$(git log $commit -1 --pretty=format:%ae)"
|
||||
case "$email" in
|
||||
*localhost.localdomain|*\(none\))
|
||||
if ! in_import && ! forced ; then
|
||||
cat <<EOF >&2
|
||||
---
|
||||
The commits you are trying to push contain the author email
|
||||
address '$email'. Please configure your
|
||||
username and email address. See:
|
||||
|
||||
http://live.gnome.org/Git/Help/AuthorEmail
|
||||
|
||||
For instructions about how to do this and how to fix your
|
||||
existing commits.
|
||||
---
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
subject="$(git log $commit -1 --pretty=format:%s)"
|
||||
if expr "$subject" : ".*Merge branch.*of.*\(git\|ssh\):" > /dev/null 2>&1; then
|
||||
if ! in_import && ! forced ; then
|
||||
cat <<EOF >&2
|
||||
---
|
||||
The commit:
|
||||
|
||||
EOF
|
||||
git log $commit -1 >&2
|
||||
cat <<EOF >&2
|
||||
|
||||
Looks like it was produced by typing 'git pull' without the --rebase
|
||||
option when you had local changes. Running 'git pull --rebase' now
|
||||
will fix the problem. Then please try, 'git push' again. Please see:
|
||||
|
||||
http://live.gnome.org/Git/Help/ExtraMergeCommits
|
||||
---
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_ref_update() {
|
||||
oldrev=$1
|
||||
newrev=$2
|
||||
refname=$3
|
||||
|
||||
change_type=update
|
||||
if expr $oldrev : "^0\+$" > /dev/null 2>&1; then
|
||||
change_type=create
|
||||
fi
|
||||
|
||||
if expr $newrev : "^0\+$" > /dev/null 2>&1; then
|
||||
if [ x$change_type = xcreate ] ; then
|
||||
# Deleting an invalid ref, allow
|
||||
return 0
|
||||
fi
|
||||
change_type=delete
|
||||
fi
|
||||
|
||||
case $refname in
|
||||
refs/heads/*)
|
||||
# Branch update
|
||||
branchname=${refname#refs/heads/}
|
||||
|
||||
range=
|
||||
case $change_type in
|
||||
create)
|
||||
range="$newrev"
|
||||
;;
|
||||
delete)
|
||||
# We really don't like to allow deleting any branch, but
|
||||
# people need to do it to clean up accidentally pushed
|
||||
# branches. Deleting master, however, has no purpose other
|
||||
# than getting around the no-fast-forward restrictions
|
||||
if [ "x$branchname" = xmaster ] ; then
|
||||
cat <<EOF >&2
|
||||
---
|
||||
You are trying to delete the branch 'master'.
|
||||
---
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
update)
|
||||
range="$oldrev..$newrev"
|
||||
if [ `git merge-base $oldrev $newrev` != $oldrev ] && ! forced ; then
|
||||
# Non-fast-forward update. Right now we have
|
||||
# receive.denyNonFastforwards in the git configs for
|
||||
# our repositories anyways, but catching it here would
|
||||
# allow overriding without having to change the config
|
||||
# temporarily.
|
||||
cat <<EOF >&2
|
||||
---
|
||||
You are trying to update the branch '$branchname' in a way that is not
|
||||
a fast-forward update. Please see:
|
||||
|
||||
http://live.gnome.org/Git/Help/NonFastForward
|
||||
---
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# For new commits introduced with this branch update, we want to run some
|
||||
# checks to catch common mistakes.
|
||||
#
|
||||
# Expression here is same as in post-receive-notify-cia; we take
|
||||
# all the branches in the repo, as "^/ref/heads/branchname", other than the
|
||||
# branch we are actualy committing to, and exclude commits already on those
|
||||
# branches from the list of commits between $oldrev and $newrev.
|
||||
|
||||
if [ -n "$range" ] ; then
|
||||
for merged in $(git rev-parse --symbolic-full-name --not --branches | \
|
||||
egrep -v "^\^$refname$" | \
|
||||
git rev-list --reverse --stdin "$range"); do
|
||||
check_commit $merged
|
||||
done
|
||||
fi
|
||||
;;
|
||||
refs/tags/*)
|
||||
# Tag update
|
||||
tagname=${refname#refs/tags/}
|
||||
|
||||
case $change_type in
|
||||
create)
|
||||
object_type=`git cat-file -t $newrev`
|
||||
case $object_type in
|
||||
commit)
|
||||
# Lightweight tag; we allow an import containing these
|
||||
# tags, but forbid them in general
|
||||
if ! in_import && ! forced ; then
|
||||
cat <<EOF >&2
|
||||
---
|
||||
You are trying to push the lightweight tag '$tagname'. You should use
|
||||
a signed tag instead. See:
|
||||
|
||||
http://live.gnome.org/Git/Help/LightweightTags
|
||||
---
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
tag)
|
||||
# Annotated tag
|
||||
;;
|
||||
*)
|
||||
# git is happy to allow tagging random objects, we aren't
|
||||
cat <<EOF >&2
|
||||
---
|
||||
You are trying to push the tag '$tagname', which points to an object
|
||||
of type $object_type. (It should point to a commit or tag object.)
|
||||
---
|
||||
EOF
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
delete)
|
||||
# Deleting a tag is probably someone trying to work-around
|
||||
# not being able to update a tag. Disallowing lightweight
|
||||
# tags will cut down on accidentally pushing tags called 'list'
|
||||
# or whatever. During import we allow the user to clean up
|
||||
# accidentally pushed tags.
|
||||
if ! in_import && ! forced ; then
|
||||
cat <<EOF >&2
|
||||
---
|
||||
You are trying to delete the tag '$tagname'.
|
||||
|
||||
http://live.gnome.org/Git/Help/TagUpdates
|
||||
---
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
update)
|
||||
if ! forced ; then
|
||||
cat <<EOF >&2
|
||||
---
|
||||
You are trying to replace the tag '$tagname' with a new tag. Please see:
|
||||
|
||||
http://live.gnome.org/Git/Help/TagUpdates
|
||||
---
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
refs/remotes/*)
|
||||
# Remote tracking branch
|
||||
cat <<EOF >&2
|
||||
---
|
||||
You are trying to push the remote tracking branch:
|
||||
|
||||
$refname
|
||||
|
||||
to $server.
|
||||
---
|
||||
EOF
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
# Something else
|
||||
cat <<EOF >&2
|
||||
---
|
||||
You are trying to push the ref:
|
||||
|
||||
$refname
|
||||
|
||||
to $server. This isn't a branch or tag.
|
||||
---
|
||||
EOF
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
if [ $# = 3 ] ; then
|
||||
check_ref_update $@
|
||||
else
|
||||
while read oldrev newrev refname; do
|
||||
check_ref_update $oldrev $newrev $refname
|
||||
done
|
||||
fi
|
||||
|
||||
exit 0
|
|
@ -0,0 +1,166 @@
|
|||
# General Utility Functions used in our Git scripts
|
||||
#
|
||||
# Copyright (C) 2008 Owen Taylor
|
||||
# Copyright (C) 2009 Red Hat, Inc
|
||||
#
|
||||
# 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 program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, If not, see
|
||||
# http://www.gnu.org/licenses/.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import smtplib
|
||||
from subprocess import Popen
|
||||
import tempfile
|
||||
import time
|
||||
import email
|
||||
|
||||
def die(message):
|
||||
print >>sys.stderr, message
|
||||
sys.exit(1)
|
||||
|
||||
# This cleans up our generation code by allowing us to use the same indentation
|
||||
# for the first line and subsequent line of a multi-line string
|
||||
def strip_string(str):
|
||||
start = 0
|
||||
end = len(str)
|
||||
if len(str) > 0 and str[0] == '\n':
|
||||
start += 1
|
||||
if len(str) > 1 and str[end - 1] == '\n':
|
||||
end -= 1
|
||||
|
||||
return str[start:end]
|
||||
|
||||
# How long to wait between mails (in seconds); the idea of waiting
|
||||
# is to try to make the sequence of mails we send out in order
|
||||
# actually get delivered in order. The waiting is done in a forked
|
||||
# subprocess and doesn't stall completion of the main script.
|
||||
EMAIL_DELAY = 5
|
||||
|
||||
# Some line that can never appear in any email we send out
|
||||
EMAIL_BOUNDARY="---@@@--- labs-git-email ---@@@---\n"
|
||||
|
||||
def tomail(x):
|
||||
if '<' in x:
|
||||
x = x[x.index('<')+1:-1]
|
||||
return x
|
||||
|
||||
# Run in subprocess
|
||||
def _do_send_emails(email_in):
|
||||
email_files = []
|
||||
current_file = None
|
||||
last_line = None
|
||||
|
||||
# Read emails from the input pipe and write each to a file
|
||||
for line in email_in:
|
||||
if current_file is None:
|
||||
current_file, filename = tempfile.mkstemp(suffix=".mail", prefix="labs-post-receive-email-")
|
||||
email_files.append(filename)
|
||||
|
||||
if line == EMAIL_BOUNDARY:
|
||||
# Strip the last line if blank; see comment when writing
|
||||
# the email boundary for rationale
|
||||
if last_line.strip() != "":
|
||||
os.write(current_file, last_line)
|
||||
last_line = None
|
||||
os.close(current_file)
|
||||
current_file = None
|
||||
else:
|
||||
if last_line is not None:
|
||||
os.write(current_file, last_line)
|
||||
last_line = line
|
||||
|
||||
if current_file is not None:
|
||||
if last_line is not None:
|
||||
os.write(current_file, last_line)
|
||||
os.close(current_file)
|
||||
|
||||
# We're done interacting with the parent process, the rest happens
|
||||
# asynchronously; send out the emails one by one and remove the
|
||||
# temporary files
|
||||
server = smtplib.SMTP('localhost')
|
||||
for i, filename in enumerate(email_files):
|
||||
if i != 0:
|
||||
time.sleep(EMAIL_DELAY)
|
||||
|
||||
f = open(filename)
|
||||
msgstr = f.read()
|
||||
message = email.message_from_string(msgstr)
|
||||
fromaddr = tomail(message['From'])
|
||||
toaddrs = [tomail(x.strip()) for x in message['To'].split(',')]
|
||||
server.sendmail(fromaddr, toaddrs, msgstr)
|
||||
|
||||
os.remove(filename)
|
||||
f.close()
|
||||
server.quit()
|
||||
|
||||
email_file = None
|
||||
|
||||
# Start a new outgoing email; returns a file object that the
|
||||
# email should be written to. Call end_email() when done
|
||||
def start_email():
|
||||
global email_file
|
||||
if email_file is None:
|
||||
email_pipe = os.pipe()
|
||||
pid = os.fork()
|
||||
if pid == 0:
|
||||
# The child
|
||||
|
||||
os.close(email_pipe[1])
|
||||
email_in = os.fdopen(email_pipe[0])
|
||||
|
||||
# Redirect stdin/stdout/stderr to/from /dev/null
|
||||
devnullin = os.open("/dev/null", os.O_RDONLY)
|
||||
os.close(0)
|
||||
os.dup2(devnullin, 0)
|
||||
|
||||
if False:
|
||||
devnullout = os.open("/dev/null", os.O_WRONLY)
|
||||
os.close(1)
|
||||
os.dup2(devnullout, 1)
|
||||
os.close(2)
|
||||
os.dup2(devnullout, 2)
|
||||
os.close(devnullout)
|
||||
|
||||
# Fork again to daemonize
|
||||
if os.fork() > 0:
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
_do_send_emails(email_in)
|
||||
except Exception:
|
||||
raise
|
||||
import syslog
|
||||
import traceback
|
||||
|
||||
syslog.openlog(os.path.basename(sys.argv[0]))
|
||||
syslog.syslog(syslog.LOG_ERR, "Unexpected exception sending mail")
|
||||
for line in traceback.format_exc().strip().split("\n"):
|
||||
syslog.syslog(syslog.LOG_ERR, line)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
email_file = os.fdopen(email_pipe[1], "w")
|
||||
else:
|
||||
# The email might not end with a newline, so add one. We'll
|
||||
# strip the last line, if blank, when emails, so the net effect
|
||||
# is to add a newline to messages without one
|
||||
email_file.write("\n")
|
||||
email_file.write(EMAIL_BOUNDARY)
|
||||
|
||||
return email_file
|
||||
|
||||
# Finish an email started with start_email
|
||||
def end_email():
|
||||
global email_file
|
||||
email_file.flush()
|
Loading…
Reference in New Issue