Files
UnrealEngineUWP/Engine/Source/Programs/AutomationTool/AutomationUtils/P4Blame.cs
Peter Sauerbrei dc1d815f84 refactored the logging system for UAT/UBT to be more like UE4
we now use an enum similar to UE4 with Fatal, Error, Warning, Display, Log, Verbose, and VeryVerbose
Log will only go to the log file unless -verbose is passed on the command line
reduced some of the output from UAT to be Log only

[CL 2631062 by Peter Sauerbrei in Main branch]
2015-07-23 14:51:46 -04:00

482 lines
16 KiB
C#

// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.IO;
using System.ComponentModel;
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace AutomationTool
{
public partial class P4Connection
{
/// <summary>
/// Information about file revision.
/// </summary>
public class RevisionInfo
{
public int Revision;
public int Changelist;
public string User;
public P4Action Action;
public DateTime Date;
protected RevisionInfo()
{
}
public override string ToString()
{
return String.Format("#{0} {1} change {2} by {3} on {4}", Revision, Action, Changelist, User, Date);
}
/// <summary>
/// Create a new RevisionInfo instance from Filelog command output
/// </summary>
/// <param name="Line">Single line from Filelog command output</param>
/// <returns></returns>
public static RevisionInfo FromFilelogOutput(string Line)
{
var Info = new RevisionInfo();
var ChangelistString = GetValueFromFileLog(Line, " change ");
Info.Revision = Int32.Parse(GetValueFromFileLog(Line, "... #"));
Info.Changelist = Int32.Parse(ChangelistString);
Info.User = GetValueFromFileLog(Line, " by ");
Info.Action = ParseAction(GetValueFromFileLog(Line, String.Format("{0} ", ChangelistString)));
Info.Date = DateTime.Parse(GetValueFromFileLog(Line, " on "));
return Info;
}
/// <summary>
/// Given a prefix, gets a value following the prefix from filelog output.
/// </summary>
/// <param name="LogLine">Filelog output line</param>
/// <param name="Key">Prefix</param>
/// <returns>Value</returns>
private static string GetValueFromFileLog(string LogLine, string Key)
{
int ValueStartIndex = LogLine.IndexOf(Key) + Key.Length;
int ValueEndIndex = LogLine.IndexOf(" ", ValueStartIndex + 1);
return LogLine.Substring(ValueStartIndex, ValueEndIndex - ValueStartIndex);
}
}
/// <summary>
/// Class representing a single line in Blame method result.
/// </summary>
public class BlameLineInfo
{
public List<BlameLineInfo> PreviousRevisions;
public RevisionInfo Revision;
public string Line;
public BlameLineInfo(string InLine, RevisionInfo InRevision)
{
PreviousRevisions = new List<BlameLineInfo>();
Revision = InRevision;
Line = InLine;
}
public override string ToString()
{
return Line;
}
public RevisionInfo FirstRevision
{
get { return CommandUtils.IsNullOrEmpty(PreviousRevisions) ? Revision : PreviousRevisions[0].Revision; }
}
}
/// <summary>
/// Diff change type
/// </summary>
public enum DiffChangeType
{
Add = 0,
Change = 1,
Delete
}
/// <summary>
/// Class representing a single change in Diff result.
/// </summary>
public class DiffChange
{
public int SourceStart;
public int SourceEnd;
public int DestStart;
public int DestEnd;
public DiffChangeType Type;
public List<string> OldContents = new List<string>();
public List<string> NewContents = new List<string>();
public DiffChange(string[] Diff, int DiffIndex)
{
// Determine what kind of change this is (add/change/delete)
var Change = Diff[DiffIndex];
var ChangeTypes = new string[] { "a", "c", "d" };
var ChangeTypeIndex = -1;
for (int ChangeIndex = 0; ChangeIndex < ChangeTypes.Length; ++ChangeIndex)
{
ChangeTypeIndex = Change.IndexOf(ChangeTypes[ChangeIndex]);
if (ChangeTypeIndex != -1)
{
Type = (DiffChangeType)ChangeIndex;
break;
}
}
// Get range of changes
var SourceRange = Change.Substring(0, ChangeTypeIndex);
ParseRange(SourceRange, out SourceStart, out SourceEnd);
var DestRange = Change.Substring(ChangeTypeIndex + 1);
ParseRange(DestRange, out DestStart, out DestEnd);
// Get the actual change contents
for (++DiffIndex; DiffIndex < Diff.Length; ++DiffIndex)
{
var ContentsLine = Diff[DiffIndex];
if (ContentsLine.StartsWith("<"))
{
OldContents.Add(ContentsLine.Substring(2));
}
else if (ContentsLine.StartsWith(">"))
{
NewContents.Add(ContentsLine.Substring(2));
}
else if (!ContentsLine.StartsWith("---"))
{
break;
}
}
}
/// <summary>
/// Parses a text to get a range of changes.
/// </summary>
/// <param name="RangeText"></param>
/// <param name="Start"></param>
/// <param name="End"></param>
private void ParseRange(string RangeText, out int Start, out int End)
{
var Range = RangeText.Split(',');
Start = Int32.Parse(Range[0]) - 1;
if (Range.Length > 1)
{
End = Int32.Parse(Range[1]) - 1;
}
else
{
End = Start;
}
}
public override string ToString()
{
return String.Format("{0}: {1},{2}->{3},{4}", Type, SourceStart, SourceEnd, DestStart, DestEnd);
}
/// <summary>
/// Number of lines that have changed in the original file
/// </summary>
public int SourceCount
{
get { return SourceEnd - SourceStart + 1; }
}
/// <summary>
/// Number of lines that have changed in the resulting file
/// </summary>
public int DestCount
{
get { return DestEnd - DestStart + 1; }
}
}
/// <summary>
/// Goes through all changes from Diff result and applies them to the blame file.
/// </summary>
/// <param name="Source"></param>
/// <param name="Diff"></param>
/// <param name="RevisionInfo"></param>
private static void ParseDiff(List<BlameLineInfo> Source, DiffChange[] Diff, RevisionInfo RevisionInfo)
{
int DestOffset = 0;
foreach (var Change in Diff)
{
DestOffset = ApplyDiff(RevisionInfo, Source, Change, DestOffset);
}
}
/// <summary>
/// Applies a single Diff change to the Blame file.
/// </summary>
/// <param name="RevisionInfo"></param>
/// <param name="Result"></param>
/// <param name="ChangeInfo"></param>
/// <param name="DestOffset"></param>
/// <returns></returns>
private static int ApplyDiff(RevisionInfo RevisionInfo, List<BlameLineInfo> Result, DiffChange ChangeInfo, int DestOffset)
{
switch (ChangeInfo.Type)
{
case DiffChangeType.Add:
DestOffset = ApplyDiffAdd(RevisionInfo, Result, ChangeInfo, DestOffset);
break;
case DiffChangeType.Delete:
DestOffset = ApplyDiffDelete(Result, ChangeInfo, DestOffset);
break;
case DiffChangeType.Change:
DestOffset = ApplyDiffChange(RevisionInfo, Result, ChangeInfo, DestOffset);
break;
}
return DestOffset;
}
/// <summary>
/// Applies 'change' to the blame file.
/// </summary>
private static int ApplyDiffChange(RevisionInfo RevisionInfo, List<BlameLineInfo> Result, DiffChange ChangeInfo, int DestOffset)
{
// Remember the first revision for lines that are about to change
var PreviousRevisions = new BlameLineInfo[ChangeInfo.SourceCount];
for (int PrevIndex = 0; PrevIndex < ChangeInfo.SourceCount; ++PrevIndex)
{
PreviousRevisions[PrevIndex] = Result[PrevIndex + ChangeInfo.SourceStart + DestOffset];
}
// Apply Change as Delete+Add operation
DestOffset = ApplyDiffDelete(Result, ChangeInfo, DestOffset);
DestOffset = ApplyDiffAdd(RevisionInfo, Result, ChangeInfo, DestOffset);
// Keep previous revisions
for (int PrevIndex = 0; PrevIndex < PreviousRevisions.Length && PrevIndex < ChangeInfo.NewContents.Count; ++PrevIndex)
{
Result[PrevIndex + ChangeInfo.DestStart].PreviousRevisions.Add(PreviousRevisions[PrevIndex]);
}
return DestOffset;
}
/// <summary>
/// Applies 'delete' to the blame file
/// </summary>
private static int ApplyDiffDelete(List<BlameLineInfo> Result, DiffChange ChangeInfo, int DestOffset)
{
Result.RemoveRange(ChangeInfo.SourceStart + DestOffset, ChangeInfo.SourceCount);
DestOffset -= ChangeInfo.SourceCount;
return DestOffset;
}
/// <summary>
/// Applies 'add' to the diff file.
/// </summary>
private static int ApplyDiffAdd(RevisionInfo RevisionInfo, List<BlameLineInfo> Result, DiffChange ChangeInfo, int DestOffset)
{
int DestInsert = ChangeInfo.DestStart;
for (int ContentIndex = 0; ContentIndex < ChangeInfo.NewContents.Count; ++ContentIndex, ++DestInsert, ++DestOffset)
{
var AddBlame = new BlameLineInfo(ChangeInfo.NewContents[ContentIndex], RevisionInfo);
Result.Insert(DestInsert, AddBlame);
}
return DestOffset;
}
/// <summary>
/// Converts Print command output to a blame file.
/// </summary>
/// <param name="Output">Print command output</param>
/// <param name="RevisionInfo">Revision of the printed file.</param>
/// <returns>List of lines in the blame file.</returns>
private static List<BlameLineInfo> PrintToBlame(string[] Lines, RevisionInfo RevisionInfo)
{
var InitialBlame = new List<BlameLineInfo>();
for (int LineIndex = 0; LineIndex < Lines.Length; ++LineIndex)
{
var LineInfo = new BlameLineInfo(Lines[LineIndex], RevisionInfo);
InitialBlame.Add(LineInfo);
}
return InitialBlame;
}
/// <summary>
/// Returns a list of revisions for the given file. Does not include rename/move changes.
/// </summary>
/// <param name="DepotFilename"></param>
/// <returns></returns>
public RevisionInfo[] Filelog(string DepotFilename, bool bQuiet = false)
{
if (!bQuiet)
{
CommandUtils.LogConsole("Filelog({0})", DepotFilename);
}
var Result = new List<RevisionInfo>();
var FilelogResult = P4("filelog " + DepotFilename, AllowSpew: false);
var LogLines = FilelogResult.Output.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
foreach (var Line in LogLines)
{
if (Line.StartsWith("... #"))
{
var RevInfo = RevisionInfo.FromFilelogOutput(Line);
Result.Add(RevInfo);
if (RevInfo.Revision == 1)
{
// There may be other revisions but they are move/rename changes.
break;
}
}
}
// Sort ascending.
Result.Sort((A, B) => { return A.Revision - B.Revision; });
return Result.ToArray();
}
/// <summary>
/// Prints the contents of a file to an array of lines.
/// </summary>
/// <param name="CommandLine"></param>
/// <param name="bQuiet"></param>
/// <returns></returns>
public string[] P4Print(string CommandLine, bool bQuiet = false)
{
if (!bQuiet)
{
CommandUtils.LogConsole("P4Print({0})", CommandLine);
}
var PrintResult = P4("print " + CommandLine, AllowSpew: false);
var OutputLines = new List<string>(PrintResult.Output.Split(new string[] { Environment.NewLine }, StringSplitOptions.None));
// The first line is just the filename
OutputLines.RemoveAt(0);
return OutputLines.ToArray();
}
/// <summary>
/// Prints the contents of a file to an array of lines.
/// </summary>
/// <param name="CommandLine"></param>
/// <param name="bQuiet"></param>
/// <returns></returns>
public string[] Annotate(string CommandLine, bool bQuiet = false)
{
if (!bQuiet)
{
CommandUtils.LogConsole("Annotate({0})", CommandLine);
}
var PrintResult = P4("annotate " + CommandLine, AllowSpew: false);
var OutputLines = new List<string>(PrintResult.Output.Split(new string[] { Environment.NewLine }, StringSplitOptions.None));
// The first line is just the filename
OutputLines.RemoveAt(0);
return OutputLines.ToArray();
}
/// <summary>
/// Performs a diff on a file at two revisions.
/// </summary>
/// <param name="DepotFilename"></param>
/// <param name="RevA"></param>
/// <param name="RevB"></param>
/// <returns>List of changes between revisions.</returns>
public DiffChange[] Diff2(string DepotFilename, int RevA, int RevB, bool bQuiet = false)
{
if (!bQuiet)
{
CommandUtils.LogConsole("Diff2({0}, {1}, {2})", DepotFilename, RevA, RevB);
}
var Changes = new List<DiffChange>();
var Diff2Result = P4(String.Format("diff2 {0}#{1} {0}#{2}", DepotFilename, RevA, RevB), AllowSpew: false);
var Diff2Output = Diff2Result.Output.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
// Skip the first line - it contains only the diff description
for (int DiffLineIndex = 1; DiffLineIndex < Diff2Output.Length; ++DiffLineIndex)
{
var DiffLine = Diff2Output[DiffLineIndex];
if (!String.IsNullOrEmpty(DiffLine) && Char.IsNumber(DiffLine[0]))
{
var NewChange = new DiffChange(Diff2Output, DiffLineIndex);
Changes.Add(NewChange);
}
}
return Changes.ToArray();
}
/// <summary>
/// Generates a Blame file for the specified depot file up to the specified revision (default = all revisions). Each line contains a complete revision history.
/// </summary>
/// <param name="DepotFilename"></param>
/// <param name="MaxRevision"></param>
/// <returns>An array of lines with blame information.</returns>
public BlameLineInfo[] Timelapse(string DepotFilename, int MaxRevision = Int32.MaxValue)
{
CommandUtils.LogConsole("Timelapse({0}#{1})", DepotFilename, (MaxRevision == Int32.MaxValue) ? "head" : MaxRevision.ToString());
// Get the file history.
var FileHistory = Filelog(DepotFilename, bQuiet: true);
if (!CommandUtils.IsNullOrEmpty(FileHistory))
{
// Construct the initial blame file from the first revision
var PrintResult = P4Print(String.Format("{0}#{1}", DepotFilename, FileHistory[0].Revision), bQuiet: true);
var Blame = PrintToBlame(PrintResult, FileHistory[0]);
var CurrentRevision = FileHistory[0].Revision;
// Apply all diff changes up to the specified revision.
for (int RevisionIndex = 1; RevisionIndex < FileHistory.Length && CurrentRevision <= MaxRevision; ++RevisionIndex)
{
var RevisionInfo = FileHistory[RevisionIndex];
switch (RevisionInfo.Action)
{
case P4Action.Delete:
case P4Action.MoveDelete:
Blame.Clear();
break;
case P4Action.Branch:
case P4Action.Add:
case P4Action.MoveAdd:
Blame = PrintToBlame(P4Print(String.Format("{0}#{1}", DepotFilename, RevisionInfo.Revision), bQuiet: true), RevisionInfo);
break;
default:
{
int PreviousRev = FileHistory[RevisionIndex - 1].Revision;
CurrentRevision = RevisionInfo.Revision;
var Diff2Output = Diff2(DepotFilename, PreviousRev, CurrentRevision, bQuiet: true);
ParseDiff(Blame, Diff2Output, FileHistory[RevisionIndex]);
}
break;
}
}
return Blame.ToArray();
}
else
{
return new BlameLineInfo[0];
}
}
/// <summary>
/// Generates a Blame file for the specified depot file up to the specified revision (default = all revisions). Only the last revision is available for each line.
/// </summary>
/// <param name="DepotFilename"></param>
/// <param name="MaxRevision"></param>
/// <returns>An array of lines with blame information.</returns>
public BlameLineInfo[] Blame(string DepotFilename)
{
CommandUtils.LogConsole("Blame({0})", DepotFilename);
// Get the file history.
var FileHistory = Filelog(DepotFilename, bQuiet: true);
// Construct the initial blame file from the first revision
var AnnotateResult = Annotate(DepotFilename, bQuiet: true);
var Blame = new List<BlameLineInfo>(AnnotateResult.Length);
// Get the revision at each line and put it in the resulting Blame file
for (int LineIndex = 0; LineIndex < AnnotateResult.Length; ++LineIndex)
{
var AnnotatedLine = AnnotateResult[LineIndex];
// The last line is empty, we could maybe just not iterate it but I'm not sure it's 100% of the time.
if (!String.IsNullOrEmpty(AnnotatedLine))
{
var RevisionEndIndex = AnnotatedLine.IndexOf(':');
var RevisionIndex = Int32.Parse(AnnotatedLine.Substring(0, RevisionEndIndex)) - 1;
var LineContentsStart = RevisionEndIndex + 2;
var LineContents = LineContentsStart >= AnnotatedLine.Length ? String.Empty : AnnotatedLine.Substring(LineContentsStart);
var BlameInfo = new BlameLineInfo(LineContents, FileHistory[RevisionIndex]);
Blame.Add(BlameInfo);
}
}
return Blame.ToArray();
}
}
}