🚀 DevOps & SRE Certification Program 📅 Starting: 1st of Every Month 🤝 +91 8409492687 🔍 Contact@DevOpsSchool.com

Upgrade & Secure Your Future with DevOps, SRE, DevSecOps, MLOps!

We spend hours on Instagram and YouTube and waste money on coffee and fast food, but won’t spend 30 minutes a day learning skills to boost our careers.
Master in DevOps, SRE, DevSecOps & MLOps!

Learn from Guru Rajesh Kumar and double your salary in just one year.


Get Started Now!

SVN hooks Tutorials & Guide

What is Hooks?

A hook script is a program triggered by some repository event, such as the creation of a new revision or the modification of an unversioned property. Each hook is handed enough information to tell what that event is, what target(s) it’s operating on, and the username of the person who triggered the event.

Server hooks are scripts that run automatically every time a particular event occurs in the StarTeam repository. Server hooks allow the user to trigger customizable actions at key points in the development life cycle.

The subversion version control system has a wonderfully handy feature called hooks. Hooks are essentially scripts that are triggered by a version control event (such as a commits, or revision property changes).

Subversion Hooks are located in your repository directory (so if you have multiple repositories you have to setup hooks for each one) in a directory called hooks, perhaps something like this: /home/svn/projectName/hooks.

What is SVN Hooks?

Subversion repositories provide a number of event hooks which are essentially opportunities for administrators to extend Subversion’s functionality at key moments of key operations. Repository hooks are implemented as programs executed by Subversion itself at those key moments—before and after a commit, before and after a user locks a file, and so on.

Subversion repositories provide a number of event hooks which are essentially opportunities for administrators to extend Subversion’s functionality at key moments of key operations.

How to setup SVN SVN Hooks

Types of SVN Hooks

  • start-commit – Notification of the beginning of a commit.
  • pre-commit – Notification just prior to commit completion.
  • post-commit – Notification of a successful commit.
  • pre-revprop-change – Notification of a revision property change attempt.
  • post-revprop-change – Notification of a successful revision property change.
  • pre-lock – Notification of a path lock attempt.
  • post-lock – Notification of a successful path lock.
  • pre-unlock – Notification of a path unlock attempt.
  • post-unlock – Notification of a successful path unlock.

What is pre-commit hook in SVN?

A pre-commit hook is a feature available in the Subversion version control system that allows code to be validated before it is committed to the repository. The PHP_CodeSniffer pre-commit hook allows you to check code for coding standard errors and stop the commit process if errors are found.

Where are SVN hooks stored?.

Subversion Hooks are located in your repository directory (so if you have multiple repositories you have to setup hooks for each one) in a directory called hooks , perhaps something like this: /home/svn/projectName/hooks .

How To setup an SVN Pre-Commit Hook?

  • Find the hooks directory for your repo.
  • There should be a pre-commit. tmpl file there – rename it to pre-commit (no extension)
  • Restart the SVN server.

How to write SVN Hooks Script?

You can write your hook using C or C++ if you like. Most people use Perl or Python.

The main thing is that svnlook should be used in your hook script and not svn. svnlook is faster and safer than svn. In fact, in pre-commit scripts, you have to use svnlook since you don’t have a repository revision.

Use Cases of SVN Hooks?

Loading...

Reference

  • https://svnbook.red-bean.com/en/1.8/svn.ref.reposhooks.html
  • http://svn.apache.org/repos/asf/subversion/trunk/tools/hook-scripts/
  • http://svn.apache.org/repos/asf/subversion/trunk/contrib/hook-scripts/
  • http://svn.code.sf.net/p/tortoisesvn/code/trunk/contrib/hook-scripts/

Some Example SVN Hooks Code

#!/usr/bin/php
<?php
define('SVNLOOK', '/usr/bin/svnlook');
// 1 = TXN, 2 = REPO
$command = SVNLOOK." changed -t {$_SERVER['argv'][1]} {$_SERVER['argv'][2]} | awk '{print \$2}' | grep -i \\.php\$";
$handle = popen($command, 'r');
if ($handle === false) {
echo 'ERROR: Could not execute "'.$command.'"'.PHP_EOL.PHP_EOL;
exit(2);
}
$contents = stream_get_contents($handle);
fclose($handle);
$filesWithBOM = array();
// Ausgabe in einzelne Zeilen aufspalten, -1 == no limit
foreach (preg_split('/\v/', $contents, -1, PREG_SPLIT_NO_EMPTY) as $path) {
$command = SVNLOOK." cat -t {$_SERVER['argv'][1]} {$_SERVER['argv'][2]} ".$path." | grep -l \$'\\xEF\\xBB\\xBF'";
system($command, $ret);
if ($ret != 0) $filesWithBOM[] = $path;
}
if (empty($filesWithBOM)) exit(0);
echo "The following files are saved with UTF-8 BOM encoding, which is not allowed:", PHP_EOL;
foreach ($filesWithBOM as $file) {
echo $file, PHP_EOL;
}
exit(1);
# This is a sample configuration file for commit-access-control.pl.
#
# $Id$
#
# This file uses the Windows ini style, where the file consists of a
# number of sections, each section starts with a unique section name
# in square brackets. Parameters in each section are specified as
# Name = Value. Any spaces around the equal sign will be ignored. If
# there are multiple sections with exactly the same section name, then
# the parameters in those sections will be added together to produce
# one section with cumulative parameters.
#
# The commit-access-control.pl script reads these sections in order,
# so later sections may overwrite permissions granted or removed in
# previous sections.
#
# Each section has three valid parameters. Any other parameters are
# ignored.
# access = (read-only|read-write)
#
# This parameter is a required parameter. Valid values are
# `read-only' and `read-write'.
#
# The access rights to apply to modified files and directories
# that match the `match' regular expression described later on.
#
# match = PERL_REGEX
#
# This parameter is a required parameter and its value is a Perl
# regular expression.
#
# To help users that automatically write regular expressions that
# match the beginning of absolute paths using ^/, the script
# removes the / character because subversion paths, while they
# start at the root level, do not begin with a /.
#
# users = username1 [username2 [username3 [username4 ...]]]
# or
# users = username1 [username2]
# users = username3 username4
#
# This parameter is optional. The usernames listed here must be
# exact usernames. There is no regular expression matching for
# usernames. You may specify all the usernames that apply on one
# line or split the names up on multiple lines.
#
# The access rights from `access' are applied to ALL modified
# paths that match the `match' regular expression only if NO
# usernames are specified in the section or if one of the listed
# usernames matches the author of the commit.
#
# By default, because you're using commit-access-control.pl in the
# first place to protect your repository, the script sets the
# permissions to all files and directories in the repository to
# read-only, so if you want to open up portions of the repository,
# you'll need to edit this file.
#
# NOTE: NEVER GIVE DIFFERENT SECTIONS THE SAME SECTION NAME, OTHERWISE
# THE PARAMETERS FOR THOSE SECTIONS WILL BE MERGED TOGETHER INTO ONE
# SECTION AND YOUR SECURITY MAY BE COMPROMISED.
[Make everything read-only for all users]
match = .*
access = read-only
[Make project1 read-write for users Jane and Joe]
match = ^(branches|tags|trunk)/project1
users = jane joe
access = read-write
[However, we don't trust Joe with project1's Makefile]
match = ^(branches|tags|trunk)/project1/Makefile
users = joe
access = read-only
#!/usr/bin/env perl
# ====================================================================
# commit-access-control.pl: check if the user that submitted the
# transaction TXN-NAME has the appropriate rights to perform the
# commit in repository REPOS using the permissions listed in the
# configuration file CONF_FILE.
#
# $HeadURL$
# $LastChangedDate$
# $LastChangedBy$
# $LastChangedRevision$
#
# Usage: commit-access-control.pl REPOS TXN-NAME CONF_FILE
#
# ====================================================================
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ====================================================================
# Turn on warnings the best way depending on the Perl version.
BEGIN {
if ( $] >= 5.006_000)
{ require warnings; import warnings; }
else
{ $^W = 1; }
}
use strict;
use Carp;
use Config::IniFiles 2.27;
######################################################################
# Configuration section.
# Svnlook path.
my $svnlook = "@SVN_BINDIR@/svnlook";
# Since the path to svnlook depends upon the local installation
# preferences, check that the required program exists to insure that
# the administrator has set up the script properly.
{
my $ok = 1;
foreach my $program ($svnlook)
{
if (-e $program)
{
unless (-x $program)
{
warn "$0: required program `$program' is not executable, ",
"edit $0.\n";
$ok = 0;
}
}
else
{
warn "$0: required program `$program' does not exist, edit $0.\n";
$ok = 0;
}
}
exit 1 unless $ok;
}
######################################################################
# Initial setup/command-line handling.
&usage unless @ARGV == 3;
my $repos = shift;
my $txn = shift;
my $cfg_filename = shift;
unless (-e $repos)
{
&usage("$0: repository directory `$repos' does not exist.");
}
unless (-d $repos)
{
&usage("$0: repository directory `$repos' is not a directory.");
}
unless (-e $cfg_filename)
{
&usage("$0: configuration file `$cfg_filename' does not exist.");
}
unless (-r $cfg_filename)
{
&usage("$0: configuration file `$cfg_filename' is not readable.");
}
# Define two constant subroutines to stand for read-only or read-write
# access to the repository.
sub ACCESS_READ_ONLY () { 'read-only' }
sub ACCESS_READ_WRITE () { 'read-write' }
######################################################################
# Load the configuration file and validate it.
my $cfg = Config::IniFiles->new(-file => $cfg_filename);
unless ($cfg)
{
die "$0: error in loading configuration file `$cfg_filename'",
@Config::IniFiles::errors ? ":\n@Config::IniFiles::errors\n"
: ".\n";
}
# Go through each section of the configuration file, validate that
# each section has the required parameters and complain about unknown
# parameters. Compile any regular expressions.
my @sections = $cfg->Sections;
{
my $ok = 1;
foreach my $section (@sections)
{
# First check for any unknown parameters.
foreach my $param ($cfg->Parameters($section))
{
next if $param eq 'match';
next if $param eq 'users';
next if $param eq 'access';
warn "$0: config file `$cfg_filename' section `$section' parameter ",
"`$param' is being ignored.\n";
$cfg->delval($section, $param);
}
my $access = $cfg->val($section, 'access');
if (defined $access)
{
unless ($access eq ACCESS_READ_ONLY or $access eq ACCESS_READ_WRITE)
{
warn "$0: config file `$cfg_filename' section `$section' sets ",
"`access' to illegal value `$access'.\n";
$ok = 0;
}
}
else
{
warn "$0: config file `$cfg_filename' section `$section' does ",
"not set `access' parameter.\n";
$ok = 0;
}
my $match_regex = $cfg->val($section, 'match');
if (defined $match_regex)
{
# To help users that automatically write regular expressions
# that match the beginning of absolute paths using ^/,
# remove the / character because subversion paths, while
# they start at the root level, do not begin with a /.
$match_regex =~ s#^\^/#^#;
my $match_re;
eval { $match_re = qr/$match_regex/ };
if ($@)
{
warn "$0: config file `$cfg_filename' section `$section' ",
"`match' regex `$match_regex' does not compile:\n$@\n";
$ok = 0;
}
else
{
$cfg->newval($section, 'match_re', $match_re);
}
}
else
{
warn "$0: config file `$cfg_filename' section `$section' does ",
"not set `match' parameter.\n";
$ok = 0;
}
}
exit 1 unless $ok;
}
######################################################################
# Harvest data using svnlook.
# Change into /tmp so that svnlook diff can create its .svnlook
# directory.
my $tmp_dir = '/tmp';
chdir($tmp_dir)
or die "$0: cannot chdir `$tmp_dir': $!\n";
# Get the author from svnlook.
my @svnlooklines = &read_from_process($svnlook, 'author', $repos, '-t', $txn);
my $author = shift @svnlooklines;
unless (length $author)
{
die "$0: txn `$txn' has no author.\n";
}
# Figure out what directories have changed using svnlook..
my @dirs_changed = &read_from_process($svnlook, 'dirs-changed', $repos,
'-t', $txn);
# Lose the trailing slash in the directory names if one exists, except
# in the case of '/'.
my $rootchanged = 0;
for (my $i=0; $i<@dirs_changed; ++$i)
{
if ($dirs_changed[$i] eq '/')
{
$rootchanged = 1;
}
else
{
$dirs_changed[$i] =~ s#^(.+)[/\\]$#$1#;
}
}
# Figure out what files have changed using svnlook.
my @files_changed;
foreach my $line (&read_from_process($svnlook, 'changed', $repos, '-t', $txn))
{
# Split the line up into the modification code and path, ignoring
# property modifications.
if ($line =~ /^.. (.*)$/)
{
push(@files_changed, $1);
}
}
# Create the list of all modified paths.
my @changed = (@dirs_changed, @files_changed);
# There should always be at least one changed path. If there are
# none, then there maybe something fishy going on, so just exit now
# indicating that the commit should not proceed.
unless (@changed)
{
die "$0: no changed paths found in txn `$txn'.\n";
}
######################################################################
# Populate the permissions table.
# Set a hash keeping track of the access rights to each path. Because
# this is an access control script, set the default permissions to
# read-only.
my %permissions;
foreach my $path (@changed)
{
$permissions{$path} = ACCESS_READ_ONLY;
}
foreach my $section (@sections)
{
# Decide if this section should be used. It should be used if
# there are no users listed at all for this section, or if there
# are users listed and the author is one of them.
my $use_this_section;
# If there are any users listed, then check if the author of this
# commit is listed in the list. If not, then delete the section,
# because it won't apply.
#
# The configuration file can list users like this on multiple
# lines:
# users = joe@mysite.com betty@mysite.com
# users = bob@yoursite.com
# Because of the way Config::IniFiles works, check if there are
# any users at all with the scalar return from val() and if there,
# then get the array value to get all users.
my $users = $cfg->val($section, 'users');
if (defined $users and length $users)
{
my $match_user = 0;
foreach my $entry ($cfg->val($section, 'users'))
{
unless ($match_user)
{
foreach my $user (split(' ', $entry))
{
if ($author eq $user)
{
$match_user = 1;
last;
}
}
}
}
$use_this_section = $match_user;
}
else
{
$use_this_section = 1;
}
next unless $use_this_section;
# Go through each modified path and match it to the regular
# expression and set the access right if the regular expression
# matches.
my $access = $cfg->val($section, 'access');
my $match_re = $cfg->val($section, 'match_re');
foreach my $path (@changed)
{
$permissions{$path} = $access if $path =~ $match_re;
}
}
# Go through all the modified paths and see if any permissions are
# read-only. If so, then fail the commit.
my @failed_paths;
foreach my $path (@changed)
{
if ($permissions{$path} ne ACCESS_READ_WRITE)
{
push(@failed_paths, $path);
}
}
if (@failed_paths)
{
warn "$0: user `$author' does not have permission to commit to ",
@failed_paths > 1 ? "these paths:\n " : "this path:\n ",
join("\n ", @failed_paths), "\n";
exit 1;
}
else
{
exit 0;
}
sub usage
{
warn "@_\n" if @_;
die "usage: $0 REPOS TXN-NAME CONF_FILE\n";
}
sub safe_read_from_pipe
{
unless (@_)
{
croak "$0: safe_read_from_pipe passed no arguments.\n";
}
print "Running @_\n";
my $pid = open(SAFE_READ, '-|');
unless (defined $pid)
{
die "$0: cannot fork: $!\n";
}
unless ($pid)
{
open(STDERR, ">&STDOUT")
or die "$0: cannot dup STDOUT: $!\n";
exec(@_)
or die "$0: cannot exec `@_': $!\n";
}
my @output;
while (<SAFE_READ>)
{
chomp;
push(@output, $_);
}
close(SAFE_READ);
my $result = $?;
my $exit = $result >> 8;
my $signal = $result & 127;
my $cd = $result & 128 ? "with core dump" : "";
if ($signal or $cd)
{
warn "$0: pipe from `@_' failed $cd: exit=$exit signal=$signal\n";
}
if (wantarray)
{
return ($result, @output);
}
else
{
return $result;
}
}
sub read_from_process
{
unless (@_)
{
croak "$0: read_from_process passed no arguments.\n";
}
my ($status, @output) = &safe_read_from_pipe(@_);
if ($status)
{
if (@output)
{
die "$0: `@_' failed with this output:\n", join("\n", @output), "\n";
}
else
{
die "$0: `@_' failed with no output.\n";
}
}
else
{
return @output;
}
}
#!/usr/bin/env python
#
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
#
'''control-chars.py: Subversion repository hook script that rejects filenames
which contain control characters. Expects to be called like a pre-commit hook:
control-chars.py <REPOS-PATH> <TXN-NAME>
Latest version should be available at
http://svn.apache.org/repos/asf/subversion/trunk/tools/hook-scripts/
See validate-files.py for more generic validations.'''
import sys
import re
import posixpath
import svn
import svn.fs
import svn.repos
import svn.core
# Can't hurt to disallow chr(0), though the C API will never pass one anyway.
control_chars = set( [chr(i) for i in range(32)] )
control_chars.add(chr(127))
def check_node(node, path):
"check NODE for control characters. PATH is used for error messages"
if node.action == 'A':
if any((c in control_chars) for c in node.name):
sys.stderr.write("'%s' contains a control character" % path)
return 3
def walk_tree(node, path, callback):
"Walk NODE"
if not node:
return 0
ret_val = callback(node, path)
if ret_val > 0:
return ret_val
node = node.child
if not node:
return 0
while node:
full_path = posixpath.join(path, node.name)
ret_val = walk_tree(node, full_path, callback)
# If we ran into an error just return up the stack all the way
if ret_val > 0:
return ret_val
node = node.sibling
return 0
def usage():
sys.stderr.write("Invalid arguments, expects to be called like a pre-commit hook.")
def main(ignored_pool, argv):
if len(argv) < 3:
usage()
return 2
repos_path = svn.core.svn_path_canonicalize(argv[1])
txn_name = argv[2]
if not repos_path or not txn_name:
usage()
return 2
repos = svn.repos.svn_repos_open(repos_path)
fs = svn.repos.svn_repos_fs(repos)
txn = svn.fs.svn_fs_open_txn(fs, txn_name)
txn_root = svn.fs.svn_fs_txn_root(txn)
base_rev = svn.fs.svn_fs_txn_base_revision(txn)
if base_rev is None or base_rev <= svn.core.SVN_INVALID_REVNUM:
sys.stderr.write("Transaction '%s' is not based on a revision" % txn_name)
return 2
base_root = svn.fs.svn_fs_revision_root(fs, base_rev)
editor, editor_baton = svn.repos.svn_repos_node_editor(repos, base_root,
txn_root)
try:
svn.repos.svn_repos_replay2(txn_root, "", svn.core.SVN_INVALID_REVNUM,
False, editor, editor_baton, None, None)
except svn.core.SubversionException as e:
# If we get a file not found error then some file has a newline in it and
# fsfs's own transaction is now corrupted.
if e.apr_err == svn.core.SVN_ERR_FS_NOT_FOUND:
match = re.search("path '(.*?)'", e.message)
if not match:
sys.stderr.write(repr(e))
return 2
path = match.group(1)
sys.stderr.write("Path name that contains '%s' has a newline." % path)
return 3
# fs corrupt error probably means that there is probably both
# file and file\n in the transaction. However, we can't really determine
# which files since the transaction is broken. Even if we didn't reject
# this it would not be able to be committed. This just gives a better
# error message.
elif e.apr_err == svn.core.SVN_ERR_FS_CORRUPT:
sys.stderr.write("Some path contains a newline causing: %s" % repr(e))
return 3
else:
sys.stderr.write(repr(e))
return 2
tree = svn.repos.svn_repos_node_from_baton(editor_baton)
return walk_tree(tree, "/", check_node)
if __name__ == '__main__':
sys.exit(svn.core.run_app(main, sys.argv))
#!/usr/bin/env python2
# Licensed under the same terms as Subversion: the Apache License, Version 2.0
#
# pre-commit hook script for Subversion CVE-2017-9800
#
# This prevents commits that set svn:externals containing suspicions
# svn+ssh:// URLs.
#
# With this script installed a commit like the one below should fail:
#
# svnmucc -mm propset svn:externals 'svn+ssh://-localhost/X X' REPOSITORY-URL
import sys, locale, urllib, urlparse, curses.ascii
from svn import wc, repos, fs
# A simple whitelist to ensure these are not suspicious:
# user@server
# [::1]:22
# server-name
# server_name
# 127.0.0.1
# with an extra restriction that a leading '-' is suspicious.
def suspicious_host(host):
if host[0] == '-':
return True
for char in host:
if not curses.ascii.isalnum(char) and not char in ':.-_[]@':
return True
return False
native = locale.getlocale()[1]
if not native: native = 'ascii'
repos_handle = repos.open(sys.argv[1].decode(native).encode('utf-8'))
fs_handle = repos.fs(repos_handle)
txn_handle = fs.open_txn(fs_handle, sys.argv[2].decode(native).encode('utf-8'))
txn_root = fs.txn_root(txn_handle)
rev_root = fs.revision_root(fs_handle, fs.txn_root_base_revision(txn_root))
for path, change in fs.paths_changed2(txn_root).iteritems():
if change.prop_mod:
# The new value, if any
txn_prop = fs.node_prop(txn_root, path, "svn:externals")
if not txn_prop:
continue
# The old value, if any
rev_prop = None
if change.change_kind == fs.path_change_modify:
rev_prop = fs.node_prop(rev_root, path, "svn:externals")
elif change.change_kind == fs.path_change_add and change.copyfrom_path:
copy_root = fs.revision_root(fs_handle, change.copyfrom_rev)
rev_prop = fs.node_prop(copy_root, change.copyfrom_path,
"svn:externals")
if txn_prop != rev_prop:
error_path = path.decode('utf-8').encode(native, 'replace')
externals = []
try:
externals = wc.parse_externals_description2(path, txn_prop)
except:
sys.stderr.write("Commit blocked due to parse failure "
"on svn:externals for %s\n" % error_path)
sys.exit(1)
for external in externals:
parsed = urlparse.urlparse(urllib.unquote(external.url))
if (parsed and parsed.scheme[:4] == "svn+"
and suspicious_host(parsed.netloc)):
sys.stderr.write("Commit blocked due to suspicious URL "
"containing %r in svn:externals "
"for %s\n" % (parsed.netloc, error_path))
sys.exit(1)
#!/usr/bin/env python
#
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
#
# log-police.py: Ensure that log messages end with a single newline.
# See usage() function for details, or just run with no arguments.
import os
import sys
import getopt
try:
my_getopt = getopt.gnu_getopt
except AttributeError:
my_getopt = getopt.getopt
import svn
import svn.fs
import svn.repos
import svn.core
def fix_log_message(log_message):
"""Return a fixed version of LOG_MESSAGE. By default, this just
means ensuring that the result ends with exactly one newline and no
other whitespace. But if you want to do other kinds of fixups, this
function is the place to implement them -- all log message fixing in
this script happens here."""
return log_message.rstrip() + "\n"
def fix_txn(fs, txn_name):
"Fix up the log message for txn TXN_NAME in FS. See fix_log_message()."
txn = svn.fs.svn_fs_open_txn(fs, txn_name)
log_message = svn.fs.svn_fs_txn_prop(txn, "svn:log")
if log_message is not None:
new_message = fix_log_message(log_message)
if new_message != log_message:
svn.fs.svn_fs_change_txn_prop(txn, "svn:log", new_message)
def fix_rev(fs, revnum):
"Fix up the log message for revision REVNUM in FS. See fix_log_message()."
log_message = svn.fs.svn_fs_revision_prop(fs, revnum, 'svn:log')
if log_message is not None:
new_message = fix_log_message(log_message)
if new_message != log_message:
svn.fs.svn_fs_change_rev_prop(fs, revnum, "svn:log", new_message)
def usage_and_exit(error_msg=None):
"""Write usage information and exit. If ERROR_MSG is provide, that
error message is printed first (to stderr), the usage info goes to
stderr, and the script exits with a non-zero status. Otherwise,
usage info goes to stdout and the script exits with a zero status."""
import os.path
stream = error_msg and sys.stderr or sys.stdout
if error_msg:
stream.write("ERROR: %s\n\n" % error_msg)
stream.write("USAGE: %s [-t TXN_NAME | -r REV_NUM | --all-revs] REPOS\n"
% (os.path.basename(sys.argv[0])))
stream.write("""
Ensure that log messages end with exactly one newline and no other
whitespace characters. Use as a pre-commit hook by passing '-t TXN_NAME';
fix up a single revision by passing '-r REV_NUM'; fix up all revisions by
passing '--all-revs'. (When used as a pre-commit hook, may modify the
svn:log property on the txn.)
""")
sys.exit(error_msg and 1 or 0)
def main(ignored_pool, argv):
repos_path = None
txn_name = None
rev_name = None
all_revs = False
try:
opts, args = my_getopt(argv[1:], 't:r:h?', ["help", "all-revs"])
except:
usage_and_exit("problem processing arguments / options.")
for opt, value in opts:
if opt == '--help' or opt == '-h' or opt == '-?':
usage_and_exit()
elif opt == '-t':
txn_name = value
elif opt == '-r':
rev_name = value
elif opt == '--all-revs':
all_revs = True
else:
usage_and_exit("unknown option '%s'." % opt)
if txn_name is not None and rev_name is not None:
usage_and_exit("cannot pass both -t and -r.")
if txn_name is not None and all_revs:
usage_and_exit("cannot pass --all-revs with -t.")
if rev_name is not None and all_revs:
usage_and_exit("cannot pass --all-revs with -r.")
if rev_name is None and txn_name is None and not all_revs:
usage_and_exit("must provide exactly one of -r, -t, or --all-revs.")
if len(args) != 1:
usage_and_exit("only one argument allowed (the repository).")
repos_path = svn.core.svn_path_canonicalize(args[0])
# A non-bindings version of this could be implemented by calling out
# to 'svnlook getlog' and 'svnadmin setlog'. However, using the
# bindings results in much simpler code.
fs = svn.repos.svn_repos_fs(svn.repos.svn_repos_open(repos_path))
if txn_name is not None:
fix_txn(fs, txn_name)
elif rev_name is not None:
fix_rev(fs, int(rev_name))
elif all_revs:
# Do it such that if we're running on a live repository, we'll
# catch up even with commits that came in after we started.
last_youngest = 0
while True:
youngest = svn.fs.svn_fs_youngest_rev(fs)
if youngest >= last_youngest:
for this_rev in range(last_youngest, youngest + 1):
fix_rev(fs, this_rev)
last_youngest = youngest + 1
else:
break
if __name__ == '__main__':
sys.exit(svn.core.run_app(main, sys.argv))
#
# mailer.conf: example configuration file for mailer.py
#
# $Id$
[general]
# The [general].diff option is now DEPRECATED.
# Instead use [defaults].diff .
#
# One delivery method must be chosen. mailer.py will prefer using the
# "mail_command" option. If that option is empty or commented out,
# then it checks whether the "smtp_hostname" option has been
# specified. If neither option is set, then the commit message is
# delivered to stdout.
#
# This command will be invoked with destination addresses on the command
# line, and the message piped into it.
#mail_command = /usr/sbin/sendmail
# This option specifies the hostname for delivery via SMTP.
#smtp_hostname = localhost
# This option specifies the TCP port number to connect for SMTP.
# If it is not specified, 25 is used for SMTP and 465 is used for
# SMTP-Over-SSL by default.
#smtp_port = 25
# Username and password for SMTP servers requiring authorisation.
#smtp_username = example
#smtp_password = example
# This option specifies whether to use SSL from the beginning of the SMTP
# connection.
#smtp_ssl = yes
# --------------------------------------------------------------------------
#
# CONFIGURATION GROUPS
#
# Any sections other than [general], [defaults], [maps] and sections
# referred to within [maps] are considered to be user-defined groups
# which override values in the [defaults] section.
# These groups are selected using the following three options:
#
# for_repos
# for_paths
# search_logmsg
#
# Each option specifies a regular expression. for_repos is matched
# against the absolute path to the repository the mailer is operating
# against. for_paths is matched against *every* path (files and
# dirs) that was modified during the commit.
#
# The options specified in the [defaults] section are always selected. The
# presence of a non-matching for_repos has no relevance. Note that you may
# still use a for_repos value to extract useful information (more on this
# later). Any user-defined groups without a for_repos, or which contains
# a matching for_repos, will be selected for potential use.
#
# The subset of user-defined groups identified by the repository are further
# refined based on the for_paths option. A group is selected if at least
# one path(*) in the commit matches the for_paths regular expression. Note
# that the paths are relative to the root of the repository and do not
# have a leading slash.
#
# (*) Actually, each path will select just one group. Thus, it is possible
# that one group will match against all paths, while another group matches
# none of the paths, even though its for_paths would have selected some of
# the paths in the commit.
#
# search_logmsg specifies a regular expression to match against the
# log message. If the regular expression does not match the log
# message, the group is not matched; if the regular expression matches
# once, the group is used. If there are multiple matches, each
# successful match generates another group-match (this is useful if
# "named groups" are used). If search_logmsg is not used, no log
# message filtering is performed.
#
# Groups are matched in no particular order. Do not depend upon their
# order within this configuration file. The values from [defaults] will
# be used if no group is matched or an option in a group does not override
# the corresponding value from [defaults].
#
# Generally, a commit email is generated for each group that has been
# selected. The script will try to minimize mails, so it may be possible
# that a single message will be generated to multiple recipients. In
# addition, it is possible for multiple messages per group to be generated,
# based on the various substitutions that are performed (see the following
# section).
#
#
# SUBSTITUTIONS
#
# The regular expressions can use the "named group" syntax to extract
# interesting pieces of the repository or commit path. These named values
# can then be substituted in the option values during mail generation.
#
# For example, let's say that you have a repository with a top-level
# directory named "clients", with several client projects underneath:
#
# REPOS/
# clients/
# gsvn/
# rapidsvn/
# winsvn/
#
# The client name can be extracted with a regular expression like:
#
# for_paths = clients/(?P<client>[^/]*)($|/)
#
# The substitution is performed using Python's dict-based string
# interpolation syntax:
#
# to_addr = commits@%(client)s.tigris.org
#
# The %(NAME)s syntax will substitute whatever value for NAME was captured
# in the for_repos and for_paths regular expressions. The set of names
# available is obtained from the following set of regular expressions:
#
# [defaults].for_repos (if present)
# [GROUP].for_repos (if present in the user-defined group "GROUP")
# [GROUP].for_paths (if present in the user-defined group "GROUP")
#
# The names from the regexes later in the list override the earlier names.
# If none of the groups match, but a for_paths is present in [defaults],
# then its extracted names will be available.
#
# Further suppose you want to match bug-ids in log messages:
#
# search_logmsg = (?P<bugid>(ProjA|ProjB)#\d)
#
# The bugids would be of the form ProjA#123 and ProjB#456. In this
# case, each time the regular expression matches, another match group
# will be generated. Thus, if you use:
#
# commit_subject_prefix = %(bugid)s:
#
# Then, a log message such as "Fixes ProjA#123 and ProjB#234" would
# match both bug-ids, and two emails would be generated - one with
# subject "ProjA#123: <...>" and "ProjB#234: <...>".
#
# Note that each unique set of names for substitution will generate an
# email. In the above example, if a commit modified files in all three
# client subdirectories, then an email will be sent to all three commits@
# mailing lists on tigris.org.
#
# The substitution variable "author" is provided by default, and is set
# to the author name passed to mailer.py for revprop changes or the
# author defined for a revision; if neither is available, then it is
# set to "no_author". Thus, you might define a line like:
#
# from_addr = %(author)s@example.com
#
# The substitution variable "repos_basename" is provided, and is set to
# the directory name of the repository. This can be useful to set
# a custom subject that can be re-used in multiple repositories:
#
# commit_subject_prefix = [svn-%(repos_basename)s]
#
# For example if the repository is at /path/to/repo/project-x then
# the subject of commit emails will be prefixed with [svn-project-x]
#
#
# SUMMARY
#
# While mailer.py will work to minimize the number of mail messages
# generated, a single commit can potentially generate a large number
# of variants of a commit message. The criteria for generating messages
# is based on:
#
# groups selected by for_repos
# groups selected by for_paths
# unique sets of parameters extracted by the above regular expressions
#
[defaults]
# This is not passed to the shell, so do not use shell metacharacters.
# The command is split around whitespace, so if you want to include
# whitespace in the command, then ### something ###.
diff = /usr/bin/diff -u -L %(label_from)s -L %(label_to)s %(from)s %(to)s
# The default prefix for the Subject: header for commits.
commit_subject_prefix =
# The default prefix for the Subject: header for propchanges.
propchange_subject_prefix =
# The default prefix for the Subject: header for locks.
lock_subject_prefix =
# The default prefix for the Subject: header for unlocks.
unlock_subject_prefix =
# The default From: address for messages. If the from_addr is not
# specified or it is specified but there is no text after the `=',
# then the revision's author is used as the from address. If the
# revision author is not specified, such as when a commit is done
# without requiring authentication and authorization, then the string
# 'no_author' is used. You can specify a default from_addr here and
# if you want to have a particular for_repos group use the author as
# the from address, you can use "from_addr =".
from_addr = invalid@example.com
# The default To: addresses for message. One or more addresses,
# separated by whitespace (no commas).
# NOTE: If you want to use a different character for separating the
# addresses put it in front of the addresses included in square
# brackets '[ ]'.
to_addr = invalid@example.com
# If this is set, then a Reply-To: will be inserted into the message.
reply_to =
# Specify which types of repository changes mailer.py will create
# diffs for. Valid options are any combination of
# 'add copy modify delete', or 'none' to never create diffs.
# If the generate_diffs option is empty, the selection is controlled
# by the deprecated options suppress_deletes and suppress_adds.
# Note that this only affects the display of diffs - all changes are
# mentioned in the summary of changed paths at the top of the message,
# regardless of this option's value.
# Meaning of the possible values:
# add: generates diffs for all added paths
# copy: generates diffs for all copied paths
# which were not changed after copying
# modify: generates diffs for all modified paths, including paths that were
# copied and modified afterwards (within the same commit)
# delete: generates diffs for all removed paths
generate_diffs = add copy modify
# Commit URL construction. This adds a URL to the top of the message
# that can lead the reader to a Trac, ViewVC or other view of the
# commit as a whole.
#
# The available substitution variable is: rev
#commit_url = http://diffs.server.com/trac/software/changeset/%(rev)s
# Diff URL construction. For the configured diff URL types, the diff
# section (which follows the message header) will include the URL
# relevant to the change type, even if actual diff generation for that
# change type is disabled (per the generate_diffs option).
#
# Available substitution variables are: path, base_path, rev, base_rev
#diff_add_url =
#diff_copy_url =
#diff_modify_url = http://diffs.server.com/?p1=%(base_path)s&p2=%(path)s
#diff_delete_url =
# When set to "yes", the mailer will suppress the creation of a diff which
# deletes all the lines in the file. If this is set to anything else, or
# is simply commented out, then the diff will be inserted. Note that the
# deletion is always mentioned in the message header, regardless of this
# option's value.
### DEPRECATED (if generate_diffs is not empty, this option is ignored)
#suppress_deletes = yes
# When set to "yes", the mailer will suppress the creation of a diff which
# adds all the lines in the file. If this is set to anything else, or
# is simply commented out, then the diff will be inserted. Note that the
# addition is always mentioned in the message header, regardless of this
# option's value.
### DEPRECATED (if generate_diffs is not empty, this option is ignored)
#suppress_adds = yes
# A revision is reported on if any of its changed paths match the
# for_paths option. If only some of the changed paths of a revision
# match, this variable controls the behaviour for the non-matching
# paths. Possible values are:
#
# yes: (Default) Show in both summary and diffs.
# summary: Show the changed paths in the summary, but omit the diffs.
# no: Show nothing more than a note saying "and changes in other areas"
#
show_nonmatching_paths = yes
# Subject line length limit. The generated subject line will be truncated
# and terminated with "...", to remain within the specified maximum length.
# Set to 0 to turn off.
#truncate_subject = 200
# --------------------------------------------------------------------------
[maps]
#
# This section can be used define rewrite mappings for option values. It
# is typically used for computing from/to addresses, but can actually be
# used to remap values for any option in this file.
#
# The mappings are global for the entire configuration file. There is
# no group-specific mapping capability. For each mapping that you want
# to perform, you will provide the name of the option (e.g. from_addr)
# and a specification of how to perform those mappings. These declarations
# are made here in the [maps] section.
#
# When an option is accessed, the value is loaded from the configuration
# file and all %(NAME)s substitutions are performed. The resulting value
# is then passed through the map. If a map entry is not available for
# the value, then it will be used unchanged.
#
# NOTES: - Avoid using map substitution names which differ only in case.
# Unexpected results may occur.
# - A colon ':' is also considered as separator between option and
# value (keep this in mind when trying to map a file path under
# windows).
#
# The format to declare a map is:
#
# option_name_to_remap = mapping_specification
#
# At the moment, there is only one type of mapping specification:
#
# mapping_specification = '[' sectionname ']'
#
# This will use the given section to map values. The option names in
# the section are the input values, and the option values are the result.
#
#
# EXAMPLE:
#
# We have two projects using two repositories. The name of the repos
# does not easily map to their commit mailing lists, so we will use
# a mapping to go from a project name (extracted from the repository
# path) to their commit list. The committers also need a special
# mapping to derive their email address from their repository username.
#
# [projects]
# for_repos = .*/(?P<project>.*)
# from_addr = %(author)s
# to_addr = %(project)s
#
# [maps]
# from_addr = [authors]
# to_addr = [mailing-lists]
#
# [authors]
# john = jconnor@example.com
# sarah = sconnor@example.com
#
# [mailing-lists]
# t600 = spottable-commits@example.com
# tx = hotness-commits@example.com
#
# --------------------------------------------------------------------------
#
# [example-group]
# # send notifications if any web pages are changed
# for_paths = .*\.html
# # set a custom prefix
# commit_subject_prefix = [commit]
# propchange_subject_prefix = [propchange]
# # override the default, sending these elsewhere
# to_addr = www-commits@example.com
# # use the revision author as the from address
# from_addr =
# # use a custom diff program for this group
# diff = /usr/bin/my-diff -u -L %(label_from)s -L %(label_to)s %(from)s %(to)s
#
# [another-example]
# # commits to personal repositories should go to that person
# for_repos = /home/(?P<who>[^/]*)/repos
# to_addr = %(who)s@example.com
#
# [issuetracker]
# search_logmsg = (?P<bugid>(?P<project>projecta|projectb|projectc)#\d+)
# # (or, use a mapping if the bug-id to email address is not this trivial)
# to_addr = %(project)s-tracker@example.com
# commit_subject_prefix = %(bugid)s:
# propchange_subject_prefix = %(bugid)s:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
#
# mailer.py: send email describing a commit
#
# $HeadURL$
# $LastChangedDate$
# $LastChangedBy$
# $LastChangedRevision$
#
# USAGE: mailer.py commit REPOS REVISION [CONFIG-FILE]
# mailer.py propchange REPOS REVISION AUTHOR REVPROPNAME [CONFIG-FILE]
# mailer.py propchange2 REPOS REVISION AUTHOR REVPROPNAME ACTION \
# [CONFIG-FILE]
# mailer.py lock REPOS AUTHOR [CONFIG-FILE]
# mailer.py unlock REPOS AUTHOR [CONFIG-FILE]
#
# Using CONFIG-FILE, deliver an email describing the changes between
# REV and REV-1 for the repository REPOS.
#
# ACTION was added as a fifth argument to the post-revprop-change hook
# in Subversion 1.2.0. Its value is one of 'A', 'M' or 'D' to indicate
# if the property was added, modified or deleted, respectively.
#
# See _MIN_SVN_VERSION below for which version of Subversion's Python
# bindings are required by this version of mailer.py.
import os
import sys
if sys.hexversion >= 0x3000000:
PY3 = True
import configparser
from urllib.parse import quote as _url_quote
else:
PY3 = False
import ConfigParser as configparser
from urllib import quote as _url_quote
import time
import subprocess
from io import BytesIO
import smtplib
import re
import tempfile
import codecs
# Minimal version of Subversion's bindings required
_MIN_SVN_VERSION = [1, 5, 0]
# Import the Subversion Python bindings, making sure they meet our
# minimum version requirements.
import svn.fs
import svn.delta
import svn.repos
import svn.core
if _MIN_SVN_VERSION > [svn.core.SVN_VER_MAJOR,
svn.core.SVN_VER_MINOR,
svn.core.SVN_VER_PATCH]:
sys.stderr.write(
"You need version %s or better of the Subversion Python bindings.\n" \
% ".".join([str(x) for x in _MIN_SVN_VERSION]))
sys.exit(1)
# Absorb difference between Python 2 and Python >= 3
if PY3:
def to_bytes(x):
return x.encode('utf-8')
def to_str(x):
return x.decode('utf-8')
# We never use sys.stdin nor sys.stdout TextIOwrapper.
_stdin = sys.stdin.buffer
_stdout = sys.stdout.buffer
else:
# Python 2
def to_bytes(x):
return x
def to_str(x):
return x
_stdin = sys.stdin
_stdout = sys.stdout
SEPARATOR = '=' * 78
def main(pool, cmd, config_fname, repos_dir, cmd_args):
### TODO: Sanity check the incoming args
if cmd == 'commit':
revision = int(cmd_args[0])
repos = Repository(repos_dir, revision, pool)
cfg = Config(config_fname, repos,
{'author': repos.author,
'repos_basename': os.path.basename(repos.repos_dir)
})
messenger = Commit(pool, cfg, repos)
elif cmd == 'propchange' or cmd == 'propchange2':
revision = int(cmd_args[0])
author = cmd_args[1]
propname = cmd_args[2]
if cmd == 'propchange2' and cmd_args[3]:
action = cmd_args[3]
else:
action = 'A'
repos = Repository(repos_dir, revision, pool)
# Override the repos revision author with the author of the propchange
repos.author = author
cfg = Config(config_fname, repos,
{'author': author,
'repos_basename': os.path.basename(repos.repos_dir)
})
messenger = PropChange(pool, cfg, repos, author, propname, action)
elif cmd == 'lock' or cmd == 'unlock':
author = cmd_args[0]
repos = Repository(repos_dir, 0, pool) ### any old revision will do
# Override the repos revision author with the author of the lock/unlock
repos.author = author
cfg = Config(config_fname, repos,
{'author': author,
'repos_basename': os.path.basename(repos.repos_dir)
})
messenger = Lock(pool, cfg, repos, author, cmd == 'lock')
else:
raise UnknownSubcommand(cmd)
return messenger.generate()
def remove_leading_slashes(path):
while path and path[0:1] == b'/':
path = path[1:]
return path
class OutputBase:
"Abstract base class to formalize the interface of output methods"
def __init__(self, cfg, repos, prefix_param):
self.cfg = cfg
self.repos = repos
self.prefix_param = prefix_param
self._CHUNKSIZE = 128 * 1024
# This is a public member variable. This must be assigned a suitable
# piece of descriptive text before make_subject() is called.
self.subject = ""
def make_subject(self, group, params):
prefix = self.cfg.get(self.prefix_param, group, params)
if prefix:
subject = prefix + ' ' + self.subject
else:
subject = self.subject
try:
truncate_subject = int(
self.cfg.get('truncate_subject', group, params))
except ValueError:
truncate_subject = 0
# truncate subject as UTF-8 string.
# Note: there still exists an issue on combining characters.
if truncate_subject:
bsubject = to_bytes(subject)
if len(bsubject) > truncate_subject:
idx = truncate_subject - 2
while b'\x80' <= bsubject[idx-1:idx] <= b'\xbf':
idx -= 1
subject = to_str(bsubject[:idx-1]) + "..."
return subject
def start(self, group, params):
"""Override this method.
Begin writing an output representation. GROUP is the name of the
configuration file group which is causing this output to be produced.
PARAMS is a dictionary of any named subexpressions of regular expressions
defined in the configuration file, plus the key 'author' contains the
author of the action being reported."""
raise NotImplementedError
def finish(self):
"""Override this method.
Flush any cached information and finish writing the output
representation."""
raise NotImplementedError
def write_binary(self, output):
"""Override this method.
Append the binary data OUTPUT to the output representation."""
raise NotImplementedError
def write(self, output):
"""Append the literal text string OUTPUT to the output representation."""
return self.write_binary(to_bytes(output))
def run(self, cmd):
"""Override this method, if the default implementation is not sufficient.
Execute CMD, writing the stdout produced to the output representation."""
# By default we choose to incorporate child stderr into the output
pipe_ob = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
close_fds=sys.platform != "win32")
buf = pipe_ob.stdout.read(self._CHUNKSIZE)
while buf:
self.write_binary(buf)
buf = pipe_ob.stdout.read(self._CHUNKSIZE)
# wait on the child so we don't end up with a billion zombies
pipe_ob.wait()
class MailedOutput(OutputBase):
def __init__(self, cfg, repos, prefix_param):
OutputBase.__init__(self, cfg, repos, prefix_param)
def start(self, group, params):
# whitespace (or another character) separated list of addresses
# which must be split into a clean list
to_addr_in = self.cfg.get('to_addr', group, params)
# if list of addresses starts with '[.]'
# use the character between the square brackets as split char
# else use whitespaces
if len(to_addr_in) >= 3 and to_addr_in[0] == '[' \
and to_addr_in[2] == ']':
self.to_addrs = \
[_f for _f in to_addr_in[3:].split(to_addr_in[1]) if _f]
else:
self.to_addrs = [_f for _f in to_addr_in.split() if _f]
self.from_addr = self.cfg.get('from_addr', group, params) \
or self.repos.author or 'no_author'
# if the from_addr (also) starts with '[.]' (may happen if one
# map is used for both to_addr and from_addr) remove '[.]'
if len(self.from_addr) >= 3 and self.from_addr[0] == '[' \
and self.from_addr[2] == ']':
self.from_addr = self.from_addr[3:]
self.reply_to = self.cfg.get('reply_to', group, params)
# if the reply_to (also) starts with '[.]' (may happen if one
# map is used for both to_addr and reply_to) remove '[.]'
if len(self.reply_to) >= 3 and self.reply_to[0] == '[' \
and self.reply_to[2] == ']':
self.reply_to = self.reply_to[3:]
def _rfc2047_encode(self, hdr):
# Return the result of splitting HDR into tokens (on space
# characters), encoding (per RFC2047) each token as necessary, and
# slapping 'em back to together again.
from email.header import Header
def _maybe_encode_header(hdr_token):
try:
hdr_token.encode('ascii')
return hdr_token
except UnicodeError:
return Header(hdr_token, 'utf-8').encode()
return ' '.join(map(_maybe_encode_header, hdr.split()))
def mail_headers(self, group, params):
from email import utils
subject = self._rfc2047_encode(self.make_subject(group, params))
from_hdr = self._rfc2047_encode(self.from_addr)
to_hdr = self._rfc2047_encode(', '.join(self.to_addrs))
hdrs = 'From: %s\n' \
'To: %s\n' \
'Subject: %s\n' \
'Date: %s\n' \
'Message-ID: %s\n' \
'MIME-Version: 1.0\n' \
'Content-Type: text/plain; charset=UTF-8\n' \
'Content-Transfer-Encoding: 8bit\n' \
'X-Svn-Commit-Project: %s\n' \
'X-Svn-Commit-Author: %s\n' \
'X-Svn-Commit-Revision: %d\n' \
'X-Svn-Commit-Repository: %s\n' \
% (from_hdr, to_hdr, subject,
utils.formatdate(), utils.make_msgid(), group,
self.repos.author or 'no_author', self.repos.rev,
os.path.basename(self.repos.repos_dir))
if self.reply_to:
hdrs = '%sReply-To: %s\n' % (hdrs, self.reply_to)
return hdrs + '\n'
class SMTPOutput(MailedOutput):
"Deliver a mail message to an MTA using SMTP."
def start(self, group, params):
MailedOutput.start(self, group, params)
self.buffer = BytesIO()
self.write_binary = self.buffer.write
self.write(self.mail_headers(group, params))
def finish(self):
"""
Send email via SMTP or SMTP_SSL, logging in if username is
specified.
Errors such as invalid recipient, which affect a particular email,
are reported to stderr and raise MessageSendFailure. If the caller
has other emails to send, it may continue doing so.
Errors caused by bad configuration, such as login failures, for
which too many occurrences could lead to SMTP server lockout, are
reported to stderr and re-raised. These should be considered fatal
(to minimize the chances of said lockout).
"""
if self.cfg.is_set('general.smtp_port'):
smtp_port = self.cfg.general.smtp_port
else:
smtp_port = 0
try:
if self.cfg.is_set('general.smtp_ssl') and self.cfg.general.smtp_ssl == 'yes':
server = smtplib.SMTP_SSL(self.cfg.general.smtp_hostname, smtp_port)
else:
server = smtplib.SMTP(self.cfg.general.smtp_hostname, smtp_port)
except Exception as detail:
sys.stderr.write("mailer.py: Failed to instantiate SMTP object: %s\n" % (detail,))
# Any error to instantiate is fatal
raise
try:
if self.cfg.is_set('general.smtp_username'):
try:
server.login(self.cfg.general.smtp_username,
self.cfg.general.smtp_password)
except smtplib.SMTPException as detail:
sys.stderr.write("mailer.py: SMTP login failed with username %s and/or password: %s\n"
% (self.cfg.general.smtp_username, detail,))
# Any error at login is fatal
raise
server.sendmail(self.from_addr, self.to_addrs, self.buffer.getvalue())
### TODO: 'raise .. from' is Python 3+. When we convert this
### script to Python 3, uncomment 'from detail' below
### (2 instances):
except smtplib.SMTPRecipientsRefused as detail:
sys.stderr.write("mailer.py: SMTP recipient(s) refused: %s: %s\n"
% (self.to_addrs, detail,))
raise MessageSendFailure ### from detail
except smtplib.SMTPSenderRefused as detail:
sys.stderr.write("mailer.py: SMTP sender refused: %s: %s\n"
% (self.from_addr, detail,))
raise MessageSendFailure ### from detail
except smtplib.SMTPException as detail:
# All other errors are fatal; this includes:
# SMTPHeloError, SMTPDataError, SMTPNotSupportedError
sys.stderr.write("mailer.py: SMTP error occurred: %s\n" % (detail,))
raise
finally:
try:
server.quit()
except smtplib.SMTPException as detail:
sys.stderr.write("mailer.py: Error occurred during SMTP session cleanup: %s\n"
% (detail,))
class StandardOutput(OutputBase):
"Print the commit message to stdout."
def __init__(self, cfg, repos, prefix_param):
OutputBase.__init__(self, cfg, repos, prefix_param)
self.write_binary = _stdout.write
def start(self, group, params):
self.write("Group: " + (group or "defaults") + "\n")
self.write("Subject: " + self.make_subject(group, params) + "\n\n")
def finish(self):
pass
if (PY3 and (codecs.lookup(sys.stdout.encoding) != codecs.lookup('utf-8'))):
def write(self, output):
"""Write text as *default* encoding string"""
return self.write_binary(output.encode(sys.stdout.encoding,
'backslashreplace'))
class PipeOutput(MailedOutput):
"Deliver a mail message to an MTA via a pipe."
def __init__(self, cfg, repos, prefix_param):
MailedOutput.__init__(self, cfg, repos, prefix_param)
# figure out the command for delivery
self.cmd = cfg.general.mail_command.split()
def start(self, group, params):
MailedOutput.start(self, group, params)
### gotta fix this. this is pretty specific to sendmail and qmail's
### mailwrapper program. should be able to use option param substitution
cmd = self.cmd + [ '-f', self.from_addr ] + self.to_addrs
# construct the pipe for talking to the mailer
self.pipe = subprocess.Popen(cmd, stdin=subprocess.PIPE,
close_fds=sys.platform != "win32")
self.write_binary = self.pipe.stdin.write
# start writing out the mail message
self.write(self.mail_headers(group, params))
def finish(self):
# signal that we're done sending content
self.pipe.stdin.close()
# wait to avoid zombies
self.pipe.wait()
class Messenger:
def __init__(self, pool, cfg, repos, prefix_param):
self.pool = pool
self.cfg = cfg
self.repos = repos
if cfg.is_set('general.mail_command'):
cls = PipeOutput
elif cfg.is_set('general.smtp_hostname'):
cls = SMTPOutput
else:
cls = StandardOutput
self.output = cls(cfg, repos, prefix_param)
class Commit(Messenger):
def __init__(self, pool, cfg, repos):
Messenger.__init__(self, pool, cfg, repos, 'commit_subject_prefix')
# get all the changes and sort by path
editor = svn.repos.ChangeCollector(repos.fs_ptr, repos.root_this, \
self.pool)
e_ptr, e_baton = svn.delta.make_editor(editor, self.pool)
svn.repos.replay2(repos.root_this, "", svn.core.SVN_INVALID_REVNUM, 1, e_ptr, e_baton, None, self.pool)
self.changelist = sorted(editor.get_changes().items())
log = to_str(repos.get_rev_prop(svn.core.SVN_PROP_REVISION_LOG) or b'')
# collect the set of groups and the unique sets of params for the options
self.groups = { }
for path, change in self.changelist:
for (group, params) in self.cfg.which_groups(path, log):
# turn the params into a hashable object and stash it away
param_list = sorted(params.items())
# collect the set of paths belonging to this group
if (group, tuple(param_list)) in self.groups:
old_param, paths = self.groups[group, tuple(param_list)]
else:
paths = { }
paths[path] = None
self.groups[group, tuple(param_list)] = (params, paths)
# figure out the changed directories
dirs = { }
for path, change in self.changelist:
path = to_str(path)
if change.item_kind == svn.core.svn_node_dir:
dirs[path] = None
else:
idx = path.rfind('/')
if idx == -1:
dirs[''] = None
else:
dirs[path[:idx]] = None
dirlist = list(dirs.keys())
commondir, dirlist = get_commondir(dirlist)
# compose the basic subject line. later, we can prefix it.
dirlist.sort()
dirlist = ' '.join(dirlist)
if commondir:
self.output.subject = 'r%d - in %s: %s' % (repos.rev, commondir, dirlist)
else:
self.output.subject = 'r%d - %s' % (repos.rev, dirlist)
def generate(self):
"Generate email for the various groups and option-params."
### the groups need to be further compressed. if the headers and
### body are the same across groups, then we can have multiple To:
### addresses. SMTPOutput holds the entire message body in memory,
### so if the body doesn't change, then it can be sent N times
### rather than rebuilding it each time.
subpool = svn.core.svn_pool_create(self.pool)
ret = 0
# build a renderer, tied to our output stream
renderer = TextCommitRenderer(self.output)
for (group, param_tuple), (params, paths) in sorted(self.groups.items()):
try:
self.output.start(group, params)
# generate the content for this group and set of params
generate_content(renderer, self.cfg, self.repos, self.changelist,
group, params, paths, subpool)
self.output.finish()
except MessageSendFailure:
ret = 1
svn.core.svn_pool_clear(subpool)
svn.core.svn_pool_destroy(subpool)
return ret
class PropChange(Messenger):
def __init__(self, pool, cfg, repos, author, propname, action):
Messenger.__init__(self, pool, cfg, repos, 'propchange_subject_prefix')
self.author = author
self.propname = propname
self.action = action
# collect the set of groups and the unique sets of params for the options
self.groups = { }
for (group, params) in self.cfg.which_groups('', None):
# turn the params into a hashable object and stash it away
param_list = sorted(params.items())
self.groups[group, tuple(param_list)] = params
self.output.subject = 'r%d - %s' % (repos.rev, propname)
def generate(self):
actions = { 'A': 'added', 'M': 'modified', 'D': 'deleted' }
ret = 0
for (group, param_tuple), params in self.groups.items():
try:
self.output.start(group, params)
self.output.write('Author: %s\n'
'Revision: %s\n'
'Property Name: %s\n'
'Action: %s\n'
'\n'
% (self.author, self.repos.rev, self.propname,
actions.get(self.action, 'Unknown (\'%s\')' \
% self.action)))
if self.action == 'A' or self.action not in actions:
self.output.write('Property value:\n')
propvalue = self.repos.get_rev_prop(self.propname)
self.output.write(propvalue)
elif self.action == 'M':
self.output.write('Property diff:\n')
tempfile1 = tempfile.NamedTemporaryFile()
tempfile1.write(_stdin.read())
tempfile1.flush()
tempfile2 = tempfile.NamedTemporaryFile()
tempfile2.write(self.repos.get_rev_prop(self.propname))
tempfile2.flush()
self.output.run(self.cfg.get_diff_cmd(group, {
'label_from' : 'old property value',
'label_to' : 'new property value',
'from' : tempfile1.name,
'to' : tempfile2.name,
}))
self.output.finish()
except MessageSendFailure:
ret = 1
return ret
def get_commondir(dirlist):
"""Figure out the common portion/parent (commondir) of all the paths
in DIRLIST and return a tuple consisting of commondir, dirlist. If
a commondir is found, the dirlist returned is rooted in that
commondir. If no commondir is found, dirlist is returned unchanged,
and commondir is the empty string."""
if len(dirlist) < 2 or '/' in dirlist:
commondir = ''
newdirs = dirlist
else:
common = dirlist[0].split('/')
for j in range(1, len(dirlist)):
d = dirlist[j]
parts = d.split('/')
for i in range(len(common)):
if i == len(parts) or common[i] != parts[i]:
del common[i:]
break
commondir = '/'.join(common)
if commondir:
# strip the common portion from each directory
l = len(commondir) + 1
newdirs = [ ]
for d in dirlist:
if d == commondir:
newdirs.append('.')
else:
newdirs.append(d[l:])
else:
# nothing in common, so reset the list of directories
newdirs = dirlist
return commondir, newdirs
class Lock(Messenger):
def __init__(self, pool, cfg, repos, author, do_lock):
self.author = author
self.do_lock = do_lock
Messenger.__init__(self, pool, cfg, repos,
(do_lock and 'lock_subject_prefix'
or 'unlock_subject_prefix'))
# read all the locked paths from STDIN and strip off the trailing newlines
self.dirlist = [to_str(x).rstrip() for x in _stdin.readlines()]
# collect the set of groups and the unique sets of params for the options
self.groups = { }
for path in self.dirlist:
for (group, params) in self.cfg.which_groups(path, None):
# turn the params into a hashable object and stash it away
param_list = sorted(params.items())
# collect the set of paths belonging to this group
if (group, tuple(param_list)) in self.groups:
old_param, paths = self.groups[group, tuple(param_list)]
else:
paths = { }
paths[path] = None
self.groups[group, tuple(param_list)] = (params, paths)
commondir, dirlist = get_commondir(self.dirlist)
# compose the basic subject line. later, we can prefix it.
dirlist.sort()
dirlist = ' '.join(dirlist)
if commondir:
self.output.subject = '%s: %s' % (commondir, dirlist)
else:
self.output.subject = '%s' % (dirlist)
# The lock comment is the same for all paths, so we can just pull
# the comment for the first path in the dirlist and cache it.
self.lock = svn.fs.svn_fs_get_lock(self.repos.fs_ptr,
to_bytes(self.dirlist[0]),
self.pool)
def generate(self):
ret = 0
for (group, param_tuple), (params, paths) in sorted(self.groups.items()):
try:
self.output.start(group, params)
self.output.write('Author: %s\n'
'%s paths:\n' %
(self.author, self.do_lock and 'Locked' or 'Unlocked'))
self.dirlist.sort()
for dir in self.dirlist:
self.output.write(' %s\n\n' % dir)
if self.do_lock:
self.output.write('Comment:\n%s\n' % (self.lock.comment or ''))
self.output.finish()
except MessageSendFailure:
ret = 1
return ret
class DiffSelections:
def __init__(self, cfg, group, params):
self.add = False
self.copy = False
self.delete = False
self.modify = False
gen_diffs = cfg.get('generate_diffs', group, params)
### Do a little dance for deprecated options. Note that even if you
### don't have an option anywhere in your configuration file, it
### still gets returned as non-None.
if len(gen_diffs):
list = gen_diffs.split(" ")
for item in list:
if item == 'add':
self.add = True
if item == 'copy':
self.copy = True
if item == 'delete':
self.delete = True
if item == 'modify':
self.modify = True
else:
self.add = True
self.copy = True
self.delete = True
self.modify = True
### These options are deprecated
suppress = cfg.get('suppress_deletes', group, params)
if suppress == 'yes':
self.delete = False
suppress = cfg.get('suppress_adds', group, params)
if suppress == 'yes':
self.add = False
class DiffURLSelections:
def __init__(self, cfg, group, params):
self.cfg = cfg
self.group = group
self.params = params
def _get_url(self, action, repos_rev, change):
# The parameters for the URLs generation need to be placed in the
# parameters for the configuration module, otherwise we may get
# KeyError exceptions.
params = self.params.copy()
params['path'] = _url_quote(change.path) if change.path else None
params['base_path'] = (_url_quote(change.base_path)
if change.base_path else None)
params['rev'] = repos_rev
params['base_rev'] = change.base_rev
return self.cfg.get("diff_%s_url" % action, self.group, params)
def get_add_url(self, repos_rev, change):
return self._get_url('add', repos_rev, change)
def get_copy_url(self, repos_rev, change):
return self._get_url('copy', repos_rev, change)
def get_delete_url(self, repos_rev, change):
return self._get_url('delete', repos_rev, change)
def get_modify_url(self, repos_rev, change):
return self._get_url('modify', repos_rev, change)
def generate_content(renderer, cfg, repos, changelist, group, params, paths,
pool):
svndate = repos.get_rev_prop(svn.core.SVN_PROP_REVISION_DATE)
### pick a different date format?
date = time.ctime(svn.core.secs_from_timestr(svndate, pool))
diffsels = DiffSelections(cfg, group, params)
diffurls = DiffURLSelections(cfg, group, params)
show_nonmatching_paths = cfg.get('show_nonmatching_paths', group, params) \
or 'yes'
params_with_rev = params.copy()
params_with_rev['rev'] = repos.rev
commit_url = cfg.get('commit_url', group, params_with_rev)
# figure out the lists of changes outside the selected path-space
other_added_data = other_replaced_data = other_deleted_data = \
other_modified_data = [ ]
if len(paths) != len(changelist) and show_nonmatching_paths != 'no':
other_added_data = generate_list('A', changelist, paths, False)
other_replaced_data = generate_list('R', changelist, paths, False)
other_deleted_data = generate_list('D', changelist, paths, False)
other_modified_data = generate_list('M', changelist, paths, False)
if len(paths) != len(changelist) and show_nonmatching_paths == 'yes':
other_diffs = DiffGenerator(changelist, paths, False, cfg, repos, date,
group, params, diffsels, diffurls, pool)
else:
other_diffs = None
data = _data(
author=repos.author,
date=date,
rev=repos.rev,
log=to_str(repos.get_rev_prop(svn.core.SVN_PROP_REVISION_LOG) or b''),
commit_url=commit_url,
added_data=generate_list('A', changelist, paths, True),
replaced_data=generate_list('R', changelist, paths, True),
deleted_data=generate_list('D', changelist, paths, True),
modified_data=generate_list('M', changelist, paths, True),
show_nonmatching_paths=show_nonmatching_paths,
other_added_data=other_added_data,
other_replaced_data=other_replaced_data,
other_deleted_data=other_deleted_data,
other_modified_data=other_modified_data,
diffs=DiffGenerator(changelist, paths, True, cfg, repos, date, group,
params, diffsels, diffurls, pool),
other_diffs=other_diffs,
)
renderer.render(data)
def generate_list(changekind, changelist, paths, in_paths):
if changekind == 'A':
selection = lambda change: change.action == svn.repos.CHANGE_ACTION_ADD
elif changekind == 'R':
selection = lambda change: change.action == svn.repos.CHANGE_ACTION_REPLACE
elif changekind == 'D':
selection = lambda change: change.action == svn.repos.CHANGE_ACTION_DELETE
elif changekind == 'M':
selection = lambda change: change.action == svn.repos.CHANGE_ACTION_MODIFY
items = [ ]
for path, change in changelist:
if selection(change) and (path in paths) == in_paths:
item = _data(
path=path,
is_dir=change.item_kind == svn.core.svn_node_dir,
props_changed=change.prop_changes,
text_changed=change.text_changed,
copied=(change.action == svn.repos.CHANGE_ACTION_ADD \
or change.action == svn.repos.CHANGE_ACTION_REPLACE) \
and change.base_path,
base_path=remove_leading_slashes(change.base_path),
base_rev=change.base_rev,
)
items.append(item)
return items
class DiffGenerator:
"This is a generator-like object returning DiffContent objects."
def __init__(self, changelist, paths, in_paths, cfg, repos, date, group,
params, diffsels, diffurls, pool):
self.changelist = changelist
self.paths = paths
self.in_paths = in_paths
self.cfg = cfg
self.repos = repos
self.date = date
self.group = group
self.params = params
self.diffsels = diffsels
self.diffurls = diffurls
self.pool = pool
self.diff = self.diff_url = None
self.idx = 0
def __nonzero__(self):
# we always have some items
return True
def __getitem__(self, idx):
while True:
if self.idx == len(self.changelist):
raise IndexError
path, change = self.changelist[self.idx]
self.idx = self.idx + 1
diff = diff_url = None
kind = None
label1 = None
label2 = None
src_fname = None
dst_fname = None
binary = None
singular = None
content = None
# just skip directories. they have no diffs.
if change.item_kind == svn.core.svn_node_dir:
continue
# is this change in (or out of) the set of matched paths?
if (path in self.paths) != self.in_paths:
continue
if change.base_rev != -1:
svndate = self.repos.get_rev_prop(svn.core.SVN_PROP_REVISION_DATE,
change.base_rev)
### pick a different date format?
base_date = time.ctime(svn.core.secs_from_timestr(svndate, self.pool))
else:
base_date = ''
# figure out if/how to generate a diff
base_path_bytes = remove_leading_slashes(change.base_path)
base_path = (to_str(base_path_bytes)
if base_path_bytes is not None else None)
if change.action == svn.repos.CHANGE_ACTION_DELETE:
# it was delete.
kind = 'D'
# get the diff url, if any is specified
diff_url = self.diffurls.get_delete_url(self.repos.rev, change)
# show the diff?
if self.diffsels.delete:
diff = svn.fs.FileDiff(self.repos.get_root(change.base_rev),
base_path_bytes, None, None, self.pool)
label1 = '%s\t%s\t(r%s)' % (base_path, self.date, change.base_rev)
label2 = '/dev/null\t00:00:00 1970\t(deleted)'
singular = True
elif change.action == svn.repos.CHANGE_ACTION_ADD \
or change.action == svn.repos.CHANGE_ACTION_REPLACE:
if base_path and (change.base_rev != -1):
# any diff of interest?
if change.text_changed:
# this file was copied and modified.
kind = 'W'
# get the diff url, if any is specified
diff_url = self.diffurls.get_copy_url(self.repos.rev, change)
# show the diff?
if self.diffsels.modify:
diff = svn.fs.FileDiff(self.repos.get_root(change.base_rev),
base_path_bytes,
self.repos.root_this, change.path,
self.pool)
label1 = ('%s\t%s\t(r%s, copy source)'
% (base_path, base_date, change.base_rev))
label2 = ('%s\t%s\t(r%s)'
% (to_str(change.path), self.date, self.repos.rev))
singular = False
else:
# this file was copied.
kind = 'C'
if self.diffsels.copy:
diff = svn.fs.FileDiff(None, None, self.repos.root_this,
change.path, self.pool)
label1 = ('/dev/null\t00:00:00 1970\t'
'(empty, because file is newly added)')
label2 = ('%s\t%s\t(r%s, copy of r%s, %s)'
% (to_str(change.path),
self.date, self.repos.rev, change.base_rev,
base_path))
singular = False
else:
# the file was added.
kind = 'A'
# get the diff url, if any is specified
diff_url = self.diffurls.get_add_url(self.repos.rev, change)
# show the diff?
if self.diffsels.add:
diff = svn.fs.FileDiff(None, None, self.repos.root_this,
change.path, self.pool)
label1 = '/dev/null\t00:00:00 1970\t' \
'(empty, because file is newly added)'
label2 = '%s\t%s\t(r%s)' \
% (to_str(change.path), self.date, self.repos.rev)
singular = True
elif not change.text_changed:
# the text didn't change, so nothing to show.
continue
else:
# a simple modification.
kind = 'M'
# get the diff url, if any is specified
diff_url = self.diffurls.get_modify_url(self.repos.rev, change)
# show the diff?
if self.diffsels.modify:
diff = svn.fs.FileDiff(self.repos.get_root(change.base_rev),
base_path,
self.repos.root_this, change.path,
self.pool)
label1 = '%s\t%s\t(r%s)' \
% (base_path, base_date, change.base_rev)
label2 = '%s\t%s\t(r%s)' \
% (to_str(change.path), self.date, self.repos.rev)
singular = False
if diff:
binary = diff.either_binary()
if binary:
content = src_fname = dst_fname = None
else:
src_fname, dst_fname = diff.get_files()
try:
content = DiffContent(self.cfg.get_diff_cmd(self.group, {
'label_from' : label1,
'label_to' : label2,
'from' : src_fname,
'to' : dst_fname,
}))
except OSError:
# diff command does not exist, try difflib.unified_diff()
content = DifflibDiffContent(label1, label2, src_fname, dst_fname)
# return a data item for this diff
return _data(
path=change.path,
base_path=base_path_bytes,
base_rev=change.base_rev,
diff=diff,
diff_url=diff_url,
kind=kind,
label_from=label1,
label_to=label2,
from_fname=src_fname,
to_fname=dst_fname,
binary=binary,
singular=singular,
content=content,
)
def _classify_diff_line(line, seen_change):
# classify the type of line.
first = line[:1]
ltype = ''
if first == '@':
seen_change = True
ltype = 'H'
elif first == '-':
if seen_change:
ltype = 'D'
else:
ltype = 'F'
elif first == '+':
if seen_change:
ltype = 'A'
else:
ltype = 'T'
elif first == ' ':
ltype = 'C'
else:
ltype = 'U'
if line[-2] == '\r':
line=line[0:-2] + '\n' # remove carriage return
return line, ltype, seen_change
class DiffContent:
"This is a generator-like object returning annotated lines of a diff."
def __init__(self, cmd):
self.seen_change = False
# By default we choose to incorporate child stderr into the output
self.pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
close_fds=sys.platform != "win32")
def __nonzero__(self):
# we always have some items
return True
def __getitem__(self, idx):
if self.pipe is None:
raise IndexError
line = self.pipe.stdout.readline()
if not line:
# wait on the child so we don't end up with a billion zombies
self.pipe.wait()
self.pipe = None
raise IndexError
line, ltype, self.seen_change = _classify_diff_line(line, self.seen_change)
return _data(
raw=line,
text=line[1:-1], # remove indicator and newline
type=ltype,
)
class DifflibDiffContent():
"This is a generator-like object returning annotated lines of a diff."
def __init__(self, label_from, label_to, from_file, to_file):
import difflib
self.seen_change = False
fromlines = open(from_file, 'U').readlines()
tolines = open(to_file, 'U').readlines()
self.diff = difflib.unified_diff(fromlines, tolines,
label_from, label_to)
def __nonzero__(self):
# we always have some items
return True
def __getitem__(self, idx):
try:
line = self.diff.next()
except StopIteration:
raise IndexError
line, ltype, self.seen_change = _classify_diff_line(line, self.seen_change)
return _data(
raw=line,
text=line[1:-1], # remove indicator and newline
type=ltype,
)
class TextCommitRenderer:
"This class will render the commit mail in plain text."
def __init__(self, output):
self.output = output
def render(self, data):
"Render the commit defined by 'data'."
w = self.output.write
w('Author: %s\nDate: %s\nNew Revision: %s\n' % (data.author,
data.date,
data.rev))
if data.commit_url:
w('URL: %s\n\n' % data.commit_url)
else:
w('\n')
w('Log:\n%s\n\n' % data.log.strip())
# print summary sections
self._render_list('Added', data.added_data)
self._render_list('Replaced', data.replaced_data)
self._render_list('Deleted', data.deleted_data)
self._render_list('Modified', data.modified_data)
if data.other_added_data or data.other_replaced_data \
or data.other_deleted_data or data.other_modified_data:
if data.show_nonmatching_paths:
w('\nChanges in other areas also in this revision:\n')
self._render_list('Added', data.other_added_data)
self._render_list('Replaced', data.other_replaced_data)
self._render_list('Deleted', data.other_deleted_data)
self._render_list('Modified', data.other_modified_data)
else:
w('and changes in other areas\n')
self._render_diffs(data.diffs, '')
if data.other_diffs:
self._render_diffs(data.other_diffs,
'\nDiffs of changes in other areas also'
' in this revision:\n')
def _render_list(self, header, data_list):
if not data_list:
return
w = self.output.write
w(header + ':\n')
for d in data_list:
if d.is_dir:
is_dir = '/'
else:
is_dir = ''
if d.props_changed:
if d.text_changed:
props = ' (contents, props changed)'
else:
props = ' (props changed)'
else:
props = ''
w(' %s%s%s\n' % (to_str(d.path), is_dir, props))
if d.copied:
if is_dir:
text = ''
elif d.text_changed:
text = ', changed'
else:
text = ' unchanged'
w(' - copied%s from r%d, %s%s\n'
% (text, d.base_rev, to_str(d.base_path), is_dir))
def _render_diffs(self, diffs, section_header):
"""Render diffs. Write the SECTION_HEADER if there are actually
any diffs to render."""
if not diffs:
return
w = self.output.write
section_header_printed = False
for diff in diffs:
if not diff.diff and not diff.diff_url:
continue
if not section_header_printed:
w(section_header)
section_header_printed = True
if diff.kind == 'D':
w('\nDeleted: %s\n' % to_str(diff.base_path))
elif diff.kind == 'A':
w('\nAdded: %s\n' % to_str(diff.path))
elif diff.kind == 'C':
w('\nCopied: %s (from r%d, %s)\n'
% (to_str(diff.path), diff.base_rev,
to_str(diff.base_path)))
elif diff.kind == 'W':
w('\nCopied and modified: %s (from r%d, %s)\n'
% (to_str(diff.path), diff.base_rev,
to_str(diff.base_path)))
else:
# kind == 'M'
w('\nModified: %s\n' % to_str(diff.path))
if diff.diff_url:
w('URL: %s\n' % diff.diff_url)
if not diff.diff:
continue
w(SEPARATOR + '\n')
if diff.binary:
if diff.singular:
w('Binary file. No diff available.\n')
else:
w('Binary file (source and/or target). No diff available.\n')
continue
wb = self.output.write_binary
for line in diff.content:
wb(line.raw)
class Repository:
"Hold roots and other information about the repository."
def __init__(self, repos_dir, rev, pool):
self.repos_dir = repos_dir
self.rev = rev
self.pool = pool
self.repos_ptr = svn.repos.open(repos_dir, pool)
self.fs_ptr = svn.repos.fs(self.repos_ptr)
self.roots = { }
self.root_this = self.get_root(rev)
self.author = self.get_rev_prop(svn.core.SVN_PROP_REVISION_AUTHOR)
if self.author is not None:
self.author = to_str(self.author)
def get_rev_prop(self, propname, rev = None):
if not rev:
rev = self.rev
return svn.fs.revision_prop(self.fs_ptr, rev, propname, self.pool)
def get_root(self, rev):
try:
return self.roots[rev]
except KeyError:
pass
root = self.roots[rev] = svn.fs.revision_root(self.fs_ptr, rev, self.pool)
return root
class Config:
# The predefined configuration sections. These are omitted from the
# set of groups.
_predefined = ('general', 'defaults', 'maps')
def __init__(self, fname, repos, global_params):
cp = configparser.ConfigParser()
cp.read(fname)
# record the (non-default) groups that we find
self._groups = [ ]
for section in cp.sections():
if not hasattr(self, section):
section_ob = _sub_section()
setattr(self, section, section_ob)
if section not in self._predefined:
self._groups.append(section)
else:
section_ob = getattr(self, section)
for option in cp.options(section):
# get the raw value -- we use the same format for *our* interpolation
value = cp.get(section, option, raw=1)
setattr(section_ob, option, value)
# be compatible with old format config files
if hasattr(self.general, 'diff') and not hasattr(self.defaults, 'diff'):
self.defaults.diff = self.general.diff
if not hasattr(self, 'maps'):
self.maps = _sub_section()
# these params are always available, although they may be overridden
self._global_params = global_params.copy()
# prepare maps. this may remove sections from consideration as a group.
self._prep_maps()
# process all the group sections.
self._prep_groups(repos)
def is_set(self, option):
"""Return None if the option is not set; otherwise, its value is returned.
The option is specified as a dotted symbol, such as 'general.mail_command'
"""
ob = self
for part in option.split('.'):
if not hasattr(ob, part):
return None
ob = getattr(ob, part)
return ob
def get(self, option, group, params):
"Get a config value with appropriate substitutions and value mapping."
# find the right value
value = None
if group:
sub = getattr(self, group)
value = getattr(sub, option, None)
if value is None:
value = getattr(self.defaults, option, '')
# parameterize it
if params is not None:
value = value % params
# apply any mapper
mapper = getattr(self.maps, option, None)
if mapper is not None:
value = mapper(value)
# Apply any parameters that may now be available for
# substitution that were not before the mapping.
if value is not None and params is not None:
value = value % params
return value
def get_diff_cmd(self, group, args):
"Get a diff command as a list of argv elements."
### do some better splitting to enable quoting of spaces
diff_cmd = self.get('diff', group, None).split()
cmd = [ ]
for part in diff_cmd:
cmd.append(part % args)
return cmd
def _prep_maps(self):
"Rewrite the [maps] options into callables that look up values."
mapsections = []
for optname, mapvalue in vars(self.maps).items():
if mapvalue[:1] == '[':
# a section is acting as a mapping
sectname = mapvalue[1:-1]
if not hasattr(self, sectname):
raise UnknownMappingSection(sectname)
# construct a lambda to look up the given value as an option name,
# and return the option's value. if the option is not present,
# then just return the value unchanged.
setattr(self.maps, optname,
lambda value,
sect=getattr(self, sectname): getattr(sect,
value.lower(),
value))
# mark for removal when all optnames are done
if sectname not in mapsections:
mapsections.append(sectname)
# elif test for other mapper types. possible examples:
# dbm:filename.db
# file:two-column-file.txt
# ldap:some-query-spec
# just craft a mapper function and insert it appropriately
else:
raise UnknownMappingSpec(mapvalue)
# remove each mapping section from consideration as a group
for sectname in mapsections:
self._groups.remove(sectname)
def _prep_groups(self, repos):
self._group_re = [ ]
repos_dir = os.path.abspath(repos.repos_dir)
# compute the default repository-based parameters. start with some
# basic parameters, then bring in the regex-based params.
self._default_params = self._global_params
try:
match = re.match(self.defaults.for_repos, repos_dir)
if match:
self._default_params = self._default_params.copy()
self._default_params.update(match.groupdict())
except AttributeError:
# there is no self.defaults.for_repos
pass
# select the groups that apply to this repository
for group in self._groups:
sub = getattr(self, group)
params = self._default_params
if hasattr(sub, 'for_repos'):
match = re.match(sub.for_repos, repos_dir)
if not match:
continue
params = params.copy()
params.update(match.groupdict())
# if a matching rule hasn't been given, then use the empty string
# as it will match all paths
for_paths = getattr(sub, 'for_paths', '')
exclude_paths = getattr(sub, 'exclude_paths', None)
if exclude_paths:
exclude_paths_re = re.compile(exclude_paths)
else:
exclude_paths_re = None
# check search_logmsg re
search_logmsg = getattr(sub, 'search_logmsg', None)
if search_logmsg is not None:
search_logmsg_re = re.compile(search_logmsg)
else:
search_logmsg_re = None
self._group_re.append((group,
re.compile(for_paths),
exclude_paths_re,
params,
search_logmsg_re))
# after all the groups are done, add in the default group
try:
self._group_re.append((None,
re.compile(self.defaults.for_paths),
None,
self._default_params,
None))
except AttributeError:
# there is no self.defaults.for_paths
pass
def which_groups(self, path, logmsg):
"Return the path's associated groups."
groups = []
for group, pattern, exclude_pattern, repos_params, search_logmsg_re in self._group_re:
match = pattern.match(to_str(path))
if match:
if exclude_pattern and exclude_pattern.match(to_str(path)):
continue
params = repos_params.copy()
params.update(match.groupdict())
if search_logmsg_re is None:
groups.append((group, params))
else:
if logmsg is None:
logmsg = ''
for match in search_logmsg_re.finditer(logmsg):
# Add captured variables to (a copy of) params
msg_params = params.copy()
msg_params.update(match.groupdict())
groups.append((group, msg_params))
if not groups:
groups.append((None, self._default_params))
return groups
class _sub_section:
pass
class _data:
"Helper class to define an attribute-based hunk o' data."
def __init__(self, **kw):
vars(self).update(kw)
class MissingConfig(Exception):
pass
class UnknownMappingSection(Exception):
pass
class UnknownMappingSpec(Exception):
pass
class UnknownSubcommand(Exception):
pass
class MessageSendFailure(Exception):
pass
if __name__ == '__main__':
def usage():
scriptname = os.path.basename(sys.argv[0])
sys.stderr.write(
"""USAGE: %s commit REPOS REVISION [CONFIG-FILE]
%s propchange REPOS REVISION AUTHOR REVPROPNAME [CONFIG-FILE]
%s propchange2 REPOS REVISION AUTHOR REVPROPNAME ACTION [CONFIG-FILE]
%s lock REPOS AUTHOR [CONFIG-FILE]
%s unlock REPOS AUTHOR [CONFIG-FILE]
If no CONFIG-FILE is provided, the script will first search for a mailer.conf
file in REPOS/conf/. Failing that, it will search the directory in which
the script itself resides.
ACTION was added as a fifth argument to the post-revprop-change hook
in Subversion 1.2.0. Its value is one of 'A', 'M' or 'D' to indicate
if the property was added, modified or deleted, respectively.
""" % (scriptname, scriptname, scriptname, scriptname, scriptname))
sys.exit(1)
# Command list: subcommand -> number of arguments expected (not including
# the repository directory and config-file)
cmd_list = {'commit' : 1,
'propchange' : 3,
'propchange2': 4,
'lock' : 1,
'unlock' : 1,
}
config_fname = None
argc = len(sys.argv)
if argc < 3:
usage()
cmd = sys.argv[1]
repos_dir = to_str(svn.core.svn_path_canonicalize(to_bytes(sys.argv[2])))
try:
expected_args = cmd_list[cmd]
except KeyError:
usage()
if argc < (expected_args + 3):
usage()
elif argc > expected_args + 4:
usage()
elif argc == (expected_args + 4):
config_fname = sys.argv[expected_args + 3]
# Settle on a config file location, and open it.
if config_fname is None:
# Default to REPOS-DIR/conf/mailer.conf.
config_fname = os.path.join(repos_dir, 'conf', 'mailer.conf')
if not os.path.exists(config_fname):
# Okay. Look for 'mailer.conf' as a sibling of this script.
config_fname = os.path.join(os.path.dirname(sys.argv[0]), 'mailer.conf')
if not os.path.exists(config_fname):
raise MissingConfig(config_fname)
ret = svn.core.run_app(main, cmd, config_fname, repos_dir,
sys.argv[3:3+expected_args])
sys.exit(1 if ret else 0)
# ------------------------------------------------------------------------
# TODO
#
# * add configuration options
# - each group defines delivery info:
# o whether to set Reply-To and/or Mail-Followup-To
# (btw: it is legal do set Reply-To since this is the originator of the
# mail; i.e. different from MLMs that munge it)
# - each group defines content construction:
# o max size of diff before trimming
# o max size of entire commit message before truncation
# - per-repository configuration
# o extra config living in repos
# o optional, non-mail log file
# o look up authors (username -> email; for the From: header) in a
# file(s) or DBM
# * get rid of global functions that should properly be class methods
view raw mailer.py hosted with ❤ by GitHub
#!/usr/bin/php
<?php
define('SVNLOOK', '/usr/bin/svnlook');
// Get list of files in this transaction, but only consider sql files in the 'SQL' folder.
// 1 = TXN, 2 = REPO
$command = SVNLOOK." changed -t {$_SERVER['argv'][1]} {$_SERVER['argv'][2]} | awk '{print \$2}' | grep /SQL/ | grep -i \\.sql\$";
// Wenn der Befehl eben nicht geht, dann SQL-Dateien per Hand raussuchen.
$handle = popen($command, 'r');
if ($handle === false) {
echo 'ERROR: Could not execute "'.$command.'"'.PHP_EOL.PHP_EOL;
exit(2);
}
$contents = stream_get_contents($handle);
fclose($handle);
$neededRollbacks = array();
$neededMigrations = array();
// Ausgabe in einzelne Zeilen aufspalten, -1 == no limit
foreach (preg_split('/\v/', $contents, -1, PREG_SPLIT_NO_EMPTY) as $path) {
$matches = array();
if (preg_match("/migration_([0-9]+)/", $path, $matches)) {
$neededRollbacks[$matches[1]] = $path;
} else if (preg_match("/rollback_([0-9]+)/", $path, $matches)) {
$neededMigrations[$matches[1]] = $path;
}
}
// Wir müssen jetzt vergleichen, ob es zu jeder migration_<num>.sql auch eine rollback_<num>.sql gibt.
$missingMigrations = array_diff_key($neededRollbacks, $neededMigrations);
$missingRollbacks = array_diff_key($neededMigrations, $neededRollbacks);
if (empty($missingMigrations) && empty($missingRollbacks)) {
exit(0);
}
echo "Each migration sql file must have a corresponding rollback sql file and vice versa:", PHP_EOL, PHP_EOL;
foreach ($missingMigrations as $rev => $file) {
echo $file, PHP_EOL;
}
foreach ($missingRollbacks as $rev => $file) {
echo $file, PHP_EOL;
}
echo PHP_EOL;
echo "If you absolutely cannot provide a rollback file, please insert @NO ROLLBACK@ into your commit comment.", PHP_EOL;
echo "This will allow your commit to pass.", PHP_EOL;
echo "See https://wiki.th-mittelhessen.de/index.php/Datenbankmigration for more information.", PHP_EOL;
exit(1);
#!/usr/bin/env python
#
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
#
import sys
import os
from svn import repos, fs, core
def duplicate_ephemeral_txnprops(repos_path, txn_name):
fs_ptr = repos.fs(repos.open(repos_path))
txn_t = fs.open_txn(fs_ptr, txn_name)
for name, value in fs.txn_proplist(txn_t).items():
if name.startswith(core.SVN_PROP_TXN_PREFIX):
name = core.SVN_PROP_REVISION_PREFIX + \
name[len(core.SVN_PROP_TXN_PREFIX):]
fs.change_txn_prop(txn_t, name, value)
def usage_and_exit(errmsg=None):
stream = errmsg and sys.stderr or sys.stdout
stream.write("""\
Usage:
persist-ephemeral-txnprops.py REPOS_PATH TXN_NAME
Duplicate ephemeral transaction properties so that the information
they carry may persist as properties of the revision created once the
transaction is committed. This is intended to be used as a Subversion
pre-commit hook script.
REPOS_PATH is the on-disk path of the repository whose transaction
properties are being examined/modified. TXN_NAME is the name of the
transaction.
Ephemeral transaction properties, whose names all begin with the
prefix "%s", will be copied to new properties which use the
prefix "%s" instead.
""" % (core.SVN_PROP_TXN_PREFIX, core.SVN_PROP_REVISION_PREFIX))
if errmsg:
stream.write("ERROR: " + errmsg + "\n")
sys.exit(errmsg and 1 or 0)
def main():
argc = len(sys.argv)
if argc != 3:
usage_and_exit("Incorrect number of arguments.")
repos_path = sys.argv[1]
txn_name = sys.argv[2]
duplicate_ephemeral_txnprops(repos_path, txn_name)
if __name__ == "__main__":
main()
#!/usr/bin/php
<?php
/**
* A commit hook for SVN.
*
* PHP version 5
*
* @category PHP
* @package PHP_CodeSniffer
* @author Jack Bates <ms419@freezone.co.uk>
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600)
* @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
* @version CVS: $Id: phpcs-svn-pre-commit,v 1.5 2009/01/14 02:44:18 squiz Exp $
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
if (is_file(dirname(__FILE__).'/../CodeSniffer/CLI.php') === true) {
include_once dirname(__FILE__).'/../CodeSniffer/CLI.php';
} else {
include_once 'PHP/CodeSniffer/CLI.php';
}
define('PHP_CODESNIFFER_SVNLOOK', '/usr/bin/svnlook');
/**
* A class to process command line options.
*
* @category PHP
* @package PHP_CodeSniffer
* @author Jack Bates <ms419@freezone.co.uk>
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600)
* @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
* @version Release: @package_version@
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class PHP_CodeSniffer_SVN_Hook extends PHP_CodeSniffer_CLI
{
/**
* Get a list of default values for all possible command line arguments.
*
* @return array
*/
public function getDefaults()
{
$defaults = parent::getDefaults();
$defaults['svnArgs'] = array();
return $defaults;
}//end getDefaults()
/**
* Processes an unknown command line argument.
*
* All unkown args are sent to SVN commands.
*
* @param string $arg The command line argument.
* @param int $pos The position of the argument on the command line.
* @param array $values An array of values determined from CLI args.
*
* @return array The updated CLI values.
* @see getCommandLineValues()
*/
public function processUnknownArgument($arg, $pos, $values)
{
$values['svnArgs'][] = $arg;
return $values;
}//end processUnknownArgument()
/**
* Runs PHP_CodeSniffer over files are directories.
*
* @param array $values An array of values determined from CLI args.
*
* @return int The number of error and warning messages shown.
* @see getCommandLineValues()
*/
public function process($values=array())
{
if (empty($values) === true) {
$values = parent::getCommandLineValues();
}
// Get list of files in this transaction.
$command = PHP_CODESNIFFER_SVNLOOK.' changed '.implode(' ', $values['svnArgs']);
$handle = popen($command, 'r');
if ($handle === false) {
echo 'ERROR: Could not execute "'.$command.'"'.PHP_EOL.PHP_EOL;
exit(2);
}
$contents = stream_get_contents($handle);
fclose($handle);
// Do not check deleted paths.
$contents = preg_replace('/^D.*/m', null, $contents);
// Drop the four characters representing the action which precede the path on
// each line.
$contents = preg_replace('/^.{4}/m', null, $contents);
$values['standard'] = $this->validateStandard($values['standard']);
if (PHP_CodeSniffer::isInstalledStandard($values['standard']) === false) {
// They didn't select a valid coding standard, so help them
// out by letting them know which standards are installed.
echo 'ERROR: the "'.$values['standard'].'" coding standard is not installed. ';
$this->printInstalledStandards();
exit(2);
}
$phpcs = new PHP_CodeSniffer($values['verbosity'], $values['tabWidth']);
// Set file extensions if they were specified. Otherwise,
// let PHP_CodeSniffer decide on the defaults.
if (empty($values['extensions']) === false) {
$phpcs->setAllowedFileExtensions($values['extensions']);
}
// Set ignore patterns if they were specified.
if (empty($values['ignored']) === false) {
$phpcs->setIgnorePatterns($values['ignored']);
}
// Initialize PHP_CodeSniffer listeners but don't process any files.
$phpcs->process(array(), $values['standard'], $values['sniffs']);
foreach (preg_split('/\v/', $contents, -1, PREG_SPLIT_NO_EMPTY) as $path) {
// No need to process folders as each changed file is checked.
if (substr($path, -1) === '/') {
continue;
}
// Do not check non-php files
if (substr($path, -4) !== ".php") {
continue;
}
// Get the contents of each file, as it would be after this transaction.
$command = PHP_CODESNIFFER_SVNLOOK.' cat '.implode(' ', $values['svnArgs']).' '.$path;
$handle = popen($command, 'r');
if ($handle === false) {
echo 'ERROR: Could not execute "'.$command.'"'.PHP_EOL.PHP_EOL;
exit(2);
}
$contents = stream_get_contents($handle);
fclose($handle);
$phpcs->processFile($path, $contents);
}//end foreach
return parent::printErrorReport(
$phpcs,
$values['reports'],
$values['showSources'],
$values['reportFile'],
$values['reportWidth']
);
}//end process()
/**
* Prints out the usage information for this script.
*
* @return void
*/
public function printUsage()
{
parent::printUsage();
echo PHP_EOL;
echo ' Each additional argument is passed to the `svnlook changed ...`'.PHP_EOL;
echo ' and `svnlook cat ...` commands. The report is printed on standard output,'.PHP_EOL;
echo ' however Subversion displays only standard error to the user, so in a'.PHP_EOL;
echo ' pre-commit hook, this script should be invoked as follows:'.PHP_EOL;
echo PHP_EOL;
echo ' '.basename($_SERVER['argv'][0]).' ... "$REPOS" -t "$TXN" >&2 || exit 1'.PHP_EOL;
}//end printUsage()
}//end class
$phpcs = new PHP_CodeSniffer_SVN_Hook();
$phpcs->checkRequirements();
$numErrors = $phpcs->process();
if ($numErrors !== 0) {
exit(1);
}
?>
#!/bin/sh
# PRE-COMMIT HOOK
#
# The pre-commit hook is invoked before a Subversion txn is
# committed. Subversion runs this hook by invoking a program
# (script, executable, binary, etc.) named 'pre-commit' (for which
# this file is a template), with the following ordered arguments:
#
# [1] REPOS-PATH (the path to this repository)
# [2] TXN-NAME (the name of the txn about to be committed)
#
# The default working directory for the invocation is undefined, so
# the program should set one explicitly if it cares.
#
# If the hook program exits with success, the txn is committed; but
# if it exits with failure (non-zero), the txn is aborted, no commit
# takes place, and STDERR is returned to the client. The hook
# program can use the 'svnlook' utility to help it examine the txn.
#
# On a Unix system, the normal procedure is to have 'pre-commit'
# invoke other programs to do the real work, though it may do the
# work itself too.
#
# *** NOTE: THE HOOK PROGRAM MUST NOT MODIFY THE TXN, EXCEPT ***
# *** FOR REVISION PROPERTIES (like svn:log or svn:author). ***
#
# This is why we recommend using the read-only 'svnlook' utility.
# In the future, Subversion may enforce the rule that pre-commit
# hooks should not modify the versioned data in txns, or else come
# up with a mechanism to make it safe to do so (by informing the
# committing client of the changes). However, right now neither
# mechanism is implemented, so hook writers just have to be careful.
#
# Note that 'pre-commit' must be executable by the user(s) who will
# invoke it (typically the user httpd runs as), and that user must
# have filesystem-level permission to access the repository.
#
# On a Windows system, you should name the hook program
# 'pre-commit.bat' or 'pre-commit.exe',
# but the basic idea is the same.
#
# The hook program typically does not inherit the environment of
# its parent process. For example, a common problem is for the
# PATH environment variable to not be set to its usual value, so
# that subprograms fail to launch unless invoked via absolute path.
# If you're having unexpected problems with a hook program, the
# culprit may be unusual (or missing) environment variables.
#
# Here is an example hook script, for a Unix /bin/sh interpreter.
# For more examples and pre-written hooks, see those in
# the Subversion repository at
# http://svn.collab.net/repos/svn/trunk/tools/hook-scripts/ and
# http://svn.collab.net/repos/svn/trunk/contrib/hook-scripts/
REPOS="$1"
TXN="$2"
# Make sure that the log message contains some text.
SVNLOOK=/usr/bin/svnlook
$SVNLOOK log -t "$TXN" "$REPOS" | grep "[a-zA-Z0-9]" > /dev/null || exit 1
# Check that a migration file has corresponding rollback file.
# This check can be overridden with magic words.
$SVNLOOK log -t "$TXN" "$REPOS" | grep "@NO ROLLBACK@" > /dev/null
if [ $? -eq 1 ]
then
/usr/share/subversion/hook-scripts/migration-check.php "$TXN" "$REPOS" >&2 || exit 1
fi
# Check if file uses UTF-8 byte order mark and abort commit if BOM is present
/usr/share/subversion/hook-scripts/bom-check.php "$TXN" "$REPOS" >&2 || exit 1
#echo "Sorry, gerade kann man nicht committen. :o" 1>&2
#exit 1
# Check that the author of this commit has the rights to perform
# the commit on the files and directories being modified.
#/usr/share/subversion/hook-scripts/commit-access-control.pl "$REPOS" "$TXN" "$REPOS"/commit-access-control.cfg || exit 1
PHP="/usr/bin/php"
AWK="/usr/bin/awk"
GREP="/bin/egrep"
SED="/bin/sed"
CHANGED=`$SVNLOOK changed -t "$TXN" "$REPOS" | $AWK '{print $2}' | $GREP \\.php$`
for FILE in $CHANGED
do
MESSAGE=`$SVNLOOK cat -t "$TXN" "$REPOS" "$FILE" | $PHP -l`
if [ $? -ne 0 ]
then
echo 1>&2
echo "***********************************" 1>&2
echo "PHP error in: $FILE:" 1>&2
echo `echo "$MESSAGE" | $SED "s| -| $FILE|g"` 1>&2
echo "***********************************" 1>&2
exit 1
fi
done
# --ignore=\.txt$,\.TXT$,\.jpg$,\.JPG$,\.js$,\.css$,\.html$,\.mkf$,\.phar$,\.sql$,\.ttf$,\.png&,\.phtml$,\.gif$,\.GIF$,\.svg$,\.SVG$,\.xml$,\.dtd$,\.db$,\.xsd$
/usr/share/php/PHP/scripts/phpcs-svn-pre-commit --ignore=web/common/classes/jpgraph,web/common/classes/geshi,web/common/classes/tcpdf,web/xpweb/lib,web/phpids/classes/phpids_lib -t "$TXN" "$REPOS" >&2 || exit 1
# All checks passed, so allow the commit.
exit 0
Subscribe
Notify of
guest


0 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments

Certification Courses

DevOpsSchool has introduced a series of professional certification courses designed to enhance your skills and expertise in cutting-edge technologies and methodologies. Whether you are aiming to excel in development, security, or operations, these certifications provide a comprehensive learning experience. Explore the following programs:

DevOps Certification, SRE Certification, and DevSecOps Certification by DevOpsSchool

Explore our DevOps Certification, SRE Certification, and DevSecOps Certification programs at DevOpsSchool. Gain the expertise needed to excel in your career with hands-on training and globally recognized certifications.

0
Would love your thoughts, please comment.x
()
x