/// <summary> /// Computes and displays the diff between two distinct versions. Uses unix "diff" command to actually /// produce the diff. This is relatively slow operation and involves spooling the data into two temp /// files and running an external process. /// </summary> /// <param name="left"> File version on the left. </param> /// <param name="right"> File version on the right. </param> /// <param name="name"> Base name (no path) of the file for display purposes. </param> /// <param name="baseReviewId"> The id of the base review. </param> /// <param name="ignoreWhiteSpaces"> Whether to ignore white spaces. </param> private void DisplayDiff(FileVersion left, FileVersion right, string name, int baseReviewId, bool ignoreWhiteSpaces) { using (var leftFile = SaveToTempFile(left)) using (var rightFile = SaveToTempFile(right)) { if (leftFile == null) return; if (rightFile == null) return; string args = leftFile.FullName + " " + rightFile.FullName; if (ignoreWhiteSpaces && !string.IsNullOrEmpty(DiffArgsIgnoreWhiteSpace)) args = DiffArgsIgnoreWhiteSpace + " " + args; if (!string.IsNullOrEmpty(DiffArgsBase)) args = DiffArgsBase + " " + args; string stderr = null; string result = null; using (Process diff = new Process()) { diff.StartInfo.UseShellExecute = false; diff.StartInfo.RedirectStandardError = true; diff.StartInfo.RedirectStandardOutput = true; diff.StartInfo.CreateNoWindow = true; diff.StartInfo.FileName = DiffExe; diff.StartInfo.Arguments = args; diff.Start(); result = Malevich.Util.CommonUtils.ReadProcessOutput(diff, false, out stderr); } if (!stderr.IsNullOrEmpty()) { ErrorOut("Diff failed."); ErrorOut(stderr); return; } using (StreamCombiner leftStream = new StreamCombiner(new StreamReader(leftFile.FullName))) using (StreamCombiner rightStream = new StreamCombiner(new StreamReader(rightFile.FullName))) using (StreamCombiner rawDiffStream = new StreamCombiner(result)) { ActivePage.Controls.Add(GenerateFileDiffView( leftStream, left.Id, GetComments(left.Id, baseReviewId), ComputeMoniker(name, left), rightStream, right.Id, GetComments(right.Id, baseReviewId), ComputeMoniker(name, right), true, rawDiffStream, name)); } } }
/// <summary> /// Generates the diff view for two file revisions. /// </summary> /// <param name="baseFile"> The base file. </param> /// <param name="baseId"> The database id of the file. </param> /// <param name="baseComments"> The set of comments associated with the base file. </param> /// <param name="baseHeader"> The caption for the base file column. </param> /// <param name="diffFile"> The diff file. </param> /// <param name="diffId"> The database id of the diff. </param> /// <param name="diffComments"> The set of comments associated with the diff file. </param> /// <param name="diffHeader"> The caption for the changed file column. </param> /// <param name="baseIsLeft"> True if the base file is left column. </param> /// <param name="rawDiff"> Stream containing raw diff.exe output. </param> /// <param name="fileName"> The name of the file being compared. </param> private Control GenerateFileDiffView( StreamCombiner baseFile, int baseId, AbstractedComment[] baseComments, string baseHeader, StreamCombiner diffFile, int diffId, AbstractedComment[] diffComments, string diffHeader, bool baseIsLeft, StreamCombiner rawDiff, string fileName) { bool isSingleFileView = baseId == diffId; { // Get user-configurable settings UserContext uc = CurrentUserContext; DiffViewOptions = new FileDiffViewOptions() { IsBaseLeft = baseIsLeft, IsUnified = isSingleFileView ? false : uc.UnifiedDiffView ?? false, OmitUnchangedLines = (Request.QueryString["showAllLines"] ?? "false") != "true", CommentClickMode = uc.CommentClickMode, AutoCollapseComments = uc.AutoCollapseComments ?? true, // default to auto collapse }; } ILineEncoder baseEncoder = GetEncoderForFile(fileName); ILineEncoder diffEncoder = GetEncoderForFile(fileName); Master.FindControl<Panel>("RootDivElement").Style[HtmlTextWriterStyle.Width] = "95%"; #region View table initialization TableGen.Table fileView; if (isSingleFileView) { // Single file view fileView = new TableGen.Table(new string[2] { "Num Base", "Txt Base" }) { ID = "fileview", EnableViewState = false, CssClass = "CssFileView CssFileViewSingle" }; fileView.ColumnGroup.ColumnNameIndexMap = new KeyValuePair<string, int>[4] { new KeyValuePair<string, int>("Num Base", 0), new KeyValuePair<string, int>("Txt Base", 1), new KeyValuePair<string, int>("Num Diff", 0), new KeyValuePair<string, int>("Txt Diff", 1), }; } else if (DiffViewOptions.IsSplit) { // Split file diff fileView = new TableGen.Table(new string[4] { "Num " + (DiffViewOptions.IsBaseLeft ? "Base" : "Diff"), "Txt " + (DiffViewOptions.IsBaseLeft ? "Base" : "Diff"), "Num " + (DiffViewOptions.IsBaseLeft ? "Diff" : "Base"), "Txt " + (DiffViewOptions.IsBaseLeft ? "Diff" : "Base"), }); } else { // Inline file diff fileView = new TableGen.Table(new string[3] { "Num " + (DiffViewOptions.IsBaseLeft ? "Base" : "Diff"), "Num " + (DiffViewOptions.IsBaseLeft ? "Diff" : "Base"), "Txt", }); fileView.ColumnGroup.ColumnNameIndexMap = new KeyValuePair<string, int>[4] { new KeyValuePair<string, int>("Num " + (DiffViewOptions.IsBaseLeft ? "Base" : "Diff"), 0), new KeyValuePair<string, int>("Num " + (DiffViewOptions.IsBaseLeft ? "Diff" : "Base"), 1), new KeyValuePair<string, int>("Txt Base", 2), new KeyValuePair<string, int>("Txt Diff", 2), }; } fileView.AppendCSSClass("CssFileView"); if (!isSingleFileView) fileView.AppendCSSClass(DiffViewOptions.IsSplit ? "CssFileViewSplit" : "CssFileViewUnified"); fileView.EnableViewState = false; fileView.ID = "fileview"; fileView.Attributes["maxLineLen"] = MaxLineLength.ToString(); AddJScriptCreateCommentOnClickAttribute(fileView); // Add the table header var fileViewHeaderGroup = fileView.CreateRowGroup(); fileViewHeaderGroup.IsHeader = true; fileView.Add(fileViewHeaderGroup); var fileViewHeader = new TableGen.Row(isSingleFileView ? 1 : 2); fileViewHeader[0].ColumnSpan = 2; fileViewHeader[DiffViewOptions.IsSplit ? 0 : 1].Add(new HyperLink() { Text = baseHeader, NavigateUrl = Request.FilePath + "?vid=" + baseId, }); if (!isSingleFileView) { fileViewHeader[1].ColumnSpan = 2; fileViewHeader[1].Add(new HyperLink() { Text = diffHeader, NavigateUrl = Request.FilePath + "?vid=" + diffId, }); } fileViewHeader.AppendCSSClass("Header"); fileViewHeaderGroup.AddRow(fileViewHeader); #endregion var baseFileInfo = new DiffFileInfo(baseFile, baseEncoder, baseId, baseComments, BaseOrDiff.Base); var diffFileInfo = new DiffFileInfo(diffFile, diffEncoder, diffId, diffComments, BaseOrDiff.Diff); int curRowNum = 1; int curRowGroupNum = 1; string baseScriptIdPrefix = baseFileInfo.ScriptId; string diffScriptIdPrefix = diffFileInfo.ScriptId; foreach (var diffItem in DiffItem.EnumerateDifferences(rawDiff)) { bool atStart = diffItem.BaseStartLineNumber == 1; bool atEnd = diffItem.BaseLineCount == int.MaxValue; var baseLines = new List<LineAndComments>(); for (int i = 0; i < diffItem.BaseLineCount && baseFileInfo.MoveNextLine(); ++i) baseLines.Add(GetLineAndComments(baseFileInfo)); var diffLines = new List<LineAndComments>(); if (isSingleFileView) { diffLines = baseLines; } else { for (int i = 0; i < diffItem.DiffLineCount && diffFileInfo.MoveNextLine(); ++i) diffLines.Add(GetLineAndComments(diffFileInfo)); } var baseLinesLength = baseLines.Count(); var diffLinesLength = diffLines.Count(); // The end is the only case where the DiffInfo line counts may be incorrect. If there are in fact // zero lines then just continue, which should cause the foreach block to end and we'll continue // like the DiffItem never existed. if (atEnd && diffItem.DiffType == DiffType.Unchanged && baseLinesLength == 0 && diffLinesLength == 0) continue; var curGroup = fileView.CreateRowGroup(); curGroup.AppendCSSClass(diffItem.DiffType.ToString()); curGroup.ID = "rowgroup" + (curRowGroupNum++).ToString(); fileView.AddItem(curGroup); var numPasses = 1; if (DiffViewOptions.IsUnified && diffItem.DiffType != DiffType.Unchanged) numPasses = 2; for (int pass = 1; pass <= numPasses; ++pass) { int lastLineWithComment = 0; int nextLineWithComment = 0; for (int i = 0; i < Math.Max(baseLinesLength, diffLinesLength); ++i) { var row = curGroup.CreateRow(); if (pass == 1) { if (DiffViewOptions.IsUnified && diffItem.DiffType != DiffType.Unchanged) row.AppendCSSClass("Base"); } else if (pass == 2) { Debug.Assert(DiffViewOptions.IsUnified); if (DiffViewOptions.IsUnified && diffItem.DiffType != DiffType.Unchanged) row.AppendCSSClass("Diff"); } if (pass == 1) { // Check if we should omit any lines. if (diffItem.DiffType == DiffType.Unchanged && DiffViewOptions.OmitUnchangedLines) { int contextLineCount = 50; if (baseLinesLength >= ((atStart || atEnd) ? contextLineCount : contextLineCount*2)) { if (i >= nextLineWithComment) { lastLineWithComment = nextLineWithComment; if (isSingleFileView) { nextLineWithComment = baseLines.IndexOfFirst(x => !x.Comments.IsNullOrEmpty(), i); } else { nextLineWithComment = Math.Min( baseLines.IndexOfFirst(x => !x.Comments.IsNullOrEmpty(), i), diffLines.IndexOfFirst(x => !x.Comments.IsNullOrEmpty(), i)); } } if (((atStart && i == 0) || (i - lastLineWithComment == contextLineCount)) && ((atEnd && nextLineWithComment == baseLinesLength) || (nextLineWithComment - i > contextLineCount))) { // Skip a bunch of lines! row = GetUnchangedLinesBreak(fileView.ColumnCount); row.ID = "row" + (curRowNum++).ToString(); curGroup.AddItem(row); i = nextLineWithComment - ((atEnd && nextLineWithComment == baseLinesLength) ? 0 : 50); continue; } } } } if (i < baseLinesLength && pass == 1) { string scriptId = baseScriptIdPrefix + baseLines[i].LineNum.ToString(); row["Num Base"].ID = scriptId + "_linenumber"; row["Num Base"].Text = baseLines[i].LineNum.ToString(); row["Txt Base"].ID = scriptId; row["Txt Base"].Add(EncodeLineTextAndComments(baseFileInfo.Encoder, "base", baseLines[i])); } if (i < diffLinesLength && !isSingleFileView) { string scriptId = diffScriptIdPrefix + diffLines[i].LineNum.ToString(); if (DiffViewOptions.IsSplit || pass == 2 || (pass == 1 && diffItem.DiffType == DiffType.Unchanged)) { row["Num Diff"].ID = scriptId + "_linenumber"; row["Num Diff"].Text = diffLines[i].LineNum.ToString(); } if (DiffViewOptions.IsSplit || pass == 2) { row["Txt Diff"].ID = scriptId; row["Txt Diff"].Add(EncodeLineTextAndComments(diffFileInfo.Encoder, "diff", diffLines[i])); } } row.ID = "row" + (curRowNum++).ToString(); curGroup.AddItem(row); } } } encoderStyles = baseEncoder.GetEncoderCssStream(); baseEncoder.Dispose(); diffEncoder.Dispose(); return fileView; }
/// <summary> /// Generates a sequence of DiffItems representing differences in rawDiffStream. /// </summary> /// <param name="includeUnchangedBlocks"> /// Indicates whether to generate DiffItems for unchanged blocks. /// </param> /// <returns>A DiffItem generator.</returns> public static IEnumerable<DiffItem> EnumerateDifferences( StreamCombiner rawDiffStream, bool includeUnchangedBlocks) { DiffItem prevItem = null; DiffItem item = null; string line = null; do { line = rawDiffStream == null ? null : rawDiffStream.ReadLine(); if (line != null && line.StartsWith("<")) { ++item.BaseLineCount; } else if (line != null && line.StartsWith("-")) { continue; } else if (line != null && line.StartsWith(">")) { ++item.DiffLineCount; } else if (line != null && line.Equals("\\ No newline at end of file")) { // This is a very annoying perforce thing. But we have to account for it. continue; } else { if (item != null) { if (item.DiffLineCount == 0) item.DiffType = DiffType.Deleted; else if (item.BaseLineCount == 0) item.DiffType = DiffType.Added; else item.DiffType = DiffType.Changed; yield return item; prevItem = item; item = null; } if (line != null) { item = new DiffItem(); Match m = DiffDecoder.Match(line); if (!m.Success) yield break; item.BaseStartLineNumber = Int32.Parse(m.Groups[1].Value); // 'a' adds AFTER the line, but we do processing once we get to the line. // So we need to really get to the next line. if (m.Groups[3].Value.Equals("a")) item.BaseStartLineNumber += 1; } if (includeUnchangedBlocks) { var unchangedItem = new DiffItem(); unchangedItem.DiffType = DiffType.Unchanged; unchangedItem.BaseStartLineNumber = prevItem == null ? 1 : prevItem.BaseStartLineNumber + prevItem.BaseLineCount; unchangedItem.BaseLineCount = item == null ? int.MaxValue : item.BaseStartLineNumber - unchangedItem.BaseStartLineNumber; unchangedItem.DiffLineCount = unchangedItem.BaseLineCount; if (unchangedItem.BaseLineCount != 0) yield return unchangedItem; } } } while (line != null); }
/// <summary> /// Creates a DiffFileInfo for a file being compared. /// </summary> /// <param name="file">The file stream.</param> /// <param name="encoder">The line encoder.</param> /// <param name="id">The file ID within the database.</param> /// <param name="comments">The array of comments for the file.</param> /// <param name="baseOrDiff">What role the file plays within the comparison.</param> public DiffFileInfo( StreamCombiner file, ILineEncoder encoder, int id, AbstractedComment[] comments, BaseOrDiff baseOrDiff) { File = file; Encoder = encoder; Id = id; ScriptId = baseOrDiff.ToString().ToLowerCultureInvariant() + "_" + Id.ToString() + "_"; Comments = comments; //CurLineNum = 0; //NextCommentIndex = 0; BaseOrDiff = baseOrDiff; }
/// <summary> /// Generates a sequence of DiffItems representing differences in rawDiffStream. Includes unchanged blocks. /// </summary> /// <returns>A DiffItem generator.</returns> public static IEnumerable<DiffItem> EnumerateDifferences(StreamCombiner rawDiffStream) { return EnumerateDifferences(rawDiffStream, true); }