如何在VisualSVN服务器中要求提交信息?

49

我们在Windows上设置了VisualSVN Server作为我们的Subversion服务器,我们在工作站上使用Ankhsvn + TortoiseSVN作为客户端。

如何配置服务器以要求提交消息不能为空?

注:commit messages是指在进行版本控制时,开发者必须填写的描述本次修改内容的信息。

10个回答

65

很高兴你问了这个问题。这是我们用普通的Windows批处理文件编写的pre-commit钩子脚本。如果日志消息少于6个字符,它将拒绝提交。只需将pre-commit.bat放到您的hooks目录中即可。

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

2
我做了一个小改进,适用于 Windows 2008 的 64 位版本:不要再使用“C:\ Program Files \ Vis ...”这样的路径,而是使用 Windows 环境变量,比如“%PROGRAMFILES%\ Vis ...”。 - Sander Versluys
4
使用环境变量VISUALSVN_SERVER来确定svnlook的位置是一个好的实践方法,例如: set SVNLOOK="%VISUALSVN_SERVER%\bin\svnlook.exe" - Ivan Zhakov
3
@sylvanaar 感谢您提供这个钩子,我们已经使用了一段时间。但是在升级到 VisualSVN Server 2.5 并升级我们的代码库后,它停止工作了。提交被预提交钩子(退出代码1)阻止并输出: svnlook: E205000:尝试“svnlook help”获取更多信息 svnlook: E205000:给出了太多参数有任何想法吗?我认为这与 svnlook 路径中的空格有关,但一直没有解决。 - Stephen Kennedy
1
@StephenKennedy,我已经完全不明白原来的代码是如何工作的了。请尝试使用更新版本。 - sylvanaar
1
为了允许提交信息中包含空格,我建议使用“usebackq delims==”而不是仅使用“usebackq”。 - Fried Hoeben
显示剩余3条评论

37
VisualSVN Server 3.9 提供了 VisualSVNServerHooks.exe check-logmessage 预提交钩子,帮助您拒绝空白或太短的提交日志。请参考文章 KB140: 验证 VisualSVN Server 中的提交日志消息 以获取详细说明。
除了内置的 VisualSVNServerHooks.exe,VisualSVN Server 和 SVN 通常使用一些其他钩子来完成此类任务:
  • start-commit — 在提交事务开始之前运行,可用于进行特殊权限检查。
  • pre-commit — 在事务结束但提交之前运行。通常用于验证非零长度的日志消息等。
  • post-commit — 在提交事务后运行。可用于发送电子邮件或备份存储库。
  • pre-revprop-change — 在修改版本属性之前运行。可用于检查权限。
  • post-revprop-change — 在修改版本属性后运行。可用于电子邮件或备份这些更改。

你需要使用 pre-commit 钩子。你可以用任何平台支持的语言自己编写,但网上也有许多脚本可供使用。通过搜索“svn precommit hook to require comment”,我找到了几个看起来符合要求的脚本:


2
请问在提交前评论中要求输入多少字符的代码是什么? - PositiveGuy
如果你要编写自己的钩子,你需要决定使用哪种语言。我建议你去找一个预先编写好的钩子来满足你的需求,并尽可能选择你熟悉的语言。在这种情况下,你可能需要修改代码,但这并不难。 - Jason Jackson
编写脚本是个问题。我也不能像魔术一样轻易地想出一个告诉SVN强制注释的脚本。我也曾经为此苦苦挣扎。我正在使用Tortoise和Visual SVN服务器。当你看着现有的钩子时,如果你毫无头绪,那就不是那么容易的事情了。 - PositiveGuy
2
@coffeeaddict,我不明白你的意思。你是指你不知道如何编写脚本,还是不确定在哪里放置它,或者其他什么问题?我刚刚在谷歌上搜索了“pre-commit svn require comment”(http://www.google.com/search?q=pre-commit+svn+require+comment),找到了各种各样的脚本。这是SVN书中关于钩子的条目:http://svnbook.red-bean.com/en/1.4/svn.ref.reposhooks.pre-commit.html。这是SVN书的另一部分,涉及安装钩子:http://svnbook.red-bean.com/en/1.4/svn.reposadmin.create.html。 - Jason Jackson

17
你的问题已经得到了技术方面的回答。我想补充一下社交方面的答案,那就是:“通过与团队建立提交消息标准并让他们同意(或接受)需要表达性提交消息的理由来实现。”
我见过太多的提交消息,比如 "patch"、 "typo"、 "fix" 或类似的,我已经数不清了。
确实-让每个人清楚为什么需要它们。
以下是一些理由的示例:
  • 生成的变更记录(嗯-如果我知道它们将(带有我的名称)公开可见-即使仅供团队使用,这实际上会成为一个很好的自动工具来强制执行良好的消息)
  • 许可问题:您可能需要稍后了解代码的来源,例如,如果您想更改代码的许可证(一些组织甚至有提交消息格式的标准-好吧,您可以自动检查此操作,但不一定会得到良好的提交消息)
  • 与其他工具的互操作性,例如与您的版本控制交互并从提交消息中提取信息的缺陷跟踪器/问题管理系统。
希望这能对您有所帮助,除了有关预提交钩子的技术答案之外。

3
我曾在一家机构工作,甚至在发布日期接近时,还会系统地回滚那些没有合适提交信息的提交记录。 - Warren Pena

6
这是一个由Batch + PowerShell组成的双部分示例pre-commit钩子,它拒绝提交少于25个字符的日志信息。
pre-commit.batpre-commit.ps1两个文件都放入存储库hooks文件夹中,例如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%

注意1:只有pre-commit.bat可以被VisualSVN调用,然后pre-commit.ps1是由pre-commit.bat调用的。
注意2:pre-commit.bat也可能被命名为pre-commit.cmd
注意3:如果您在某些带重音符号的字符和[Console] :: Error.WriteLine输出方面遇到编码问题,则可以在pre-commit.bat中添加例如chcp 1252,在@echo off之后的下一行。

2
由于某些原因,$dataline 变成了一个字符串数组,而不是一行。所以我将那一行改为 $datalines = ($log | where {$_.trim() -ne ""}) -join "``n" - leiflundgren
是的,-join "``n" 是必需的,否则会出现多行提交信息失败的情况。我无法解释为什么,因为我不懂PS。 - Laurent.B

4
我们使用优秀的CS-Script工具来进行预提交挂钩,这样我们就可以用我们正在开发的语言编写脚本。以下是一个示例,确保提交消息超过10个字符,并确保不检入.suo和.user文件。您还可以测试制表符/空格缩进,或在签入时执行小型代码标准强制执行,但要小心使您的脚本做得太多,因为您不希望减慢提交速度。
// 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) {
  }
}

真是太棒了,非常有帮助! - Andriy Volkov

4
VisualSVN提供给您的钩子是“Windows NT命令脚本”,基本上就是批处理文件。
在批处理文件中编写if-then-else非常丑陋,而且可能很难调试。它看起来会像以下内容(搜索pre-commit.bat)(未经测试):
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 

您需要在路径上拥有grep.exe,%1是该存储库的路径,%2是即将提交的事务名称。还要查看存储库钩子目录中的pre-commit.tmpl。


1
为了将消息传回客户端,您需要将其回显到stderr - 使用:ECHO“bad boy”1>&2 - gbjbaanb
抱歉,我不是NT命令脚本大师,也没有时间去学习和研究它。你能告诉我们如何检查这里的注释长度吗? - PositiveGuy

3

注意:这仅适用于TortoiseSVN

只需右键单击您的版本库顶部级别。在上下文菜单中选择TortoiseSVN,然后选择属性,以查看此对话框:

enter image description here

点击右下角的New按钮,并选择Log Sizes。输入您想要在提交和锁定时要求的字符数(以下示例中为10)。

enter image description here

从刚刚修改的顶级目录进行提交。现在您的版本库要求所有用户在提交更改之前添加注释。


这种方法仅适用于使用TortoiseSVN的本地SVN存储库。 - M-Razavi
好的,我已经根据您的评论更新了帖子。谢谢! - Mr. B

3

在Windows上使用此预提交挂钩。它是用Windows批处理编写的,并使用grep命令行实用程序来检查提交长度。

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

请记住,您需要一份grep的副本,我建议使用gnu工具版本


你提供的GNU工具链接无法使用。下载不可用。 - irperez
1
它在SF上!http://downloads.sourceforge.net/project/unxutils/unxutils/current/UnxUtils.zip?use_mirror=kent - gbjbaanb

3
以下是一个Windows Shell JScript,您可以通过指定钩子来使用它:
%SystemRoot%\System32\CScript.exe //nologo <..path..to..script> %1 %2

这很容易阅读,所以请尽情实验。

顺便说一下,使用JScript的原因是它不依赖于任何其他工具(如Perl、CygWin等)的安装。

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);

我唯一的问题是它仍然接受“blah”...很奇怪。 - irperez

2
在我的服务器上添加提交钩子之前,我只是将svnprops分发给TortoiseSVN客户端。因此,作为替代方案:在TortoiseSVN->属性中添加/设置属性名称tsvn:logminsize。当然,这并不能保证服务器的使用,因为客户端/用户可以选择不这样做,但如果您愿意,您可以分发svnprops文件。这样,用户就不必设置自己的值-您可以向所有用户提供它们。这也适用于诸如bugtraq:设置以将问题跟踪信息链接到日志中的设置。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接