We use the excellent CS-Script tool for our prefixes so that we can write scripts in the language we are doing this. Here is an example that provides a commit message longer than 10 characters and ensures that the .suo and .user files are not checked. You can also test the indentation of the table / space or force the small code standards to be executed on but be careful when doing your script too much, since you don't want to slow down the commit.
// run from pre-commit.cmd like so: // css.exe /nl /c C:\SVN\Scripts\PreCommit.cs %1 %2 using System; using System.Diagnostics; using System.Text; using System.Text.RegularExpressions; using System.Linq; class PreCommitCS { /// <summary>Controls the procedure flow of this script</summary> public static int Main(string[] args) { if (args.Length < 2) { Console.WriteLine("usage: PreCommit.cs repository-path svn-transaction"); Environment.Exit(2); } try { var proc = new PreCommitCS(args[0], args[1]); proc.RunChecks(); if (proc.MessageBuffer.ToString().Length > 0) { throw new CommitException(String.Format("Pre-commit hook violation\r\n{0}", proc.MessageBuffer.ToString())); } } catch (CommitException ex) { Console.WriteLine(ex.Message); Console.Error.WriteLine(ex.Message); throw ex; } catch (Exception ex) { var message = String.Format("SCRIPT ERROR! : {1}{0}{2}", "\r\n", ex.Message, ex.StackTrace.ToString()); Console.WriteLine(message); Console.Error.WriteLine(message); throw ex; } // return success if we didn't throw return 0; } public string RepoPath { get; set; } public string SvnTx { get; set; } public StringBuilder MessageBuffer { get; set; } /// <summary>Constructor</summary> public PreCommitCS(string repoPath, string svnTx) { this.RepoPath = repoPath; this.SvnTx = svnTx; this.MessageBuffer = new StringBuilder(); } /// <summary>Main logic controller</summary> public void RunChecks() { CheckCommitMessageLength(10); // Uncomment for indent checks /* string[] changedFiles = GetCommitFiles( new string[] { "A", "U" }, new string[] { "*.cs", "*.vb", "*.xml", "*.config", "*.vbhtml", "*.cshtml", "*.as?x" }, new string[] { "*.designer.*", "*.generated.*" } ); EnsureTabIndents(changedFiles); */ CheckForIllegalFileCommits(new string[] {"*.suo", "*.user"}); } private void CheckForIllegalFileCommits(string[] filesToExclude) { string[] illegalFiles = GetCommitFiles( new string[] { "A", "U" }, filesToExclude, new string[] {} ); if (illegalFiles.Length > 0) { Echo(String.Format("You cannot commit the following files: {0}", String.Join(",", illegalFiles))); } } private void EnsureTabIndents(string[] filesToCheck) { foreach (string fileName in filesToCheck) { string contents = GetFileContents(fileName); string[] lines = contents.Replace("\r\n", "\n").Replace("\r", "\n").Split(new string[] { "\n" }, StringSplitOptions.None); var linesWithSpaceIndents = Enumerable.Range(0, lines.Length) .Where(i => lines[i].StartsWith(" ")) .Select(i => i + 1) .Take(11) .ToList(); if (linesWithSpaceIndents.Count > 0) { var message = String.Format("{0} has spaces for indents on line(s): {1}", fileName, String.Join(",", linesWithSpaceIndents)); if (linesWithSpaceIndents.Count > 10) message += "..."; Echo(message); } } } private string GetFileContents(string fileName) { string args = GetSvnLookCommandArgs("cat") + " \"" + fileName + "\""; string svnlookResults = ExecCmd("svnlook", args); return svnlookResults; } private void CheckCommitMessageLength(int minLength) { string args = GetSvnLookCommandArgs("log"); string svnlookResults = ExecCmd("svnlook", args); svnlookResults = (svnlookResults ?? "").Trim(); if (svnlookResults.Length < minLength) { if (svnlookResults.Length > 0) { Echo("Your commit message was too short."); } Echo("Please describe the changes you've made in a commit message in order to successfully commit. Include support ticket number if relevant."); } } private string[] GetCommitFiles(string[] changedIds, string[] includedFiles, string[] exclusions) { string args = GetSvnLookCommandArgs("changed"); string svnlookResults = ExecCmd("svnlook", args); string[] lines = svnlookResults.Split(new string[] { "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); var includedPatterns = (from a in includedFiles select ConvertWildcardPatternToRegex(a)).ToArray(); var excludedPatterns = (from a in exclusions select ConvertWildcardPatternToRegex(a)).ToArray(); var opts = RegexOptions.IgnoreCase; var results = from line in lines let fileName = line.Substring(1).Trim() let changeId = line.Substring(0, 1).ToUpper() where changedIds.Any(x => x.ToUpper() == changeId) && includedPatterns.Any(x => Regex.IsMatch(fileName, x, opts)) && !excludedPatterns.Any(x => Regex.IsMatch(fileName, x, opts)) select fileName; return results.ToArray(); } private string GetSvnLookCommandArgs(string cmdType) { string args = String.Format("{0} -t {1} \"{2}\"", cmdType, this.SvnTx, this.RepoPath); return args; } /// <summary> /// Executes a command line call and returns the output from stdout. /// Raises an error is stderr has any output. /// </summary> private string ExecCmd(string command, string args) { Process proc = new Process(); proc.StartInfo.FileName = command; proc.StartInfo.Arguments = args; proc.StartInfo.UseShellExecute = false; proc.StartInfo.CreateNoWindow = true; proc.StartInfo.RedirectStandardOutput = true; proc.StartInfo.RedirectStandardError = true; proc.Start(); var stdOut = proc.StandardOutput.ReadToEnd(); var stdErr = proc.StandardError.ReadToEnd(); proc.WaitForExit(); // Do after ReadToEnd() call per: http://chrfalch.blogspot.com/2008/08/processwaitforexit-never-completes.html if (!string.IsNullOrWhiteSpace(stdErr)) { throw new Exception(string.Format("Error: {0}", stdErr)); } return stdOut; } /// <summary> /// Writes the string provided to the Message Buffer - this fails /// the commit and this message is presented to the comitter. /// </summary> private void Echo(object s) { this.MessageBuffer.AppendLine((s == null ? "" : s.ToString())); } /// <summary> /// Takes a wildcard pattern (like *.bat) and converts it to the equivalent RegEx pattern /// </summary> /// <param name="wildcardPattern">The wildcard pattern to convert. Syntax similar to VB Like operator with the addition of pipe ("|") delimited patterns.</param> /// <returns>A regex pattern that is equivalent to the wildcard pattern supplied</returns> private string ConvertWildcardPatternToRegex(string wildcardPattern) { if (string.IsNullOrEmpty(wildcardPattern)) return ""; // Split on pipe string[] patternParts = wildcardPattern.Split('|'); // Turn into regex pattern that will match the whole string with ^$ StringBuilder patternBuilder = new StringBuilder(); bool firstPass = true; patternBuilder.Append("^"); foreach (string part in patternParts) { string rePattern = Regex.Escape(part); // add support for ?, #, *, [...], and [!...] rePattern = rePattern.Replace("\\[!", "[^"); rePattern = rePattern.Replace("\\[", "["); rePattern = rePattern.Replace("\\]", "]"); rePattern = rePattern.Replace("\\?", "."); rePattern = rePattern.Replace("\\*", ".*"); rePattern = rePattern.Replace("\\#", "\\d"); if (firstPass) { firstPass = false; } else { patternBuilder.Append("|"); } patternBuilder.Append("("); patternBuilder.Append(rePattern); patternBuilder.Append(")"); } patternBuilder.Append("$"); string result = patternBuilder.ToString(); if (!IsValidRegexPattern(result)) { throw new ArgumentException(string.Format("Invalid pattern: {0}", wildcardPattern)); } return result; } private bool IsValidRegexPattern(string pattern) { bool result = true; try { new Regex(pattern); } catch { result = false; } return result; } } public class CommitException : Exception { public CommitException(string message) : base(message) { } }