Introduction
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…
These 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 the 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);
Environment.Exit(1);
}
}
}
static bool ShouldFileBeChecked(string path)
{
Regex filePattern = new Regex(@"^.*\.(cs|js|xml|config)$", RegexOptions.IgnoreCase);
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;
line = reader.ReadLine();
}
}
return false;
}
}
}
How It Works?
When Tortoise calls a pre-commit hook, it passes a path to temporary file as 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):
Then go to Hook Scripts section and click Add... button:
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:
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;
static List<byte> rawSonarDataBuffer = new List<byte>();
I also made some other modification in a different file and removed one file, so my commit window looked like this:
After clicking Commit button, the hook did its job and blocked the commit:
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:
TortoiseSVN window looks a bit nicer and has Retry without hooks option...