How to require commit messages in VisualSVN server?
Asked Answered
P

10

49

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?

Peafowl answered 29/10, 2008 at 18:30 Comment(0)
C
37

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:

Cardona answered 29/10, 2008 at 18:36 Comment(4)
what's the code to require x chars on the pre-commit comments?Limonene
You will have to decide what language you wish to use if you are writing your own hook. I would just go find a pre-written hook to accomplish whatever your needs are and try to find it in a language with which you are comfortable. You may need to modify the code, which shouldn't be to difficult in this case.Cardona
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.Limonene
@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.html. Here is another part of the SVN book referring to installing hooks: svnbook.red-bean.com/en/1.4/svn.reposadmin.create.htmlCardona
P
65

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
Pointless answered 12/8, 2009 at 14:18 Comment(8)
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...."Karmen
It's good practive to use environment variable VISUALSVN_SERVER to discover location of svnlook. I.e.: set SVNLOOK="%VISUALSVN_SERVER%\bin\svnlook.exe"Continuator
@Pointless 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.Holub
@StephenKennedy I no longer understand how the original worked at all. Try the updated version.Pointless
To allow spaces in the commit messages, I suggest using: "usebackq delims==" instead of just "usebackq"Evince
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"Balefire
@FriedHoeben this worked for me, I've updated the answer. hopefully someone with more rep than me will accept it!Chapbook
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!Basilicata
C
37

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:

Cardona answered 29/10, 2008 at 18:36 Comment(4)
what's the code to require x chars on the pre-commit comments?Limonene
You will have to decide what language you wish to use if you are writing your own hook. I would just go find a pre-written hook to accomplish whatever your needs are and try to find it in a language with which you are comfortable. You may need to modify the code, which shouldn't be to difficult in this case.Cardona
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.Limonene
@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.html. Here is another part of the SVN book referring to installing hooks: svnbook.red-bean.com/en/1.4/svn.reposadmin.create.htmlCardona
I
17

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.

Invalid answered 29/10, 2008 at 18:46 Comment(1)
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.Migrate
I
6

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.

Indies answered 19/7, 2012 at 12:8 Comment(2)
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"Flurry
yes, -join "``n" is required otherwise it fails with multi-line commit messages. I can't explain why, I don't know PS.Ideally
U
4

What VisualSVN offers you to enter as hooks are "Windows NT command scripts", which are basically batch files.

Writing if-then-else in batch files is very ugly and probably very hard to debug.

It will look something like the following (search for pre-commit.bat) (not tested):

SVNLOOK.exe log -t "%2" "%1" | grep.exe "[a-zA-Z0-9]" > nul || GOTO ERROR
GOTO OK
:ERROR
ECHO "Please enter comment and then retry commit!"
exit 1
:OK
exit 0 

You need a grep.exe on the path, %1 is the the path to this repository, %2 the name of the txn about to be committed. Also have a look at the pre-commit.tmpl in the hooks directory of your repository.

Unblinking answered 30/10, 2008 at 9:1 Comment(2)
you need to echo to stderr in order to get the message back to the client - use: ECHO "bad boy" 1>&2Displant
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?Limonene
P
4

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) {
  }
}
Phenolphthalein answered 11/10, 2012 at 21:32 Comment(1)
Awesome and super-helpful!Irvingirwin
D
3

Use this pre-commit hook on Windows. It's written in Windows Batch and uses grep command-line utility to check the commit length.

svnlook log -t "%2" "%1" | c:\tools\grep -c "[a-zA-z0-9]" > nul
if %ERRORLEVEL% NEQ 1 exit 0

echo Please enter a check-in comment 1>&2
exit 1

Remember that you'll need a copy of grep, I recommend the gnu tools version.

Displant answered 20/2, 2009 at 18:42 Comment(2)
the gnu tools link you gave does not work. The download is not available.Tanagra
its on SF! downloads.sourceforge.net/project/unxutils/unxutils/current/…Displant
J
3

Here's a Windows Shell JScript that you can use by specifying the hook as:

%SystemRoot%\System32\CScript.exe //nologo <..path..to..script> %1 %2

It's pretty easy-to-read, so go ahead an experiment.

BTW, the reason to do this in JScript is that it does not rely on any other tools (Perl, CygWin, etc.) to be installed.

if (WScript.Arguments.Length < 2)
{
    WScript.StdErr.WriteLine("Repository Hook Error: Missing parameters. Should be REPOS_PATH then TXN_NAME, e.g. %1 %2 in pre-commit hook");
    WScript.Quit(-1);
}

var oShell = new ActiveXObject("WScript.Shell");
var oFSO = new ActiveXObject("Scripting.FileSystemObject");

var preCommitStdOut = oShell.ExpandEnvironmentStrings("%TEMP%\\PRE-COMMIT." + WScript.Arguments(1) + ".stdout");
var preCommitStdErr = oShell.ExpandEnvironmentStrings("%TEMP%\\PRE-COMMIT." + WScript.Arguments(1) + ".stderr");

var commandLine = "%COMSPEC% /C \"C:\\Program Files\\VisualSVN Server\\bin\\SVNLook.exe\" log -t ";

commandLine += WScript.Arguments(1);
commandLine += " ";
commandLine += WScript.Arguments(0);
commandLine += "> " + preCommitStdOut + " 2> " + preCommitStdErr;


// Run Synchronously, don't show a window
// WScript.Echo("About to run: " + commandLine);
var exitCode = oShell.Run(commandLine, 0, true);

var fsOUT = oFSO.GetFile(preCommitStdOut).OpenAsTextStream(1);
var fsERR = oFSO.GetFile(preCommitStdErr).OpenAsTextStream(1);

var stdout = fsOUT && !fsOUT.AtEndOfStream ? fsOUT.ReadAll() : "";
var stderr = fsERR && !fsERR.AtEndOfStream ? fsERR.ReadAll() : "";

if (stderr.length > 0)
{
    WScript.StdErr.WriteLine("Error with SVNLook: " + stderr);
    WScript.Quit(-2);
}

// To catch naught commiters who write 'blah' as their commit message

if (stdout.length < 5)
{
    WScript.StdErr.WriteLine("Please provide a commit message that describes why you've made these changes.");
    WScript.Quit(-3);
}

WScript.Quit(0);
Jemine answered 4/8, 2009 at 15:15 Comment(1)
The only issue I have is that it still accepts "blah"... weird.Tanagra
R
3

Note: This Only Applies To TortoiseSVN

Simply right-click the top level of your Repository. In the context-menu select TortoiseSVN, then Properties, to see this dialog:

enter image description here

Click the New button near the bottom right, and select Log Sizes. Enter the number of characters you want to require for Commit and Lock (10 in example below).

enter image description here

Do a Commit from the top Level directory you just modified. Now your repository requires all users to Comment before Committing changes.

Reprehend answered 27/4, 2014 at 2:39 Comment(2)
This approach only works for local SVN repository with TortoiseSVN.Thromboplastin
Good point. I updated the post to reflect your comment. Thanks!Reprehend
M
2

Prior to adding commit hooks to my server, I just distributed svnprops to the TortoiseSVN clients.

So, as an alternative:

In TortoiseSVN -> Properties property name - add/set tsvn:logminsize appropriately.

This of course is no guarantee on the server as clients/users can opt not to do it, but you can distribute svnprops files if you like. This way, users don't have to set their own values - you can provide them to all users.

This also works for things like bugtraq: settings to link issue tracking stuff in the logs.

Moia answered 19/12, 2008 at 14:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.