SVN::Hooks(3pm)

πŸ“¦
version 1.36 Β· 3 versions
, in ubuntu noble, ubuntu resolute, ubuntu stonking

πŸ’– SVN::Hooks β€” bestie edition πŸ’–

So you wanna write Subversion hooks and not lose your whole entire mind? Same. This is the SVN::Hooks page from the User Contributed Perl Documentation section of the manual, and it's the framework for implementing Subversion hooks. Let's get into it. 🫢


✨ NAME ✨

SVN::Hooks β€” Framework for implementing Subversion hooks πŸ”₯

πŸŽ€ VERSION πŸŽ€

version 1.36


🌸 SYNOPSIS 🌸

okay so. the whole vibe is that one single script can implement several hooks. not one script per hook, bestie. one script. ✨

#!/usr/bin/perl

use SVN::Hooks;

START_COMMIT {
    my ($repo_path, $username, $capabilities, $txn_name) = @_;
    # ...
};

PRE_COMMIT {
    my ($svnlook) = @_;
    # ...
};

run_hook($0, @ARGV);

orrrrr you can just use hooks that already exist via plugins, which is honestly the move πŸ’…:

#!/usr/bin/perl

use SVN::Hooks;
use SVN::Hooks::DenyFilenames;
use SVN::Hooks::DenyChanges;
use SVN::Hooks::CheckProperty;
...

run_hook($0, @ARGV);

πŸ¦‹ INTRODUCTION πŸ¦‹

okay bestie, real talk: to really get what this is about, you have to get Subversion <http://subversion.apache.org/> and its hooks. everything you'd ever want to know lives in the svnbook, a.k.a. Version Control with Subversion, at <http://svnbook.red-bean.com/nightly/en/index.html>. go read it, it's giving textbook but in a good way. πŸ“š

Subversion is a version control system, and like all of them it keeps historical revisions of files and directories. Every revision holds info about all the changes made since the last one: date, author, log message, files changed, files renamed, etc. the receipts, basically. 🧾

Subversion runs on a client/server model. The server holds the repository, which is the database with all that historical info we just talked about. Users use a Subversion client tool to query and change the repository, and also to maintain one or more working areas. A working area is a directory on the user's machine with a copy of a particular revision of the repository. The user can do all sorts of stuff in their working area with the client tool and then "commit" it all in one atomic operation that bumps the repository up to a new revision. slay. πŸ’…

A hook is a specifically named program that the Subversion server calls during the execution of some operations. There are exactly nine hooks, and they have to live under the "hooks" directory in the repository. When you make a new repository you get nine template files in that directory, all with the .tmpl suffix, and each one has helpful instructions inside explaining how to turn it into a working hook. cute, right? πŸŽ€

So like, when Subversion is doing a commit operation for a client, it calls the "start-commit" hook, then the "pre-commit" hook, and then the "post-commit" hook. The first two can gather all kinds of info about the specific commit transaction going down and decide to reject it if it doesn't comply with the policies you set. The "post-commit" one can be used to log or alert interested parties about the commit that just happened. πŸ“£

IMPORTANT NOTE from the svnbook, and I need you to actually read this one bestie: "For security reasons, the Subversion repository executes hook programs with an empty environment---that is, no environment variables are set at all, not even $PATH (or %PATH%, under Windows). Because of this, many administrators are baffled when their hook program runs fine by hand, but doesn't work when run by Subversion. Be sure to explicitly set any necessary environment variables in your hook program and/or use absolute paths to programs." 😭 it's always the environment variables. ALWAYS.

Not even the current directory where the hooks run is specified by Subversion. BUT. The hooks executed by the SVN::Hooks framework run with their currect directory set to the repository's root directory in the server. Which is lowkey useful sometimes. ✨


πŸ“ the existing hook scripts situation

There are several useful hook scripts available elsewhere <http://svn.apache.org/repos/asf/subversion/trunk/contrib/hook-scripts/>, mainly for those three associated with the commit operation. But here's the thing β€” when you try to combine the functionality of two or more of those scripts into a single hook, you normally end up facing two problems. πŸ™ˆ

  • Complexity β€” To integrate the funcionality of more than one script you have to write a driver script that's called by Subversion and calls all the other scripts in order, passing them the arguments they need. And on top of that, some of those scripts may have configuration files to read, and you may have to maintain several of them. the admin-toil is real. πŸ’€
  • Inefficiency β€” This arrangement is inefficient in two ways. First, because each script runs as a separate process, which usually has a high startup cost because they are, well, scripts and not binaries. And second, because as each script is called in turn, they have no memory of the scripts called before, so they have to gather the information about the transaction again and again, normally by calling the "svnlook" command, which spawns yet another process. it's giving infinite fork bomb energy fr fr. 😭

SVN::Hooks is a framework for implementing Subversion hooks that tries to solve these problems. πŸ‘‘

Instead of having separate scripts implementing different functionality, you have a single script implementing all the funcionality you need β€” either directly or using some of the existing plugins, which are implemented by Perl modules in the SVN::Hooks:: namespace. And this single script can be used to implement all nine standard hooks, because each hook knows when to perform based on the context in which the script was called. main character script energy, no cap. πŸ’–


✨ USAGE ✨

Okay bestie, here's the whole vibe. Head into the hooks directory that lives under the directory where your repository was created. You're gonna see nine hook templates just chilling there. What you do now is create a script right there using the SVN::Hooks module. Like this:

$ cd /path/to/repo/hooks

$ cat >svn-hooks.pl <<END_OF_SCRIPT
#!/usr/bin/perl

use SVN::Hooks;

run_hook($0, @ARGV);

END_OF_SCRIPT

$ chmod +x svn-hooks.pl

This one script is gonna serve every single hook for you, no cap. Now make symbolic links pointing at it for each hook you actually care about. 🫢

You could make symlinks for all nine hooks, but then Subversion calls your script for all hooked operations β€” even the ones you don't care about. Nothing bad happens, it's just your server doing extra work for absolutely no reason. 😭
$ ln -s svn-hooks.pl start-commit
$ ln -s svn-hooks.pl pre-commit
$ ln -s svn-hooks.pl post-commit
$ ln -s svn-hooks.pl pre-revprop-change

As-is, this script does literally nothing. You gotta implement some hooks or use some of the existing ones that ship as plugins. Either way, the script should end with a call to run_hooks passing it the name with which it was called ($0) and all the arguments it received (@ARGV). Periodt. πŸ’…


🌸 Implementing Hooks 🌸

You implement hooks with one of the nine hook directives down below. Each one takes a single block (that's an anonymous function, bestie) as its argument. run_hook calls that block with the proper arguments, exactly as listed. Those arguments are the ones that came from @ARGV β€” except the ones identified by SVN::Look. Those are SVN::Look objects, and you use them to grok all the detailed info about the repository and the current transaction. (Go read the SVN::Look docs to find out how to use it, fr fr. πŸ‘‘)

  • POST_COMMIT(SVN::Look)
  • POST_LOCK(repos-path, username)
  • POST_REVPROP_CHANGE(SVN::Look, username, property-name, action)
  • POST_UNLOCK(repos-path, username)
  • PRE_COMMIT(SVN::Look)
  • PRE_LOCK(repos-path, path, username, comment, steal-lock-flag)
  • PRE_REVPROP_CHANGE(SVN::Look, username, property-name, action)
  • PRE_UNLOCK(repos-path, path, username, lock-token, break-unlock-flag)
  • START_COMMIT(repos-path, username, capabilities, txn-name)

Here's an example of a script implementing two hooks, icon behaviour honestly:

#!/usr/bin/perl

use SVN::Hooks;

# ...

START_COMMIT {
    my ($repos_path, $username, $capabilities, $txn_name) = @_;

    exists $committers{$username}
        or die "User '$username' is not allowed to commit.\n";

    $capabilities =~ /mergeinfo/
        or die "Your Subversion client does not support mergeinfo capability.\n";
};

PRE_COMMIT {
    my ($svnlook) = @_;

    foreach my $added ($svnlook->added()) {
        $added !~ /\.(exe|o|jar|zip)$/
            or die "Please, don't commit binary files such as '$added'.\n";
    }
};

run_hook($0, @ARGV);
Heads up! Those hook directives look like function definitions but they are not. They're function calls, so they have to end with a semi-colon. Don't forget it, bestie. 😭

Most of the start-commit and pre-* hooks exist to check some condition. If the condition holds, they just end without returning anything. If it doesn't? They die with a suitable error message. That's the whole game. πŸ’€

Also note: each hook directive can be called more than once if you need to implement more than one specific hook. The hooks run in the order they were defined. Slay. ✨


πŸ¦‹ Using Plugins πŸ¦‹

There are several hooks already implemented as plugin modules under the namespace SVN::Hooks::, and you can absolutely use them. The main ones are described real quick below. Go read their own documentation for the full tea. πŸ“

  • SVN::Hooks::AllowPropChange β€” Allow only specified users make changes in revision properties.
  • SVN::Hooks::CheckCapability β€” Check if the Subversion client implements the required capabilities.
  • SVN::Hooks::CheckJira β€” Integrate Subversion with the JIRA <http://www.atlassian.com/software/jira/> ticketing system.
  • SVN::Hooks::CheckLog β€” Check if the log message in a commit conforms to a Regexp.
  • SVN::Hooks::CheckMimeTypes β€” Check if the files added to the repository have the svn:mime-type property set. Moreover, for text files, check if the properties svn:eol-style and svn:keywords are also set.
  • SVN::Hooks::CheckProperty β€” Check for specific properties for specific kinds of files.
  • SVN::Hooks::CheckStructure β€” Check if the files and directories being added to the repository conform to a specific structure.
  • SVN::Hooks::DenyChanges β€” Deny the addition, modification, or deletion of specific files and directories in the repository. Usually used to deny modifications in the tags directory.
  • SVN::Hooks::DenyFilenames β€” Deny the addition of files which file names doesn't comply with a Regexp. Usually used to disallow some characteres in the filenames.
  • SVN::Hooks::Notify β€” Sends notification emails after successful commits.
  • SVN::Hooks::UpdateConfFile β€” Allows you to maintain Subversion configuration files versioned in the same repository where they are used. Usually used to maintain the configuration file for the hooks and the repository access control file.

And here's an example of a script using some plugins, so you can see the vibe in action:

#!/usr/bin/perl

use SVN::Hooks;
use SVN::Hooks::CheckProperty;
use SVN::Hooks::DenyChanges;
use SVN::Hooks::DenyFilenames;

# Accept only letters, digits, underlines, periods, and hifens
DENY_FILENAMES(qr/[^-\/\.\w]/i);

# Disallow modifications in the tags directory
DENY_UPDATE(qr:^tags:);

# OpenOffice.org documents need locks
CHECK_PROPERTY(qr/\.(?:od[bcfgimpst]|ot[ghpst])$/i => 'svn:needs-lock');

run_hook($0, @ARGV);

Those directives are implemented and exported by the hooks. Here's the cute part: when you use hooks, you don't need to be explicit about which of the nine hooks will get triggered by the directives. That's on purpose, bestie β€” because some plugins can trigger more than one hook. The plugin documentation should tell you which hooks can be triggered, so you know exactly which symbolic links you need to create in the hooks repository directory. iykyk πŸ’…


πŸŽ€ Configuration file πŸŽ€

Before calling the hooks, run_hook evaluates a file called svn-hooks.conf under the conf directory in the repository β€” if it exists, that is. Which means: you can choose to put all the directives in this file instead of in the script under the hooks directory. ✨

The advantage? You can then manage the configuration file with SVN::Hooks::UpdateConfFile and have it versioned under the same repository that it controls. Main character energy tbh. πŸ‘‘

One way to do this is to use this hook script:

#!/usr/bin/perl

use SVN::Hooks;
use SVN::Hooks::UpdateConfFile;
use ...

UPDATE_CONF_FILE(
    'conf/svn-hooks.conf' => 'svn-hooks.conf',
    validator             => [qw(/usr/bin/perl -c)],
    rotate                => 2,
);

run_hook($0, @ARGV);

Use this hook script and create a directory called conf at the root of the repository (right alongside the common trunk, branches, and tags directories). Add the svn-hooks.conf file under the conf directory. Then, whenever you commit a new version of the file, the pre-commit hook will validate it sintactically (/usr/bin/perl -c) and copy its new version to the conf/svn-hooks.conf file in the repository. (Read the SVN::Hooks::UpdateConfFile documentation to understand it in details, bestie.) 🌸

Being a Perl script, it's possible to get fancy with the configuration file β€” variables, functions, whatever you want. But for most purposes it consists just in a series of configuration directives. Simple as. πŸ’–

Don't forget to end it with the 1; statement, though! Because it's evaluated with a do statement and needs to end with a true expression. This one gets people every time. 😭

Please, see the plugins documentation to know about the directives. 🫢


🌈 PLUGIN DEVELOPER TUTORIAL 🌈

Yet to do.


πŸ’œ EXPORT πŸ’œ

run_hook

This one is responsible to invoke the right plugins depending on the context in which it was called. She's the main character here, honestly.

Its first argument must be the name of the hook that was called. Usually you just pass $0 to it, since it knows to extract the basename of the parameter. ✨

Its second argument must be the path to the directory where the repository was created.

The remaining arguments depend on the hook for which it's being called, like this:

  • start-commit repo-path user capabilities txn-name
  • pre-commit repo-path txn
  • post-commit repo-path rev
  • pre-lock repo-path path user
  • post-lock repo-path user
  • pre-unlock repo-path path user
  • post-unlock repo-path user
  • pre-revprop-change repo-path rev user propname action
  • post-revprop-change repo-path rev user propname action

But as these are exactly the arguments Subversion passes when it calls the hooks, you usually call run_hook like this:

run_hook($0, @ARGV);

That's it. That's the whole thing. πŸ’…


⭐ REPOSITORY ⭐

<https://github.com/gnustavo/SVN-Hooks>


🌸 AUTHOR 🌸

Gustavo L. de M. Chaves <gnustavo@cpan.org>


This software is copyright (c) 2022 by CPqD <www.cpqd.com.br>.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.


And that's a wrap, bestie!! πŸŽ€ This was perl v5.36.0, dated 2022-09-11, and it's the SVN::Hooks(3pm) manpage β€” go forth and hook all the things. Obsessed with you. πŸ’–βœ¨

πŸ“¦ where this page comes from