How to require commit messages in VisualSVN server?

26,091

Solution 1

VisualSVN Server 3.9 provides the VisualSVNServerHooks.exe check-logmessage pre-commit hook that helps you reject commits with empty or short log messages. See the article KB140: Validating commit log messages in VisualSVN Server for instructions.

Besides the built-in VisualSVNServerHooks.exe, VisualSVN Server and SVN in general uses a number of hooks to accomplish tasks like this.

  • start-commit — run before commit transaction begins, can be used to do special permission checking
  • pre-commit — run at the end of the transaction, but before commit. Often used to validate things such as a non zero length log message.
  • post-commit — runs after the transaction has been committed. Can be used for sending emails, or backing up repository.
  • pre-revprop-change — runs before a revision property change. Can be used to check permissions.
  • post-revprop-change — runs after a revision property change. Can be used to email or backup these changes.

You need to use the pre-commit hook. You can write it yourself in just about any language your platform supports, but there are a number of scripts on the web. Googling "svn precommit hook to require comment" I found a couple that looked like they would fit the bill:

Solution 2

I'm glad you asked this question. This is our pre-commit hook script written in common Windows Batch. It denies commit if the log message is less than 6 characters. Just put the pre-commit.bat to your hooks directory.

pre-commit.bat

setlocal enabledelayedexpansion

set REPOS=%1
set TXN=%2

set SVNLOOK="%VISUALSVN_SERVER%\bin\svnlook.exe"

SET M=

REM Concatenate all the lines in the commit message
FOR /F "usebackq delims==" %%g IN (`%SVNLOOK% log -t %TXN% %REPOS%`) DO SET M=!M!%%g

REM Make sure M is defined
SET M=0%M%

REM Here the 6 is the length we require
IF NOT "%M:~6,1%"=="" goto NORMAL_EXIT

:ERROR_TOO_SHORT
echo "Commit note must be at least 6 letters" >&2
goto ERROR_EXIT

:ERROR_EXIT
exit /b 1

REM All checks passed, so allow the commit.
:NORMAL_EXIT
exit 0

Solution 3

The technical answers to your question have already been given. I'd like to add the social answer, which is: "By establishing commit message standards with your team and getting them to agree (or accept) reasons why one would need expressive commit messages"

I've seen so many commit messages that said "patch", "typo", "fix" or similar that I've lost count.

Really - make it clear to everybody why you'd need them.

Examples for reasons are:

  • Generated Changenotes (well - this'd actually make a nice automatic tool to enforce good messages if I know that they will be (with my name) publically visible - if only for the team)
  • License issues: You might need to know the origin of code later, e.g. should you want to change the license to your code (Some organizations even have standards for commit message formatting - well, you could automate the checking for this, but you'd not necessarily get good commit messages with this)
  • Interoperability with other tools, e.g. bugtrackers/issue management systems that interface with your version control and extract information from the commit messages.

Hope that helps, additionally to the technical answers about precommit hooks.

Solution 4

Here is a two part sample Batch + PowerShell pre-commit hook that denies commit a log message with less than 25 characters.

Put both pre-commit.bat and pre-commit.ps1 into your repository hooks folder, e.g. C:\Repositories\repository\hooks\

pre-commit.ps1

# Store hook arguments into variables with mnemonic names
$repos    = $args[0]
$txn      = $args[1]

# Build path to svnlook.exe
$svnlook = "$env:VISUALSVN_SERVER\bin\svnlook.exe"

# Get the commit log message
$log = (&"$svnlook" log -t $txn $repos)

# Check the log message contains non-empty string
$datalines = ($log | where {$_.trim() -ne ""})
if ($datalines.length -lt 25)
{
  # Log message is empty. Show the error.
  [Console]::Error.WriteLine("Commit with empty log message is prohibited.")
  exit 3
}

exit 0

pre-commit.bat

@echo off
set PWSH=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe
%PWSH% -command $input ^| %1\hooks\pre-commit.ps1 %1 %2
if errorlevel 1 exit %errorlevel%

Note 1 : pre-commit.bat is the only one that can be called by VisualSVN and then pre-commit.ps1 is the one that is called by pre-commit.bat.

Note 2 : pre-commit.bat may also be named pre-commit.cmd.

Note 3 : If you experiment encoding issues with some accented characters and the [Console]::Error.WriteLine output, then add for instance chcp 1252 into pre-commit.bat, next line after @echo off.

Solution 5

We use the excellent CS-Script tool for our pre-commit hooks so that we can write scripts in the language we're doing development in. Here's an example that ensures there's a commit message longer than 10 characters, and ensures that .suo and .user files aren't checked in. You can also test for tab/space indents, or do small code standards enforcement at check-in, but be careful making your script do too much as you don't want to slow down a 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's 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) {
  }
}
Share:
26,091
Evan Nagle
Author by

Evan Nagle

I work with Olo solving problems for people who want to order food online. I'm also a Stack Overflow alumnus and Microsoft alumnus. I love Jesus, my family, programming, Texas, and craft beer. Find me on twitter and GitHub as @aggieben.

Updated on September 14, 2020

Comments

  • Evan Nagle
    Evan Nagle over 3 years

    We've got VisualSVN Server set up as our Subversion server on Windows, and we use Ankhsvn + TortoiseSVN as clients on our workstations.

    How can you configure the server to require commit messages to be non-empty?

  • gbjbaanb
    gbjbaanb over 15 years
    you need to echo to stderr in order to get the message back to the client - use: ECHO "bad boy" 1>&2
  • PositiveGuy
    PositiveGuy about 15 years
    what's the code to require x chars on the pre-commit comments?
  • PositiveGuy
    PositiveGuy about 15 years
    sorry, not an NT command script guru and don't have time to hack one up and hit my head learning it. Any chance you can tell us how to check the comment length here?
  • irperez
    irperez almost 15 years
    The only issue I have is that it still accepts "blah"... weird.
  • gbjbaanb
    gbjbaanb almost 15 years
  • Warren Pena
    Warren Pena almost 15 years
    I've worked for an organization that even went so far as to systematically roll back commits without acceptable commit messages, especially as the release approached.
  • Sander Versluys
    Sander Versluys over 14 years
    i've got a little improvement which is usefull in 64bit version of windows 2008: instead of using "C:\Program Files\Vis..." use Windows Environment Variables like "%PROGRAMFILES%\Vis...."
  • PositiveGuy
    PositiveGuy almost 14 years
    writing the script is the problem. I too can't just magically come up with a script that tells SVN to force comments. I too have struggled with this. I'm using Tortoise and Visual SVN server. It's not that easy when you have no clue even when looking at existing hooks.
  • Jason Jackson
    Jason Jackson almost 14 years
    @coffeeaddict, I don't get what you mean. Do you mean you don't know how to write a script, or you aren't sure where to put it, or what? I just googled "pre-commit svn require comment" (google.com/search?q=pre-commit+svn+require+comment) and found all kinds of scripts. Here is the SVN book entry on the hook: svnbook.red-bean.com/en/1.4/svn.ref.reposhooks.pre-commit.ht‌​ml. Here is another part of the SVN book referring to installing hooks: svnbook.red-bean.com/en/1.4/svn.reposadmin.create.html
  • Ivan Zhakov
    Ivan Zhakov almost 14 years
    It's good practive to use environment variable VISUALSVN_SERVER to discover location of svnlook. I.e.: set SVNLOOK="%VISUALSVN_SERVER%\bin\svnlook.exe"
  • Stephen Kennedy
    Stephen Kennedy over 12 years
    @sylvanaar We've been using this hook for some time (thank you very much!) but after upgrading to VisualSVN Server 2.5 and upgrading our repository it's stopped working. Commit blocked by pre-commit hook (exit code 1) with output: svnlook: E205000: Try 'svnlook help' for more info svnlook: E205000: Too many arguments given Any idea? I think it's to do with spaces in the svnlook path but haven't been able to resolve it.
  • sylvanaar
    sylvanaar over 12 years
    @StephenKennedy I no longer understand how the original worked at all. Try the updated version.
  • Fried Hoeben
    Fried Hoeben over 12 years
    To allow spaces in the commit messages, I suggest using: "usebackq delims==" instead of just "usebackq"
  • Summer-Time
    Summer-Time over 12 years
    hi, i got the error Repository hook failed Commit failed (details follow): Commit blocked by pre-commit hook (exit code 1) with output: svnlook.exe: missing argument: t Type 'svnlook help' for usage. "Commit note must be at least 6 letters"
  • enashnash
    enashnash about 12 years
    @FriedHoeben this worked for me, I've updated the answer. hopefully someone with more rep than me will accept it!
  • Mas Biru
    Mas Biru over 10 years
    Awesome and super-helpful!
  • M-Razavi
    M-Razavi over 9 years
    This approach only works for local SVN repository with TortoiseSVN.
  • Mr. B
    Mr. B over 9 years
    Good point. I updated the post to reflect your comment. Thanks!
  • Bent Tranberg
    Bent Tranberg about 9 years
    To paraphrase Simon Cowell, that's the biggest +1 I've given this season. For anybody else trying this, based on my experience while fumbling around trying to figure out exactly what to do: All you need to do is create this file in the hook directory of the particular repo. Nothing else. It worked instantly. This was on VisualSVN Server 3.2.2, and of course I can't guarantee it's the same with every SVN server installation. Thanks!
  • leiflundgren
    leiflundgren about 7 years
    By some reason $dataline became an array of strings, rather than one line. So I changed that line to $datalines = ($log | where {$_.trim() -ne ""}) -join "``n"
  • Laurent.B
    Laurent.B over 6 years
    yes, -join "``n" is required otherwise it fails with multi-line commit messages. I can't explain why, I don't know PS.