Creating a pre-commit hook for TortoiseSvn

Occasionally we will either have files in SVN we don’t want updated or files we don’t want committed to source control. In the latter case you might ignore those files but equally they might be part of a csproj (for example) but you don’t want them updated after the initial check in.

A great example of this is a configuration file, checked in initially with a placeholder for a private key. Thus on your CI/build box this file is included in the build but the private key is kept out of the repo.

I’m using TortoiseSvn and whilst this may well be a standard feature of SVN I’ll be discussing it from the TortoiseSvn point of view.

A pre-commit hook can be set-up on your local machine. Obviously in some cases it’d be preferable to have the server handle such tasks, but in situations where that’s not possible we can create our own code and hook into TortoiseSvn’s pre-commit hook.

We’re going to write this in .NET (although it’s easy to implement in any language) as ultimately we’ll just be creating a pretty standard console application.

Configuring the hook in TortoiseSVN

In the Settings | Hook Scripts section we can add a hook, select Pre-Commit Hook from the Hook Type dropdown and the Working Copy Path is set to our local copy of the repository this hook should apply to. Hence this is not a hook against all projects’s or repositories you might have checked out but is on a per checkout basis. Next up, the Command Line To Execute should be the full path (including the EXE or script) to the application which should be executed prior to a commit.

Writing our pre-commit application

We can write the application in any language or scripting if we are able to run the interpreter for the script (for example the TortoiseSVN dialog settings in the previously listed link, show WScript running a JavaScript file).

For our purposes we’re going to create a Console application.

When executed, our application will be passed arguments (via the Main method’s arguments). There are four arguments sent to your application in the following order

  • The path and filename of a .tmp file which contains a newline delimited list of the files to be committed
  • The depth of commit/update
  • The path and filename of a .tmp file which contains the message that is to be saved as part of the commit
  • The current working directory

Note: different hooks send different arguments, see https://tortoisesvn.net/docs/release/TortoiseSVN_en/tsvn-dug-settings.html but listed here also for completness

  • Start-commit
    Arguments – PATH, MESSAGEFILE, CWD
  • Manual Pre-commit
    Arguments – PATH, MESSAGEFILE, CWD
  • Pre-commit
    Arguments – PATH DEPTH MESSAGEFILE CWD
  • Post-commit
    Arguments – PATH, DEPTH, MESSAGEFILE, REVISION, ERROR, CWD
  • Start-update
    Arguments – PATH, CWD
  • Pre-update
    Arguments – PATH, DEPTH, REVISION, CWD
  • Post-update
    Arguments – PATH, DEPTH, REVISION, ERROR, CWD, RESULTPATH
  • Pre-connect
    Arguments – no parameters are passed to this script. You can pass a custom parameter by appending it to the script path.

The meaning of each argument is as follows

  • PATH
    A path to a temporary file which contains all the paths for which the operation was started. Each path is on a separate line in the temp file.

    Note that for operations done remotely, e.g. in the repository browser, those paths are not local paths but the urls of the affected items.

  • DEPTH
    The depth with which the commit/update is done.

    Possible values are:

    • -2 svn_depth_unknown
    • -1 svn_depth_exclude
    • 0 svn_depth_empty
    • 1 svn_depth_files
    • 2 svn_depth_immediates
    • 3 svn_depth_infinity
  • MESSAGEFILE
    Path to a file containing the log message for the commit. The file contains the text in UTF-8 encoding. After successful execution of the start-commit hook, the log message is read back, giving the hook a chance to modify it.
  • REVISION
    The repository revision to which the update should be done or after a commit completes.
  • ERROR
    Path to a file containing the error message. If there was no error, the file will be empty.
  • CWD
    The current working directory with which the script is run. This is set to the common root directory of all affected paths.
  • RESULTPATH
    A path to a temporary file which contains all the paths which were somehow touched by the operation. Each path is on a separate line in the temp file.

Your application, or script should return 0 for success anything other than 0 for failure. We can also write to the console error stream to return a message for TortoiseSVN to display.

Here’s a stupid little example which just stops any more check-ins

class Program
{
   static int Main(string[] args)
   { 
      Console.Error.WriteLine("Stop checking in");
      return 1;
     }
}

Sample

The following code stops any check-ins of a file named keys.txt that has any data within it

public static class KeysFileCheck
{
   public static int CheckFiles(string fileList)
   {
      return CheckFiles(
         File.ReadAllLines(fileList)
            .Where(path => Path.GetFileName(path) == "keys.txt")
            .ToArray());
   }

   private static int CheckFiles(string[] keyFiles)
   {
      var result = 0;
      foreach (var keyFile in keyFiles)
      {
         using (var fs = new FileStream(keyFile, FileMode.Open, FileAccess.Read))
         {
            result |= CheckKeyFile(keyFile, fs);
         }
      }
      return result;
   }

   private static int CheckKeyFile(string filename, Stream stream)
   {
      using (var sr = new StreamReader(stream))
      {
         var data = sr.ReadToEnd();
         if (!String.IsNullOrEmpty(data))
         {
            Console.Error.WriteLine(
               "The keys file {0} cannot be saved with data in it as the build server will fail to build the solution", 
               filename);
            return 1;
         }
      }

      return 0;
   }
}

Or Console application’s main method would simply be

class Program
{
   static int Main(string[] args)
   { 
      return KeysFileCheck.CheckFiles(args[0]);
   }
}