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):
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; // 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:
After clicking Commit button the hook did it's 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...