Index: project/core/sourcecontrol/Mks.cs =================================================================== --- project/core/sourcecontrol/Mks.cs (revision 4005) +++ project/core/sourcecontrol/Mks.cs (working copy) @@ -3,6 +3,7 @@ using System.IO; using Exortech.NetReflector; using ThoughtWorks.CruiseControl.Core.Util; +using System.Text.RegularExpressions; namespace ThoughtWorks.CruiseControl.Core.Sourcecontrol { @@ -12,6 +13,7 @@ public const string DefaultExecutable = "si.exe"; public const int DefaultPort = 8722; public const bool DefaultAutoGetSource = true; + private readonly MksHistoryParser mksHistoryParser; public Mks() : this(new MksHistoryParser(), new ProcessExecutor()) { @@ -19,6 +21,7 @@ public Mks(IHistoryParser parser, ProcessExecutor executor) : base(parser, executor) { + mksHistoryParser = (MksHistoryParser)parser; } [ReflectorProperty("executable")] @@ -50,6 +53,9 @@ public override Modification[] GetModifications(IIntegrationResult from, IIntegrationResult to) { + Log.Info(string.Format("Getting Mks project path")); + mksHistoryParser.MksProjectPath = GetMksProjectPath(); + ProcessInfo info = NewProcessInfoWithArgs(BuildModsCommand()); Log.Info(string.Format("Getting Modifications: {0} {1}", info.FileName, info.Arguments)); Modification[] modifications = GetModifications(info, from.StartTime, to.StartTime); @@ -57,6 +63,25 @@ return ValidModifications(modifications, from.StartTime, to.StartTime); } + public string GetMksProjectPath() + { + ProcessInfo info = NewProcessInfoWithArgs(BuildProjectInfoCommand()); + ProcessExecutor processExecutor = new ProcessExecutor(); + ProcessResult result = processExecutor.Execute(info); + if(result.Failed) + { + throw new CruiseControlException("Could not execute: " + info.FileName + " " + info.Arguments + Environment.NewLine + result.StandardOutput + result.StandardError); + } + + Regex regex = new Regex(@"^Project Name:\s*(?.*)", RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase | RegexOptions.Multiline); + Match match = regex.Match(result.StandardOutput); + if(match.Success==false) + { + throw new CruiseControlException("Could not find mks project path!"); + } + return match.Groups["Path"].Value; + } + /* as the "mods" command gets modifications to a project between checkpoints on the working project, * if CheckpointOnSuccess is set to false the modifications are filtered according to the modified time of the files * */ @@ -86,8 +111,6 @@ public override void GetSource(IIntegrationResult result) { - result.BuildProgressInformation.SignalStartRunTask("Getting source from MKS"); - if (AutoGetSource) { ProcessInfo resynchProcess = NewProcessInfoWithArgs(BuildResyncCommand()); @@ -152,14 +175,45 @@ return buffer.ToString(); } - //MEMBER_INFO_TEMPLATE = "memberinfo -S {SandboxRoot\SandboxFile} --user={user} --password={password} {member}" + //PROJECTINFO_TEMPLATE = "projectinfo -R -S {SandboxRoot\SandboxFile} --user={user} --password={password} --quiet --noacl --noassociatedIssues --noattributes --nodevpaths --noshowCheckpointDescription" + private string BuildProjectInfoCommand() + { + ProcessArgumentBuilder buffer = new ProcessArgumentBuilder(); + buffer.AppendArgument("projectinfo"); + buffer.AppendArgument("--noacl"); + buffer.AppendArgument("--noassociatedIssues"); + buffer.AppendArgument("--noattributes"); + buffer.AppendArgument("--nodevpaths"); + buffer.AppendArgument("--noshowCheckpointDescription"); + AppendCommonArguments(buffer, false); + return buffer.ToString(); + } + + //MEMBER_INFO_TEMPLATE = "memberinfo --user={user} --password={password} --quiet {full member filepath}" private string BuildMemberInfoCommand(Modification modification) { ProcessArgumentBuilder buffer = new ProcessArgumentBuilder(); buffer.AppendArgument("memberinfo"); - AppendCommonArguments(buffer, false); - string modificationPath = (modification.FolderName == null) ? SandboxRoot : Path.Combine(SandboxRoot, modification.FolderName); - buffer.AddArgument(Path.Combine(modificationPath, modification.FileName)); + + if (modification.FolderName != null) + { + // If the sandbox is on a file system required backslashes, we need to flip the slashes on the MKS + // project folder (which has "/" slashes). + if (SandboxRoot.IndexOf("\\") > 0) + { + modification.FolderName = modification.FolderName.Replace("/", "\\"); + } + string mksProjectPath = Path.Combine(SandboxRoot, modification.FolderName); + AppendMemberInfoArguments(buffer, Path.Combine(mksProjectPath, modification.FileName)); + } + else + { + // This should only be a fallback if we couldn't identify the MKS project path + //MEMBER_INFO_TEMPLATE = "memberinfo -S {SandboxRoot\SandboxFile} --user={user} --password={password} --quiet {member filename}" + AppendCommonArguments(buffer, false); + buffer.AddArgument(Path.Combine(SandboxRoot, modification.FileName)); + } + return buffer.ToString(); } @@ -175,6 +229,15 @@ buffer.AppendArgument("--quiet"); } + + private void AppendMemberInfoArguments(ProcessArgumentBuilder buffer, string mksProjectPath) + { + buffer.AppendArgument("--user={0}", User); + buffer.AppendArgument("--password={0}", Password); + buffer.AppendArgument("--quiet"); + buffer.AppendArgument("\"" + mksProjectPath + "\""); + } + private void RemoveReadOnlyAttribute() { ProcessArgumentBuilder buffer = new ProcessArgumentBuilder(); @@ -188,4 +251,4 @@ return new ProcessInfo(Executable, args); } } -} \ No newline at end of file +} Index: project/core/sourcecontrol/MksHistoryParser.cs =================================================================== --- project/core/sourcecontrol/MksHistoryParser.cs (revision 4005) +++ project/core/sourcecontrol/MksHistoryParser.cs (working copy) @@ -8,16 +8,48 @@ { public class MksHistoryParser : IHistoryParser { - private static Regex MODIFICATION_SEARCH_REGEX = new Regex(@"^(?(Revision|Added|Deleted))\s(changed|member):\s+(?.*?)(was|now).*\s(to|at)\s(?\d+(\.\d+)*)(\r\n|$)", RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase | RegexOptions.Multiline); - private static Regex MEMBER_INFO_REGEX = new Regex(@".*?\s+Created\sBy:\s(?\w+)\son\s(?.*?)\r\n(.|\r\n)*?\s+Revision\sDescription:\r\n(?(.|\r\n)*?)\s+Labels:(.|\r\n|$)*?", RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase); + private string mksProjectPath; + public string MksProjectPath + { + get + { + return mksProjectPath; + } + set + { + mksProjectPath = RemoveProject(value); + } + } + + private static readonly Regex SUBPROJECT_SEARCH_REGEX = new Regex(@"^\s*((Subproject\schanged|Dropped\ssubproject|Added\ssubproject):\s+(?.*?)(\s+at.*(\r\n|$)|\s+was.*(\r\n|$)|\s+now.*(\r\n|$)|\s+from.*(\r\n|$)|(\r\n|$)))", + RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase | RegexOptions.Multiline); + private static readonly Regex MODIFICATION_SEARCH_REGEX = new Regex(@"^\s*(?(Member\srevision|Revision|Added|Dropped|Deleted))\s(changed|member):\s+(?.*?)\s+(at|was|now|from).*\s(?\d+(\.\d+)*)(\r\n|$)", + RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase | RegexOptions.Multiline); + private static readonly Regex MEMBER_INFO_REGEX = new Regex(@".*?\s+Created\sBy:\s+.*\((?.*)\)\son\s(?.*)\r\n(.|\r\n)*?\s+Revision\sDescription:\r\n\s*(?(.|\r\n)*?)\s+Labels:(.|\r\n|$)*?", + RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase); + public virtual Modification[] Parse(TextReader history, DateTime from, DateTime to) { ArrayList result = new ArrayList(); - MatchCollection revisionMatches = MODIFICATION_SEARCH_REGEX.Matches(history.ReadToEnd()); + string modifications = history.ReadToEnd(); + MatchCollection subprojectMatches = SUBPROJECT_SEARCH_REGEX.Matches(modifications); + MatchCollection revisionMatches = MODIFICATION_SEARCH_REGEX.Matches(modifications); + foreach (Match revisionMatch in revisionMatches) { - result.Add(CreateModification(revisionMatch)); + string SubProjectPath = string.Empty; + + for (int i = subprojectMatches.Count - 1; i >= 0; i--) + { + if (subprojectMatches[i].Index < revisionMatch.Index) + { + SubProjectPath = ParseSubprojectFolder(subprojectMatches[i].Groups["SubprojectName"].Value.Trim()); + break; + } + } + Modification modification = CreateModification(revisionMatch, SubProjectPath); + result.Add(modification); } return (Modification[]) result.ToArray(typeof (Modification)); @@ -25,29 +57,58 @@ public virtual void ParseMemberInfoAndAddToModification(Modification modification, StringReader reader) { + MatchCollection memberInfoMatches = MEMBER_INFO_REGEX.Matches(reader.ReadToEnd()); foreach (Match match in memberInfoMatches) { modification.UserName = match.Groups["UserName"].Value.Trim(); modification.ModifiedTime = ParseDate(match.Groups["ModifiedTime"].Value.Trim()); - modification.Comment = match.Groups["Comment"].Value.Trim(); - } + modification.Comment = match.Groups["Comment"].Value.Trim(); + } + } - // Dates returned from MKS seem to be in format Aug 26, 2005 - 5:32 AM but I haven't been able to verify this for all locales - // This format is not supported by DateTime.Parse under .NET 2.0. So we TryParseExact to see if just this format can be read. - private static DateTime ParseDate(string dateString) - { - DateTime date; - if (DateTime.TryParseExact(dateString, "MMM d, yyyy - h:mm tt", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out date)) - return date; - return Convert.ToDateTime(dateString); - } + // Dates returned from some versions of MKS seem to be in format Aug 26, 2005 - 5:32 AM, but I haven't been + // able to verify this for all locales. This format is not supported by DateTime.Parse under .NET 2.0. + // So we TryParseExact to see if just this format can be read. + // + // Later versions of MKS (like 2007) return dates in format Aug 26, 2005 05:32:23 AM CST. For this case, + // trim off the time zone, because again, DateTime.Parse will get unhappy. + private static DateTime ParseDate(string dateString) + { + DateTime date; - private static Modification CreateModification(Match match) + string ParsedDateString = dateString.Remove(22).Trim(); + if (DateTime.TryParseExact(ParsedDateString, "MMM d, yyyy - h:mm tt", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out date)) + return date; + + ParsedDateString = dateString.Trim().Substring(0, dateString.Trim().Length - 4); // Trim the timezone off the end of the date/time + + return Convert.ToDateTime(ParsedDateString); + } + + private string ParseSubprojectFolder(string subprojectfile) { + subprojectfile = RemoveProject(subprojectfile); + return subprojectfile.Replace(mksProjectPath, ""); + } + + private static string RemoveProject(string projectPath) + { + int lastIndexOfFrontSlash = projectPath.LastIndexOf("/"); + if (lastIndexOfFrontSlash > 0) + { + return projectPath.Substring(0, lastIndexOfFrontSlash + 1); + } + + // If we get here, we didn't have a valid subproject filepath. + return string.Empty; + } + + private static Modification CreateModification(Match match, string subprojectPath) + { Modification modification = new Modification(); - ParseFileAndFolderName(match.Groups["Filename"].Value.Trim(), modification); + ParseFileAndFolderName(subprojectPath + match.Groups["Filename"].Value.Trim(), modification); modification.Version = match.Groups["NewRevision"].Value.Trim(); modification.Type = ParseModificationType(match.Groups["ModificationType"].Value.Trim()); return modification; @@ -55,17 +116,33 @@ private static string ParseModificationType(string modificationType) { - return ("Revision" == modificationType) ? "Modified" : modificationType; + switch (modificationType) + { + case "Revision": + case "Member revision": + return "Modified"; + case "Dropped": + return "Deleted"; + default: + return modificationType; + } } private static void ParseFileAndFolderName(string file, Modification modification) { + Console.WriteLine(string.Format("ParseFileAndFolderName: {0}", file)); int lastIndexOfFrontSlash = file.LastIndexOf("/"); - modification.FileName = file.Substring(lastIndexOfFrontSlash + 1); if (lastIndexOfFrontSlash > 0) { + modification.FileName = file.Substring(lastIndexOfFrontSlash + 1); modification.FolderName = file.Substring(0, lastIndexOfFrontSlash); } + else + { + modification.FileName = file; + modification.FolderName = ""; + } } } } +