/// <summary> /// Gets the change from the repository /// Returns null if any error occurs, or the change is not pending. /// </summary> /// <param name="changeId"> shelveset identifier. </param> /// <param name="includeBranchedFiles"> Include full text for branched and integrated files. </param> /// <returns> The change. </returns> public Change GetChange(string changeId, bool includeBranchedFiles) { if (!File.Exists(diffFromSvnBatFilename)) { Console.WriteLine("difffromsvn.bat does not exist in the same directory as review.exe."); Console.WriteLine(" This batch file must run 'diff.exe %6 %7'"); return null; } // first get the list of all files (including paths): // svn status --changelist xxx string result = RunClient(@"status --changelist """ + changeId + "\" \"" + localRoot + "\" ", false); StringReader sr = new StringReader(result); string line = null; bool seenDash = false; Regex fileAfterSpaceRegEx = new Regex(@"^(?<changetype>.).......(?<filename>(\S)+)$"); List<ChangeFile> files = new List<ChangeFile>(); // Results look like this: //--- Changelist 'mine': // util\VisitorPrefsGetter.java //M util\VisitorPrefsEidMigrator.java // Rules: // skip past first line that begins with "-" // then for each line, get the filename by finding the first non-whitespace after a white-space while ((line = sr.ReadLine()) != null) { if (!seenDash) { if ((line.Length > 0) && (line[0] == '-')) { seenDash = true; } continue; } Match match = fileAfterSpaceRegEx.Match(line); if (!match.Success) { Console.WriteLine("Could not interpret svn status output: " + line); return null; } // what type of change is it? string typeOfChange = match.Groups[2].Value; if (typeOfChange.Length == 0) { // no change, skip this continue; } ChangeFile.SourceControlAction action = ChangeFile.SourceControlAction.EDIT; switch (typeOfChange) { case "C": // conflicted case "M": // modified case "R": // replaced case " ": // no change but added to the changelist action = ChangeFile.SourceControlAction.EDIT; break; case "D": // deleted case "!": action = ChangeFile.SourceControlAction.DELETE; break; case "I": // ignored case "A": // added action = ChangeFile.SourceControlAction.ADD; break; case "?": // not under version control break; default: // unexpected Console.WriteLine("Unexpected change type: " + typeOfChange); return null; } // this is the filename string filename = match.Groups[3].Value; bool isText = false; // get the mime-type to see if it's text string mimetype = RunClient("propget svn:mime-type " + "\"" + filename + "\"", false); if (mimetype == null || mimetype.StartsWith("text") || mimetype.Length == 0) { isText = true; } // get the current revision string serverFilename = null; int revisionId = GetRevisionFromFile(filename, ref serverFilename); ChangeFile cf = new ChangeFile(serverFilename, action, revisionId, isText); cf.LocalFileName = filename; files.Add(cf); } if (!seenDash) return null; if (files.Count > 0) { // TODO: figure out name of client and description Change change = new Change(SourceControl, Client, changeId, DateTime.Now.ToUniversalTime(), changeId, files.ToArray()); FillInFileData(change); return change; } return null; }
/// <summary> /// Iterates through every file in the change list, and: /// 1. fills in its local path and /// 2. diff if it is an edit, or the add file itself if it is an add. /// </summary> /// <param name="change"> The change list, instantiated. </param> private void FillInFileData(Change change) { foreach (ChangeFile file in change.Files) { if (File.Exists(file.LocalFileName)) file.LastModifiedTime = File.GetLastWriteTimeUtc(file.LocalFileName); if (!file.IsText) continue; if (file.Action == ChangeFile.SourceControlAction.EDIT || (file.Action == ChangeFile.SourceControlAction.INTEGRATE)) { // The following depends on a diff command that will diff %6 and %7. // Contents of the file is one line: // @diff %6 %7 // This is something that the svn developers recommend (they choose not to fix this) string result = RunClient("diff --diff-cmd \"" + diffFromSvnBatFilename + "\" \"" + file.LocalFileName + "\"", true); // skip past first line StringReader sr = new StringReader(result); StringBuilder sb = new StringBuilder(); for (; ; ) { string line = sr.ReadLine(); if (line == null) break; if (line.Equals("")) continue; if (line.StartsWith("=")) continue; sb.Append(line); sb.Append('\n'); } file.Data = sb.ToString(); sr.Close(); } else if (file.Action == ChangeFile.SourceControlAction.ADD || (file.Action == ChangeFile.SourceControlAction.BRANCH)) { try { StreamReader reader = new StreamReader(file.LocalFileName); file.Data = reader.ReadToEnd(); reader.Close(); } catch (FileNotFoundException) { Console.WriteLine("File not found: " + file.LocalFileName); throw new SourceControlRuntimeError(); } } } }
/// <summary> /// For every file in the change list, fill in its local path and either diff if it is an edit, or the /// file itself if it is an add. /// </summary> /// <param name="change"> The change list, instantiated. </param> /// <param name="includeBranchedFiles"> Include the text for branched and integrated files. </param> private void FillInFileData(Change change, bool includeBranchedFiles) { foreach (ChangeFile file in change.Files) { string where = RunClient("where \"" + file.ServerFileName + "\"", false); if (where == null) BugOut("sd|p4 where " + file.ServerFileName); Match files = ClientWhereParser.Match(where); if (!files.Success) BugOut(where); file.LocalFileName = files.Groups[3].Value; if (File.Exists(file.LocalFileName)) file.LastModifiedTime = File.GetLastWriteTimeUtc(file.LocalFileName); if (!file.IsText) continue; if (file.Action == ChangeFile.SourceControlAction.EDIT || (file.Action == ChangeFile.SourceControlAction.INTEGRATE && includeBranchedFiles)) { file.Data = RunClient("diff \"" + file.ServerFileName + "\"", true); } else if (file.Action == ChangeFile.SourceControlAction.ADD || (file.Action == ChangeFile.SourceControlAction.BRANCH && includeBranchedFiles)) { try { StreamReader reader = new StreamReader(file.LocalFileName); file.Data = reader.ReadToEnd(); reader.Close(); } catch (FileNotFoundException) { Console.WriteLine("File not found: " + file.LocalFileName); throw new SourceControlRuntimeError(); } } } }
/// <summary> /// Gets the change from the source control system. The change must be pending. /// Returns null if any error occurs, or the change is not pending. /// </summary> /// <param name="changeNo"> CL identifier. </param> /// <param name="changeListId"> Incude the text of branched and integrated files. </param> /// <returns> The change. </returns> public Change GetChange(string changeListId, bool includeBranchedFiles) { int changeNo; if (!Int32.TryParse(changeListId, out changeNo)) { Console.WriteLine("Change List number is not a number!"); return null; } string description = RunClient("describe " + changeNo, false); if (description == null) return null; StringReader reader = new StringReader(description); string firstLine = reader.ReadLine(); if (firstLine == null) { Console.WriteLine("The description is empty. Cannot proceed."); return null; } Match firstLineMatch = ClientDescribeTitleParser.Match(firstLine); if (!firstLineMatch.Success) { Console.WriteLine("This change is not pending!"); return null; } string clientName = firstLineMatch.Groups[1].Value; DateTime timeStamp; if (!DateTime.TryParse(firstLineMatch.Groups[2].Value, out timeStamp)) BugOut(description + "\n\n[Could not parse the time stamp]"); timeStamp = timeStamp.ToUniversalTime(); if (!"".Equals(reader.ReadLine())) BugOut(description + "\n\n[No newline before change description]"); StringBuilder changeDescription = new StringBuilder(); for (; ; ) { string line = reader.ReadLine(); if (line == null) BugOut(description + "\n\n[Unexpected EOL]"); if (line.Equals("Affected files ...")) break; changeDescription.Append(line.Trim()); changeDescription.Append('\n'); } if (!"".Equals(reader.ReadLine())) BugOut(description + "\n\n[No newline before the list of files]"); List<ChangeFile> files = new List<ChangeFile>(); for (; ; ) { string fileString = reader.ReadLine(); if (fileString == null || "".Equals(fileString)) break; Match match = ClientDescribeFileParser.Match(fileString); if (!match.Success) BugOut(description + "\n\n[Could not match " + fileString + "]"); string fileName = match.Groups[1].Value; string rev = match.Groups[2].Value; string action = match.Groups[3].Value; int revision = -1; if (!Int32.TryParse(rev, out revision)) BugOut(description + "\n\n[Could not parse revision in " + fileString + "]"); // Make sure the file is text. string openedFile = RunClient("opened \"" + fileName + "\"", false); if (openedFile == null) BugOut("opened \"" + fileName + "\""); // Ignore binary files bool isText = ClientOpenedIsText.IsMatch(openedFile); ChangeFile.SourceControlAction a = ChangeFile.SourceControlAction.ADD; if ("edit".Equals(action)) a = ChangeFile.SourceControlAction.EDIT; else if ("add".Equals(action)) a = ChangeFile.SourceControlAction.ADD; else if ("delete".Equals(action)) a = ChangeFile.SourceControlAction.DELETE; else if ("branch".Equals(action)) a = ChangeFile.SourceControlAction.BRANCH; else if ("integrate".Equals(action)) a = ChangeFile.SourceControlAction.INTEGRATE; else BugOut(description + "\n\n[Unknown file action: " + action + "]"); ChangeFile file = new ChangeFile(fileName, a, revision, isText); files.Add(file); } if (files.Count > 0) { Change change = new Change(SourceControl, clientName, changeListId, timeStamp, changeDescription.ToString(), files.ToArray()); FillInFileData(change, includeBranchedFiles); return change; } return null; }
/// <summary> /// Ensures that the diffs in files can in fact be parsed by Malevich. If non-graphical characters or /// incorrect (mixed: Unix + Windows, or Windows + Mac) line endings are present, this throws "sd diff" /// off and it produces the results that we will not be able to read. This checks that this does not occur. /// </summary> /// <param name="change"> Change List. </param> /// <returns> True if the differ is intact. </returns> private static bool VerifyDiffIntegrity(Change change) { Regex diffDecoder = new Regex(@"^([0-9]+)(,[0-9]+)?([a,d,c]).*$"); bool result = true; foreach (SourceControl.ChangeFile file in change.Files) { if (file.Data == null || (file.Action != SourceControl.ChangeFile.SourceControlAction.EDIT && file.Action != SourceControl.ChangeFile.SourceControlAction.INTEGRATE)) continue; StringReader reader = new StringReader(file.Data); for (; ; ) { string line = reader.ReadLine(); if (line == null) break; if (line.StartsWith("> ") || line.StartsWith("< ") || line.Equals("---") || line.Equals("\\ No newline at end of file") || diffDecoder.IsMatch(line)) continue; Console.WriteLine("Cannot parse the difference report for {0}.", file.LocalOrServerFileName); Console.WriteLine("{0}", file.Data); Console.WriteLine(); result = false; break; } } if (!result) { Console.WriteLine(); Console.WriteLine("Found problems processing the file differences in the change list."); Console.WriteLine(); Console.WriteLine("This is typically caused by incorrect or mixed end of line markers, or other"); Console.WriteLine("non-graphical characters that your source control system could not process."); Console.WriteLine(); Console.WriteLine("Please fix the files in question and resubmit the change."); } return result; }
/// <summary> /// For every file in the change list, fill in its local path and either diff if it is an edit, or the /// file itself if it is an add. /// </summary> /// <param name="change"> The change list, instantiated. </param> /// <param name="includeBranchedFiles"> Include the text for branched and integrated files. </param> private void FillInFileData(Change change, bool includeBranchedFiles) { foreach (ChangeFile file in change.Files) { string where = RunClient("where \"" + file.ServerFileName + "\"", false); if (string.IsNullOrEmpty(where)) BugOut("sd|p4 where " + file.ServerFileName); Match files = ClientWhereParser.Match(where); if (!files.Success) BugOut(where); file.LocalFileName = files.Groups[3].Value; if (File.Exists(file.LocalFileName)) file.LastModifiedTime = File.GetLastWriteTimeUtc(file.LocalFileName); if (!file.IsText) continue; if (file.Action == ChangeFile.SourceControlAction.EDIT || (file.Action == ChangeFile.SourceControlAction.INTEGRATE && includeBranchedFiles)) { if (!file.IsShelved) file.Data = RunClient("diff \"" + file.ServerFileName + "\"", true); else { // Diff shelved file with diff2 // diff2 file#previousRevision file@=CL file.Data = RunClient(string.Format("diff2 \"{0}#{1}\" \"{0}@={2}\"", file.ServerFileName, file.Revision, change.ChangeListId), true); } } else if (file.Action == ChangeFile.SourceControlAction.ADD || (file.Action == ChangeFile.SourceControlAction.BRANCH && includeBranchedFiles)) { try { if (!file.IsShelved) { StreamReader reader = new StreamReader(file.LocalFileName); file.Data = reader.ReadToEnd(); reader.Close(); } else { file.Data = RunClient(string.Format("print -q \"{0}@={1}\"", file.ServerFileName, change.ChangeListId), false); } } catch (FileNotFoundException) { Log.Error("File not found: " + file.LocalFileName); throw new SourceControlRuntimeError("File not found: " + file.LocalFileName); } } } }
/// <summary> /// Gets the change from the source control system. The change must be pending. /// Returns null if any error occurs, or the change is not pending. /// </summary> /// <param name="changeNo"> CL identifier. </param> /// <param name="changeListId"> Incude the text of branched and integrated files. </param> /// <param name="skipFiles"> True to skip files when not needed - faster. </param> /// <returns> The change. </returns> public Change GetChange(string changeListId, bool includeBranchedFiles, bool skipFile = false) { bool shelved = false; int changeNo; if (!Int32.TryParse(changeListId, out changeNo)) { Log.Error("Change List number is not a number!"); return null; } // Use -S for shelved changelists string description = RunClient("describe -s -S " + changeNo, false); if (string.IsNullOrEmpty(description)) return null; StringReader reader = new StringReader(description); string firstLine = reader.ReadLine(); if (firstLine == null) { Log.Error("The description is empty. Cannot proceed."); return null; } Match firstLineMatch = ClientDescribeTitleParser.Match(firstLine); if (!firstLineMatch.Success) { Log.Error("This change is not pending!"); return null; } string clientName = firstLineMatch.Groups[1].Value; DateTime timeStamp; if (!DateTime.TryParse(firstLineMatch.Groups[2].Value, out timeStamp)) BugOut(description + "\n\n[Could not parse the time stamp]"); timeStamp = timeStamp.ToUniversalTime(); if (!"".Equals(reader.ReadLine())) BugOut(description + "\n\n[No newline before change description]"); StringBuilder changeDescription = new StringBuilder(); for (; ; ) { string line = reader.ReadLine(); if (line == null) BugOut(description + "\n\n[Unexpected EOL]"); if (line.Equals("Affected files ...")) break; if (line.Equals("Shelved files ...")) { shelved = true; break; } changeDescription.Append(line.Trim()); changeDescription.Append('\n'); } if (!"".Equals(reader.ReadLine())) BugOut(description + "\n\n[No newline before the list of files]"); List<ChangeFile> files = new List<ChangeFile>(); if (!skipFile) { for (; ; ) { string fileString = reader.ReadLine(); if (fileString == null || "".Equals(fileString)) break; Match match = ClientDescribeFileParser.Match(fileString); if (!match.Success) BugOut(description + "\n\n[Could not match " + fileString + "]"); string fileName = match.Groups[1].Value; string rev = match.Groups[2].Value; string action = match.Groups[3].Value; int revision = -1; if (rev != "none" && !Int32.TryParse(rev, out revision)) BugOut(description + "\n\n[Could not parse revision in " + fileString + "]"); // Make sure the file is text. bool isText = false; if (!shelved) { var openedFile = RunClient("opened \"" + fileName + "\"", false); if (string.IsNullOrEmpty(openedFile) && !shelved) BugOut("opened \"" + fileName + "\""); // Ignore binary files isText = ClientOpenedIsText.IsMatch(openedFile); } else { var stats = RunClient(string.Format("fstat \"{0}@={1}\"", fileName, changeListId), false); if (string.IsNullOrEmpty(stats) && !shelved) BugOut("opened \"" + fileName + "\""); // Ignore binary files isText = stats.Contains("... headType text") ? true : false; } ChangeFile.SourceControlAction a = ChangeFile.SourceControlAction.ADD; if ("edit".Equals(action)) a = ChangeFile.SourceControlAction.EDIT; else if ("add".Equals(action) || "move/add".Equals(action)) a = ChangeFile.SourceControlAction.ADD; else if ("delete".Equals(action) || "move/delete".Equals(action)) a = ChangeFile.SourceControlAction.DELETE; else if ("branch".Equals(action)) a = ChangeFile.SourceControlAction.BRANCH; else if ("integrate".Equals(action)) a = ChangeFile.SourceControlAction.INTEGRATE; else if ("move/add".Equals(action)) a = ChangeFile.SourceControlAction.RENAME; else BugOut(description + "\n\n[Unknown file action: " + action + "]"); ChangeFile file = new ChangeFile(fileName, a, revision, isText, shelved); files.Add(file); } } if (files.Count > 0 || skipFile) { Change change = new Change(SourceControl, clientName, changeListId, timeStamp, description, files.ToArray()); FillInFileData(change, includeBranchedFiles); return change; } return null; }