/* * Copyright 1998-2014 Epic Games, Inc. All Rights Reserved. */ using System; using System.IO; using System.Collections.Generic; using System.Xml.Serialization; namespace P4ChangeParser { /// /// Takes the XML data generated from the parser and creates nicely formatted reports /// class OutputFormatter { /// /// Settings for generating a report from a set of changes /// public class ReportSettings { /// Format of this report public P4ParsedChangelist.EExportFormats Format; /// Include TTP numbers at the end of the one-line summary? (plain text) public bool bAppendTTPNumbersToOneLineSummary; /// Include P4 changelist numbers at the end of the one-line summary? (plain text) public bool bAppendChangelistNumbersToOneLineSummary; /// String to use when indenting text public string IndentText; /// String to use for bullets (or empty for no bullets) public string BulletText; /// Name of category to automatically create for changes that weren't supplied a category /// If this is empty, uncategorized changes will be omitted public string UncategorizedCategoryName; /// True if one-line summaries of changes should be prefixed with the type of change public bool bPrefixChangesWithType; /// /// Constructor /// public ReportSettings() { Format = P4ParsedChangelist.EExportFormats.EF_PlainText; bAppendTTPNumbersToOneLineSummary = true; bAppendChangelistNumbersToOneLineSummary = true; IndentText = " "; BulletText = "* "; UncategorizedCategoryName = "Uncategorized"; bPrefixChangesWithType = true; } } /// /// Types of changes /// public enum EChangeType { New, Change, Fix, Unknown, } /// /// Describes a single change /// class ChangeDesc { /// Type of change public EChangeType Type; /// One-line description of this change public String BriefDescription; /// Extras description text public List ExtraDescription; /// P4 changelist number that this change was submitted with public int ChangelistNumber; /// Bug TTP numbers public List TTPNumbers; /// /// Constructor /// public ChangeDesc() { Type = EChangeType.Unknown; BriefDescription = String.Empty; ExtraDescription = new List(); ChangelistNumber = 0; TTPNumbers = new List(); } } /// /// Describes a node in the category tree /// class ProjectCategory { /// Name of this category public String Name; /// List of changes in this category public List Changes; /// List of sub-categories public List Subcategories; /// /// Constructor /// public ProjectCategory() { Name = String.Empty; Changes = new List(); Subcategories = new List(); } } /// /// Generates a report from a set of changes using the specified settings /// /// Report generation settings /// Change data /// Output report string public static void GenerateReportFromChanges( ReportSettings InReportSettings, List InChanges, out String OutReportString ) { // Sort everything into buckets based on category ProjectCategory MainCategory = new ProjectCategory(); MainCategory.Name = "Main"; // We'll only create an "uncategorized" category if we need to (later) ProjectCategory UncategorizedCategory = null; // Process changes foreach( var Changelist in InChanges ) { foreach( var Subchange in Changelist.Subchanges ) { ChangeDesc NewChangeDesc = new ChangeDesc(); // Store changelist number NewChangeDesc.ChangelistNumber = Changelist.BaseChangelist.Id; // Classify this change if( Subchange.StartTag.StartTagName.Equals( "Fix", StringComparison.InvariantCultureIgnoreCase ) ) { NewChangeDesc.Type = EChangeType.Fix; } else if( Subchange.StartTag.StartTagName.Equals( "Change", StringComparison.InvariantCultureIgnoreCase ) ) { NewChangeDesc.Type = EChangeType.Change; } else if( Subchange.StartTag.StartTagName.Equals( "New", StringComparison.InvariantCultureIgnoreCase ) ) { NewChangeDesc.Type = EChangeType.New; } // Store the change description NewChangeDesc.BriefDescription = Subchange.StartTag.BriefDescription; // Store the extra description if( Subchange.AuxiliaryTags.ContainsKey( "extra" ) ) { List< P4AuxiliaryTag > AuxTags = Subchange.AuxiliaryTags[ "extra" ]; foreach( P4AuxiliaryTag CurAuxTag in AuxTags ) { P4ExtraTag CurExtraTag = CurAuxTag as P4ExtraTag; NewChangeDesc.ExtraDescription.Add( CurExtraTag.Description ); } } // Iterate over the types of tags attached to this change ProjectCategory BestCategory = null; foreach( var CurAuxTag in Subchange.AuxiliaryTags.Keys ) { // Now iterate over the ordered list of tags of a single type attached to this change List CurAuxList = Subchange.AuxiliaryTags[ CurAuxTag ]; foreach( P4AuxiliaryTag CurTag in CurAuxList ) { // TTP number if( CurTag is P4TTPTag ) { P4TTPTag TTPTag = (P4TTPTag)CurTag; NewChangeDesc.TTPNumbers.Add( TTPTag.TTPNumber ); } // Project (aka. category) else if( CurTag is P4ProjTag ) { if( BestCategory == null ) { P4ProjTag ProjTag = (P4ProjTag)CurTag; // Parse the string String[] CategoryPath = ProjTag.Project.Split( '.' ); ProjectCategory SearchCategory = MainCategory; foreach( var CurCategoryString in CategoryPath ) { // Try to find an existing category at this level in the hierarchy bool bFoundExistingCategory = false; foreach( var CurCategory in SearchCategory.Subcategories ) { if( CurCategory.Name.Equals( CurCategoryString, StringComparison.InvariantCultureIgnoreCase ) ) { // Found an existing category at this level bFoundExistingCategory = true; // The subcategory we found now becomes the search path SearchCategory = CurCategory; break; } } // If we didn't find a category go ahead and create one now if( !bFoundExistingCategory ) { var NewSubcategory = new ProjectCategory(); NewSubcategory.Name = CurCategoryString; SearchCategory.Subcategories.Add( NewSubcategory ); // The new subcategory now becomes the search path SearchCategory = NewSubcategory; } } // We now have a subcategory to put this change into BestCategory = SearchCategory; } else { // We already have a category for this change. Just ignore this entry. } } } } // If the user didn't mention a category, then give it a default one if( BestCategory == null ) { if( UncategorizedCategory == null && InReportSettings.UncategorizedCategoryName.Length > 0 ) { var NewSubcategory = new ProjectCategory(); NewSubcategory.Name = InReportSettings.UncategorizedCategoryName; } // Place the item into the 'uncategorized' category BestCategory = UncategorizedCategory; } if( BestCategory != null ) { // Attach this change to the best category for it BestCategory.Changes.Add( NewChangeDesc ); } } } // Sort the data SortCategoriesRecursively( InReportSettings, MainCategory ); // Write output buffer according to the user's configuration using( StringWriter OutputWriter = new StringWriter() ) { // Omit the header name for the main category bool bIncludeCategoryHeader = false; int IndentLevel = 1; WriteCategoryChangesRecursively( OutputWriter, InReportSettings, MainCategory, bIncludeCategoryHeader, IndentLevel ); // Store the report string OutReportString = OutputWriter.ToString(); } } /// /// Recursively writes formatted project category to a string buffer /// /// Output string writer (buffer is appended to) /// Settings /// Category data to write /// Include the category header> /// Indent level static void WriteCategoryChangesRecursively( StringWriter OutputWriter, ReportSettings InReportSettings, ProjectCategory InCategory, bool bIncludeCategoryHeader, int InIndentLevel ) { // Indent items beneath the header! int NextIndentLevel = InIndentLevel; if( bIncludeCategoryHeader ) { NextIndentLevel += 1; } if( InReportSettings.Format == P4ParsedChangelist.EExportFormats.EF_PlainText ) { if( bIncludeCategoryHeader ) { // Category name header for( int IndentIndex = 0; IndentIndex < InIndentLevel; ++IndentIndex ) { OutputWriter.Write( InReportSettings.IndentText ); } OutputWriter.Write( InReportSettings.BulletText ); OutputWriter.WriteLine( InCategory.Name ); } } // Recurse! foreach( var CurSubcategory in InCategory.Subcategories ) { bool bIncludeSubcategoryHeader = true; WriteCategoryChangesRecursively( OutputWriter, InReportSettings, CurSubcategory, bIncludeSubcategoryHeader, NextIndentLevel ); } if( InReportSettings.Format == P4ParsedChangelist.EExportFormats.EF_PlainText ) { // Change description foreach( var CurChange in InCategory.Changes ) { String OneLineSummary = CurChange.BriefDescription; // Prefix the summary with the "type" of change, if we need to if( InReportSettings.bPrefixChangesWithType && CurChange.Type != EChangeType.Unknown ) { String ChangeTypeName = String.Empty; switch( CurChange.Type ) { case EChangeType.Fix: ChangeTypeName = "Fix"; break; case EChangeType.Change: ChangeTypeName = "Change"; break; case EChangeType.New: ChangeTypeName = "New"; break; } OneLineSummary = ChangeTypeName + ": " + OneLineSummary; } // Append the TTP numbers too if we need to if( InReportSettings.bAppendTTPNumbersToOneLineSummary && CurChange.TTPNumbers.Count > 0 ) { OneLineSummary += " ("; bool bIsFirstTTPNumber = true; foreach( var CurTTPNumber in CurChange.TTPNumbers ) { if( !bIsFirstTTPNumber ) { OneLineSummary += ", "; } OneLineSummary += "TTP " + CurTTPNumber.ToString(); bIsFirstTTPNumber = false; } OneLineSummary += ")"; } // Append the P4 changelist numbers too if we need to if( InReportSettings.bAppendChangelistNumbersToOneLineSummary ) { OneLineSummary += " [CL " + CurChange.ChangelistNumber + "]"; } // One-line summary for( int IndentIndex = 0; IndentIndex < NextIndentLevel; ++IndentIndex ) { OutputWriter.Write( InReportSettings.IndentText ); } OutputWriter.Write( InReportSettings.BulletText ); OutputWriter.WriteLine( OneLineSummary ); // Extra description lines { foreach( string CurLine in CurChange.ExtraDescription ) { // Indent int ExtraTextIndentLevel = NextIndentLevel + 1; for( int IndentIndex = 0; IndentIndex < ExtraTextIndentLevel; ++IndentIndex ) { OutputWriter.Write( InReportSettings.IndentText ); } OutputWriter.Write( InReportSettings.BulletText ); OutputWriter.WriteLine( CurLine ); } } } } } /// /// Sorts categories alphabetically by name /// class CategorySortComparer : IComparer< ProjectCategory > { // IComparer: Compares two objects and returns a value indicating whether one is less than, // equal to, or greater than the other. public int Compare( ProjectCategory x, ProjectCategory y ) { return x.Name.CompareTo( y.Name ); } } /// /// Sorts changes based on change type, then alphabetically by summary /// class ChangeSortComparer : IComparer { // IComparer: Compares two objects and returns a value indicating whether one is less than, // equal to, or greater than the other. public int Compare( ChangeDesc x, ChangeDesc y ) { // Sort categories such that changes of types earlier in our enum appear first if( x.Type < y.Type ) { return -1; } else if( x.Type > y.Type ) { return 1; } // Same type, so simply sort alphabetically by description return x.BriefDescription.CompareTo( y.BriefDescription ); } } /// /// Recursively sorts categories /// /// Settings /// Category to sort (along with all child categories) static void SortCategoriesRecursively( ReportSettings InReportSettings, ProjectCategory InCategory ) { // Sort categories by name alphabetically. InCategory.Subcategories.Sort( new CategorySortComparer() ); // Sort changes by their type of change (new -> changed -> fix), then alphabetically InCategory.Changes.Sort( new ChangeSortComparer() ); // Recurse! foreach( var CurSubcategory in InCategory.Subcategories ) { SortCategoriesRecursively( InReportSettings, CurSubcategory ); } } } }