首页 文章

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

提问于
浏览
49

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

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

11 回答

  • 6

    VisualSVN Server 3.9提供了 VisualSVNServerHooks.exe check-logmessage 预提交挂钩,可帮助您拒绝空或短日志消息的提交 . 有关说明,请参阅文章KB140: Validating commit log messages in VisualSVN Server .

    除了内置的 VisualSVNServerHooks.exe ,VisualSVN Server和SVN通常使用number of hooks来完成这样的任务 .

    • start-commit - 在提交事务开始之前运行,可用于执行特殊权限检查

    • pre-commit - 在事务结束时运行,但在提交之前运行 . 通常用于验证诸如非零长度日志消息之类的内容 .

    • post-commit - 在提交事务后运行 . 可用于发送电子邮件或备份存储库 .

    • pre-revprop-change - 在修订版属性更改之前运行 . 可用于检查权限 .

    • post-revprop-change - 在修订版属性更改后运行 . 可用于通过电子邮件发送或备份这些更改 .

    你需要使用 pre-commit 钩子 . 您可以使用您的平台支持的任何语言自己编写,但Web上有许多脚本 . 谷歌搜索"svn precommit hook to require comment"我找到了一对看起来像是符合条件的夫妇:

  • 17

    我很高兴你问这个问题 . 这是我们共同编写的预提交钩子脚本Windows Batch . 如果日志消息少于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
    
  • 1

    已经给出了您的问题的技术答案 . 我想添加社交答案,即:“通过与您的团队 Build 提交消息标准并让他们同意(或接受)为什么需要表达性提交消息的原因”

    我看过很多提交消息,上面写着“补丁”,“拼写错误”,“修复”或类似错误 .

    真的 - 让每个人都清楚你为什么需要它们 .

    原因的例子是:

    • Generated Changenotes (好吧 - 这实际上是一个很好的自动工具来强制执行好消息,如果我知道他们将(以我的名字)公开可见 - 如果只是为了团队)

    • License issues :您可能需要稍后知道代码的来源,例如你是否想要将许可证更改为你的代码(有些组织甚至有提交消息格式的标准 - 好吧,你可以自动检查这个,但你不一定得到好的提交消息)

    • Interoperability with other tools ,例如bugtrackers / issue管理系统,它与您的版本控制接口并从提交消息中提取信息 .

    希望有帮助,除了关于precommit钩子的技术答案 .

  • 4

    这是一个两部分示例 Batch + PowerShell pre-commit hook,它拒绝提交少于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.ps1pre-commit.bat 调用的那个 .

    注2: pre-commit.bat 也可以命名为 pre-commit.cmd .

    注3:如果您尝试使用某些重音字符和 [Console]::Error.WriteLine 输出进行编码问题,则将 chcp 1252 添加到 pre-commit.bat@echo off 之后的下一行 .

  • 3

    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是要提交的txn的名称 . 还要查看存储库的hooks目录中的pre-commit.tmpl .

  • 3

    我们使用优秀的CS-Script工具进行预提交钩子,这样我们就可以用我们的语言编写脚本,确保有's a commit message longer than 10 characters, and ensures that .suo and .user files aren'签入 . 您还可以测试制表符/空格缩进,或执行小代码标准执行在办理登机手续时,但要小心使你的脚本做得太多,因为你不想减慢提交速度 .

    // 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) {
      }
    }
    
  • 2

    这是一个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);
    
  • 37

    在Windows上使用此预提交挂钩 . 它是用Windows Batch编写的,并使用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 tools version .

  • 65

    Note: This Only Applies To TortoiseSVN

    只需右键单击存储库的顶级 . 在上下文菜单中选择TortoiseSVN,然后选择Properties,以查看此对话框:

    enter image description here

    单击右下角附近的“新建”按钮,然后选择“日志大小” . 输入提交和锁定所需的字符数(下例中为10) .

    enter image description here

    从刚修改的顶级目录执行提交 . 现在,您的存储库要求所有用户在提交更改之前进行注释 .

  • 3

    在将提交挂钩添加到我的服务器之前,我只是将svnprops分发给TortoiseSVN客户端 .

    所以,作为替代方案:

    在TortoiseSVN - >属性属性名称 - 适当添加/设置 tsvn:logminsize .

    这当然不能保证在服务器上,因为客户端/用户可以选择不这样做,但是如果你愿意,你可以分发svnprops文件 . 这样,用户就不必设置自己的值 - 您可以将它们提供给所有用户 .

    这也适用于bugtraq:设置以链接日志中的问题跟踪内容 .

  • 4

    我相信你必须设置一个预提交钩子来检查消息 .

    事实上,通过谷歌搜索我得到的第一个结果是一个perl预提交脚本,完全按照你的意图 .

    Perl pre-commit hook example (untested)

相关问题