How to require commit messages in VisualSVN server?

后端 未结 10 1924
故里飘歌
故里飘歌 2020-12-07 12:00

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 se

10条回答
  •  天命终不由人
    2020-12-07 12:11

    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 {
    
      /// Controls the procedure flow of this script
      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; }
    
      /// Constructor
      public PreCommitCS(string repoPath, string svnTx) {
        this.RepoPath = repoPath;
        this.SvnTx = svnTx;
        this.MessageBuffer = new StringBuilder();
      }
    
      /// Main logic controller
      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;
      }
    
      /// 
      /// Executes a command line call and returns the output from stdout.
      /// Raises an error is stderr has any output.
      /// 
      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;
      }
    
      /// 
      /// Writes the string provided to the Message Buffer - this fails
      /// the commit and this message is presented to the comitter.
      /// 
      private void Echo(object s) {
        this.MessageBuffer.AppendLine((s == null ? "" : s.ToString()));
      }
    
      /// 
      /// Takes a wildcard pattern (like *.bat) and converts it to the equivalent RegEx pattern
      /// 
      /// The wildcard pattern to convert.  Syntax similar to VB's Like operator with the addition of pipe ("|") delimited patterns.
      /// A regex pattern that is equivalent to the wildcard pattern supplied
      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) {
      }
    }
    

提交回复
热议问题