morzel.net

.net, js, html, arduino, java... no rants or clickbaits.

Save yourself some troubles with TortoiseGit pre-commit hook

INTRO

Back in 2013, when I was using SVN, I wrote the post about creating a TortoiseSVN pre-commit hook that can prevent someone from committing code which is not meant to be shared (e. g. some hack done for troubleshooting). The idea was to mark “uncommittable” code with a comment containing NOT_FOR_REPO text and block the commit if such text is found in any of the modified or added files… The technique saved me a few times and proved to be useful to others…

This days I’m mostly using Git, and with Git’s decentralized nature and cheap branching the above technique is less needed but might still be helpful. The good news is that the same hook can be used in both TortoiseSVN and in TortoiseGit (I like to do commits with Tortoise UI and reserve command line for things like interactive rebase)…

First I will show you how to implement a pre-commit hook (I will use C# but you can use anything that Windows can run) and then you will see how to setup the hook in TortoiseGit Settings... 

 

TORTOISE PRE-COMMIT HOOK IN C#

You can find full code sample in this GitHub repository (it's a C# 6 console project from Visual Studio 2015, targeting .NET 4.5.2). Below is the class that implements the hook:

using System;
using System.IO;
using System.Text.RegularExpressions;

namespace DontLetMeCommit
{
    class Program
    {
        const string NotForRepoMarker = "NOT_FOR_REPO";

        static void Main(string[] args)
        {
            string[] affectedPaths = File.ReadAllLines(args[0]);
                        
            foreach (string path in affectedPaths)
            {
                if (ShouldFileBeChecked(path) && HasNotForRepoMarker(path))
                {
                    string errorMessage = $"{NotForRepoMarker} marker found in {path}";
                    Console.Error.WriteLine(errorMessage); // Notice write to Error output stream!
                    Environment.Exit(1);
                }
            }
        }

        static bool ShouldFileBeChecked(string path)
        {
            // Here we are choosing to check only selected file types but you may want to check
            // all the files except specified types or skip filtering altogether...
            Regex filePattern = new Regex(@"^.*\.(cs|js|xml|config)$", RegexOptions.IgnoreCase);

            // List of files affected by the commit might include (re)moved files so we check if file exists...
            return File.Exists(path) && filePattern.IsMatch(path);
        }

        static bool HasNotForRepoMarker(string path)
        {
            using (StreamReader reader = File.OpenText(path))
            {
                string line = reader.ReadLine();

                while (line != null)
                {
                    if (line.Contains(NotForRepoMarker)) 
                        return true; // "Uncommittable" code marker found - let's block the commit!

                    line = reader.ReadLine();
                }
            }

            return false;
        }
    }
}

How it works?

When Tortoise calls a pre-commit hook it passes a path to temporary file as a the first argument (args[0]). Each line in that file contains a path to a file that is affected by the commit. Hook reads all the lines (paths) from tmp file and checks if NOT_FOR_REPO text appears in any of them. If that's the case the commit is blocked by ending the program with non-zero code (call to Environment.Exit). Before that happens, a message is printed to Error stream (Tortoise will present this message to user). HasNotForRepoMarker method checks file by reading it line-by-line (via StreamReader) and stopping as soon as the marker is found. On my laptop full scan of 100 MB text file with one million lines takes about half a second so I guess its fast enough :) ShouldFileBeChecked method is there to decide if a path is interesting for us. We definitely don't want to check paths of removed files, hence the File.Exists call. I've also added Regex file name pattern matching to show you that you can be quite picky about which files you wish to check... That's it, compile it and you can use it as a hook!

 

ENABLING THE HOOK IN TORTOISEGIT SETTINGS

To set the hook first right click any folder and open TortoiseGit | Settings menu (I'm using TortoiseGit 2.1.0.0):

Menu step 1

Then go to Hook Scripts section and click Add... button:

Menu step 2... Click to enlarge...

Now choose Pre-Commit Hook type, next choose a Working Tree Path (select a folder which you want to protect with the hook - its subdirectories will be covered too!), and then choose Command Line To Execute (in case of C# hook this is an *.exe file). Make sure that what Wait for the script to finish and Hide script while running checkboxes are ticked (first checkbox is to make sure that commit is not going to complete unit all files are scanned and the second prevents console window from appearing). Her's how my settings look like:

Menu step 3... Click to enlarge...

Now click OK and voila - you have a pre-commit hook. Let's test it...

 

TESTING THE HOOK 

To check if the hook is working I've added NOT_FOR_REPO comment in one of the files from C:\Examples\Repos\blog-post-sonar Git repository:

namespace SonarServer
{
    class Program
    {
        const byte DataSampleStartMarker = 255; // NOT_FOR_REPO
        static List<byte> rawSonarDataBuffer = new List<byte>();
		

 I also did some other modification in a different file and removed one file, so my commit window looked like this:

Git commit... Click to enlarge...

After clicking Commit button the hook did it's job and blocked the commit:

Git commit blocked

Cool, and what if you really want to commit this even if the NOT_FOR_REPO marker is present? In that case use can do the commit through Git command line because TortoiseGit hook is something different than a "native" Git hook (from .git/.hooks)

And here's a proof that the same hook works when used with TortoiseSVN:

SVN commit blocked... Click to enlarge...

TortoiseSVN window looks a bit nicer and has Retry without hooks option...

 

 

TortoiseSVN pre-commit hook in C# - save yourself some troubles!

Probably everyone who creates or debugs a program happens to make temporary changes to the code that make current task easier but should never get into the repository. And probably everyone has accidentally put such code into next revision. If you are lucky enough, mistake will be revealed quickly and the only result will be a bit of shame, if not...

If only there was a way to mark “uncommitable” code...

You can do it and it’s pretty simple!

TortoiseSVN lets you set so-called pre-commit hook. It’s a program (or script) that is run when user clicks “OK” button in “SVN Commit” window. Hook can for example check content of modified files and block commit when deemed appropriate. Tortoise hooks differ from Subversion hooks in that they are executed locally and not on the server that hosts the repository. You therefore don’t have to worry whether your hook will be accepted by the admin or if it works on the server (e.g. server may not have .NET installed), you also don’t affect the experience of other users of the repository. Client-side hooks are quicker too.

Detailed description of hooks can be found in „4.30.8. Client Side Hook Scripts” chapter of Tortoises help file.

Tortoise supports 7 kinds of hooks: start-commit, pre-commit, post-commit, start-update, pre-update, post-update and pre-connect. We are concerned with pre-commit action. The essence of the hook is to check whether one of added or modified files contains temporary code marker. Our marker may be a “NOT_FOR_REPO” text put into a comment placed above temporary code.

This is whole hook’s code – simple console application, that may save your ass :)

using System;
using System.IO;
using System.Text.RegularExpressions;

namespace NotForRepoPreCommitHook
{
    class Program
    {
        const string NotForRepoMarker = "NOT_FOR_REPO";

        static void Main(string[] args)
        {              
            string[] affectedPaths = File.ReadAllLines(args[0]);

            Regex fileExtensionPattern = new Regex(@"^.*\.(cs|js|xml|config)$", RegexOptions.IgnoreCase);

            foreach (string path in affectedPaths)
            {
                if (fileExtensionPattern.IsMatch(path) && File.Exists(path))
                {
                    if (ContainsNotForRepoMarker(path))
                    {
                        string errorMessage = string.Format("{0} marker found in {1}", NotForRepoMarker, path);
                        Console.Error.WriteLine(errorMessage);    
                        Environment.Exit(1);  
                    }
                }
            }             
        }

        static bool ContainsNotForRepoMarker(string path)
        {
            StreamReader reader = File.OpenText(path);

            try
            {
                string line = reader.ReadLine();

                while (line != null)
                {
                    if (line.Contains(NotForRepoMarker))
                    {
                        return true;
                    }

                    line = reader.ReadLine();
                }
            }
            finally
            {
                reader.Close();
            }  

            return false;
        }
    }
}

TSVN calls pre-commit hook with four parameters. We are interested only in the first one. It contains a path to *.tmp file. In this file there is a list of files affected by current commit. Each line is one path. After loading the list, files are filtered by extension (useful if you don’t want to process files of all types). Checking if file exists is also important – the list from *.tmp file contains paths for deleted files too! Detection of the marker represented by NotForRepoMarker constant is realized by ContainsNotForRepoMarker method. Despite its simplicity it provides good performance. On mine (middle range) laptop, 100 MB file takes less than a second to process. If marker is found, program exits with error code (value different than 0). Before quitting, information about which file contains the marker is sent to standard error output (via Console.Error). This message will get displayed in Tortoise window.

The code is simple, isn’t it? In addition, hook installation is also trivial!

To attach hook, choose “Settings” item from Tortoise’s context menu. Then select “Hook scripts” element and click “Add…” button. Such window will appear:

TSVN hooks configuration window

Set „Hook Type” to „Pre-Commit Hook”. Fill “Working Copy Path” field with a path to the directory that contains local copy of the repo (different folders can have different hooks). In “Command Line To Execute” field, set path to the application that implements the hook. Check “Wait for the script to finish” and “Hide the script while running” options (the latter will prevent console window from showing). Press “OK” button and voila, hook is installed!

Now mark some code with “NOT_FOR_REPO” comment and try to execute commit. You should see something like that:

Operation blocked by pre-commit hook

Notice the „Retry without hooks” button – it allows commit to be completed by ignoring hooks.

We now have a hook that prevents from temporary code submission. One may also want to create a hook that enforces log message to be filled, blocks *.log files commits etc. Your private hooks – you decide! And if some of the hooks will be usefull for the whole team, you can always remake them as Subversion hooks.

Tested on TortoiseSVN 1.7.8/Subversion 1.7.6.

Update 24.03.2014: Added emphasis to checking "Wait for the script to finish" option - without it hook will not block commits!

Update 17.09.2013: (additional info): You may set hook on a parent folder which contains multiple repositories checkouts. If you are willing to sacrifice a bit of performance for added protection you may resign from filtering files before checking for NotForRepoMarker marker.