2021-01-19 18:51:24 -04:00
// Copyright Epic Games, Inc. All Rights Reserved.
using EpicGames.Core ;
using Microsoft.Extensions.Logging ;
using Microsoft.Extensions.Logging.Console ;
using P4VUtils.Commands ;
using System ;
using System.Collections.Generic ;
using System.ComponentModel ;
using System.IO ;
using System.Linq ;
using System.Reflection ;
using System.Threading.Tasks ;
using System.Xml ;
namespace P4VUtils
{
class CustomToolInfo
{
public string Name { get ; set ; }
public string Arguments { get ; set ; }
public bool AddToContextMenu { get ; set ; } = true ;
2021-01-21 17:41:00 -04:00
public bool ShowConsole { get ; set ; }
2021-06-08 19:06:41 -04:00
public bool RefreshUI { get ; set ; } = true ;
public string Shortcut { get ; set ; } = "" ;
2021-12-13 17:09:39 -05:00
public bool PromptForArgument { get ; set ; }
2021-07-14 13:23:58 -04:00
public string PromptText { get ; set ; } = "" ;
2021-01-19 18:51:24 -04:00
public CustomToolInfo ( string Name , string Arguments )
{
this . Name = Name ;
this . Arguments = Arguments ;
}
}
abstract class Command
{
public abstract string Description { get ; }
public abstract CustomToolInfo CustomTool { get ; }
public abstract Task < int > Execute ( string [ ] Args , IReadOnlyDictionary < string , string > ConfigValues , ILogger Logger ) ;
}
class Program
{
2021-11-03 15:49:48 -04:00
// UEHelpersInRoot - commands that help with common but simple operations
public static IReadOnlyDictionary < string , Command > RootHelperCommands { get ; } = new Dictionary < string , Command > ( StringComparer . OrdinalIgnoreCase )
{
["describe"] = new DescribeCommand ( ) ,
2022-05-17 15:02:27 -04:00
#if ! IS_LINUX
2021-11-03 15:49:48 -04:00
["copyclnum"] = new CopyCLCommand ( ) ,
2022-05-17 15:02:27 -04:00
#endif
2021-11-03 15:49:48 -04:00
} ;
2022-04-14 04:26:33 -04:00
// UESubmit - commands that help with submitting files/changelists
public static IReadOnlyDictionary < string , Command > SubmissionCommands { get ; } = new Dictionary < string , Command > ( StringComparer . OrdinalIgnoreCase )
{
["submitandvirtualize"] = new SubmitAndVirtualizeCommand ( ) ,
} ;
2021-06-10 09:32:54 -04:00
// UEHelpers - commands that help with common but simple operations
public static IReadOnlyDictionary < string , Command > HelperCommands { get ; } = new Dictionary < string , Command > ( StringComparer . OrdinalIgnoreCase )
{
["findlastedit"] = new FindLastEditCommand ( ) ,
2021-08-04 17:18:20 -04:00
["findlasteditbyline"] = new P4BlameCommand ( ) ,
2021-06-10 09:32:54 -04:00
["snapshot"] = new SnapshotCommand ( ) ,
2021-08-07 22:49:04 -04:00
["reconcilecode"] = new FastReconcileCodeEditsCommand ( ) ,
["reconcileall"] = new FastReconcileAllEditsCommand ( ) ,
2021-11-03 15:49:48 -04:00
["unshelvetocurrentrevision"] = new UnshelveToCurrentRevision ( ) ,
["unshelvemakedatawritable"] = new UnshelveMakeDataWritable ( ) ,
["convertcldatatolocalwritable"] = new ConvertCLDataToLocalWritable ( ) ,
["convertdatatolocalwritable"] = new ConvertDataToLocalWritable ( ) ,
2021-06-10 09:32:54 -04:00
} ;
// UEIntegrate Folder commands - complex commands to facilitate integrations/backout
public static IReadOnlyDictionary < string , Command > IntegrateCommands { get ; } = new Dictionary < string , Command > ( StringComparer . OrdinalIgnoreCase )
2021-01-19 18:51:24 -04:00
{
2021-03-28 19:43:37 -04:00
["cherrypick"] = new CherryPickCommand ( ) ,
2021-01-28 18:04:48 -04:00
["converttoedit"] = new ConvertToEditCommand ( ) ,
["edigrate"] = new EdigrateCommand ( ) ,
2021-06-10 09:32:54 -04:00
["backout"] = new BackoutCommand ( ) ,
} ;
// UEHorde Folder - local build and horde preflights
public static IReadOnlyDictionary < string , Command > HordeCommands { get ; } = new Dictionary < string , Command > ( StringComparer . OrdinalIgnoreCase )
{
["compile"] = new CompileCommand ( ) ,
2021-01-19 18:51:24 -04:00
["preflight"] = new PreflightCommand ( ) ,
["preflightandsubmit"] = new PreflightAndSubmitCommand ( ) ,
2021-06-08 19:06:41 -04:00
["movewriteablepreflightandsubmit"] = new MoveWriteableFilesthenPreflightAndSubmitCommand ( ) ,
2021-01-19 18:51:24 -04:00
} ;
2022-04-14 04:26:33 -04:00
public static IDictionary < string , Command > Commands = SubmissionCommands . Concat ( RootHelperCommands ) . Concat ( HelperCommands ) . Concat ( IntegrateCommands ) . Concat ( HordeCommands ) . ToDictionary ( p = > p . Key , p = > p . Value , StringComparer . OrdinalIgnoreCase ) ;
2021-06-10 09:32:54 -04:00
2021-01-19 18:51:24 -04:00
static void PrintHelp ( ILogger Logger )
{
Logger . LogInformation ( "P4VUtils" ) ;
Logger . LogInformation ( "Provides useful shortcuts for working with P4V" ) ;
Logger . LogInformation ( "" ) ;
Logger . LogInformation ( "Usage:" ) ;
Logger . LogInformation ( " P4VUtils [Command] [Arguments...]" ) ;
Logger . LogInformation ( "" ) ;
List < KeyValuePair < string , string > > Table = new List < KeyValuePair < string , string > > ( ) ;
foreach ( KeyValuePair < string , Command > Pair in Commands )
{
Table . Add ( new KeyValuePair < string , string > ( Pair . Key , Pair . Value . Description ) ) ;
}
Logger . LogInformation ( "Commands:" ) ;
2022-04-13 10:27:29 -04:00
HelpUtils . PrintTable ( Table , 2 , 15 , ConsoleUtils . WindowWidth - 1 , Logger ) ;
2021-01-19 18:51:24 -04:00
}
static async Task < int > Main ( string [ ] Args )
{
using ILoggerFactory Factory = LoggerFactory . Create ( Builder = > Builder . AddEpicDefault ( ) ) ; //.AddSimpleConsole(Options => { Options.SingleLine = true; Options.IncludeScopes = false; }));
ILogger Logger = Factory . CreateLogger < Program > ( ) ;
2022-05-23 09:49:24 -04:00
Log . SetInnerLogger ( Logger ) ;
2021-01-19 18:51:24 -04:00
2021-01-21 17:41:00 -04:00
try
{
return await InnerMain ( Args , Logger ) ;
}
catch ( Exception Ex )
{
Logger . LogError ( Ex , "Unhandled exception: {Ex}" , Ex . ToString ( ) ) ;
return 1 ;
}
}
static async Task < int > InnerMain ( string [ ] Args , ILogger Logger )
{
2021-01-19 18:51:24 -04:00
if ( Args . Length = = 0 | | Args [ 0 ] . Equals ( "-help" , StringComparison . OrdinalIgnoreCase ) )
{
PrintHelp ( Logger ) ;
return 0 ;
}
2021-12-13 17:09:39 -05:00
else if ( Args [ 0 ] . StartsWith ( "-" , StringComparison . Ordinal ) )
2021-01-19 18:51:24 -04:00
{
2021-12-13 17:09:39 -05:00
Logger . LogInformation ( "Missing command name" ) ;
2021-01-19 18:51:24 -04:00
PrintHelp ( Logger ) ;
return 1 ;
}
else if ( Args [ 0 ] . Equals ( "install" , StringComparison . OrdinalIgnoreCase ) )
{
Logger . LogInformation ( "Adding custom tools..." ) ;
return await UpdateCustomToolRegistration ( true , Logger ) ;
}
else if ( Args [ 0 ] . Equals ( "uninstall" , StringComparison . OrdinalIgnoreCase ) )
{
Logger . LogInformation ( "Removing custom tools..." ) ;
return await UpdateCustomToolRegistration ( false , Logger ) ;
}
else if ( Commands . TryGetValue ( Args [ 0 ] , out Command ? Command ) )
{
if ( Args . Any ( x = > x . Equals ( "-help" , StringComparison . OrdinalIgnoreCase ) ) )
{
List < KeyValuePair < string , string > > Parameters = CommandLineArguments . GetParameters ( Command . GetType ( ) ) ;
2022-04-13 10:27:29 -04:00
Logger . LogInformation ( "{Title}" , Args [ 0 ] ) ;
Logger . LogInformation ( "{Description}" , Command . GetType ( ) ) ;
Logger . LogInformation ( "Parameters:" ) ;
HelpUtils . PrintTable ( Parameters , 4 , 24 , HelpUtils . WindowWidth - 1 , Logger ) ;
2021-01-19 18:51:24 -04:00
return 0 ;
}
Dictionary < string , string > ConfigValues = ReadConfig ( ) ;
return await Command . Execute ( Args , ConfigValues , Logger ) ;
}
else
2022-05-17 12:00:08 -04:00
{
2021-12-13 17:09:39 -05:00
Logger . LogError ( "Unknown command: {Command}" , Args [ 0 ] ) ;
2021-01-19 18:51:24 -04:00
PrintHelp ( Logger ) ;
return 1 ;
}
}
static Dictionary < string , string > ReadConfig ( )
{
Dictionary < string , string > ConfigValues = new Dictionary < string , string > ( StringComparer . OrdinalIgnoreCase ) ;
string BasePath = Path . GetDirectoryName ( Assembly . GetExecutingAssembly ( ) . Location ) ! ;
AppendConfig ( Path . Combine ( BasePath , "P4VUtils.ini" ) , ConfigValues ) ;
AppendConfig ( Path . Combine ( BasePath , "NotForLicensees" , "P4VUtils.ini" ) , ConfigValues ) ;
return ConfigValues ;
}
static void AppendConfig ( string SourcePath , Dictionary < string , string > ConfigValues )
{
if ( File . Exists ( SourcePath ) )
{
string [ ] Lines = File . ReadAllLines ( SourcePath ) ;
foreach ( string Line in Lines )
{
2021-12-13 17:09:39 -05:00
int EqualsIdx = Line . IndexOf ( '=' , StringComparison . Ordinal ) ;
2021-01-19 18:51:24 -04:00
if ( EqualsIdx ! = - 1 )
{
string Key = Line . Substring ( 0 , EqualsIdx ) . Trim ( ) ;
string Value = Line . Substring ( EqualsIdx + 1 ) . Trim ( ) ;
ConfigValues [ Key ] = Value ;
}
}
}
}
2021-01-21 17:41:00 -04:00
public static bool TryLoadXmlDocument ( FileReference Location , XmlDocument Document )
{
if ( FileReference . Exists ( Location ) )
{
try
{
Document . Load ( Location . FullName ) ;
return true ;
}
catch
{
}
}
return false ;
}
2021-06-10 09:32:54 -04:00
static string GetToolName ( XmlElement ToolNode )
2021-01-19 18:51:24 -04:00
{
2022-05-24 07:14:12 -04:00
return ToolNode . SelectSingleNode ( "Definition" ) ? . SelectSingleNode ( "Name" ) ? . InnerText ? ? string . Empty ;
2021-06-10 09:32:54 -04:00
}
2021-01-19 18:51:24 -04:00
2021-06-10 09:32:54 -04:00
// returns true if all tools were removed
static bool RemoveCustomToolsFromNode ( XmlElement RootNode , FileReference DotNetLocation , FileReference AssemblyLocation , ILogger Logger )
{
int ToolsChecked = 0 ;
int ToolsRemoved = 0 ;
2022-05-24 07:14:12 -04:00
XmlNodeList ? CustomToolDefList = RootNode . SelectNodes ( "CustomToolDef" ) ;
if ( CustomToolDefList = = null )
{
return false ;
}
2021-06-10 09:32:54 -04:00
// Removes tools explicitly calling the assembly location identified above - i assume as a way to "filter" only those we explicitly added (@Ben.Marsh) - nochecking, remove this comment once verified.
2022-05-24 07:14:12 -04:00
foreach ( XmlNode ? ChildNode in CustomToolDefList )
2021-01-19 18:51:24 -04:00
{
XmlElement ? ChildElement = ChildNode as XmlElement ;
if ( ChildElement ! = null )
{
2021-06-10 09:32:54 -04:00
ToolsChecked + + ;
2021-01-19 18:51:24 -04:00
XmlElement ? CommandElement = ChildElement . SelectSingleNode ( "Definition/Command" ) as XmlElement ;
if ( CommandElement ! = null & & new FileReference ( CommandElement . InnerText ) = = DotNetLocation )
{
XmlElement ? ArgumentsElement = ChildElement . SelectSingleNode ( "Definition/Arguments" ) as XmlElement ;
if ( ArgumentsElement ! = null )
{
string [ ] Arguments = CommandLineArguments . Split ( ArgumentsElement . InnerText ) ;
if ( Arguments . Length > 0 & & new FileReference ( Arguments [ 0 ] ) = = AssemblyLocation )
{
2021-12-13 17:09:39 -05:00
Logger . LogInformation ( "Removing Tool {ToolName}" , GetToolName ( ChildElement ) ) ;
2021-06-10 09:32:54 -04:00
RootNode . RemoveChild ( ChildElement ) ;
ToolsRemoved + + ;
2021-01-19 18:51:24 -04:00
}
}
}
}
}
2021-06-10 09:32:54 -04:00
return ToolsChecked = = ToolsRemoved ;
}
2021-11-03 15:49:48 -04:00
static void InstallCommandsListInFolder ( string FolderName , bool AddFolderToContextMenu , IReadOnlyDictionary < string , Command > InputCommmands , XmlDocument Document , FileReference DotNetLocation , FileReference AssemblyLocation , ILogger Logger )
2021-06-10 09:32:54 -04:00
{
// <CustomToolDefList> // list of custom tools (top level)
// < CustomToolDef > // loose custom tool in top level
// < CustomToolFolder> // folder containing custom tools
// < Name > Test </ Name >
// < CustomToolDefList > // list of custom tools in folder
// < CustomToolDef > // definition of tool
// This is the top level node, there will also be a per folder node added of same name
XmlElement ? Root = Document . SelectSingleNode ( "CustomToolDefList" ) as XmlElement ;
if ( Root ! = null )
2021-01-19 18:51:24 -04:00
{
2021-06-10 09:32:54 -04:00
XmlElement FolderDefinition = Document . CreateElement ( "CustomToolFolder" ) ;
XmlElement FolderDescription = Document . CreateElement ( "Name" ) ;
FolderDescription . InnerText = FolderName ;
FolderDefinition . AppendChild ( FolderDescription ) ;
2021-11-03 15:49:48 -04:00
XmlElement FolderToContextMenu = Document . CreateElement ( "AddToContext" ) ;
FolderToContextMenu . InnerText = AddFolderToContextMenu ? "true" : "false" ;
FolderDefinition . AppendChild ( FolderToContextMenu ) ;
2021-06-10 09:32:54 -04:00
XmlElement FolderDefList = Document . CreateElement ( "CustomToolDefList" ) ;
foreach ( KeyValuePair < string , Command > Pair in InputCommmands )
2021-01-19 18:51:24 -04:00
{
CustomToolInfo CustomTool = Pair . Value . CustomTool ;
XmlElement ToolDef = Document . CreateElement ( "CustomToolDef" ) ;
{
XmlElement Definition = Document . CreateElement ( "Definition" ) ;
{
XmlElement Description = Document . CreateElement ( "Name" ) ;
Description . InnerText = CustomTool . Name ;
Definition . AppendChild ( Description ) ;
XmlElement Command = Document . CreateElement ( "Command" ) ;
2022-05-17 15:02:27 -04:00
Command . InnerText = DotNetLocation . FullName . QuoteArgument ( ) ;
2021-01-19 18:51:24 -04:00
Definition . AppendChild ( Command ) ;
XmlElement Arguments = Document . CreateElement ( "Arguments" ) ;
2021-01-21 17:41:00 -04:00
Arguments . InnerText = $"{AssemblyLocation.FullName.QuoteArgument()} {Pair.Key} {CustomTool.Arguments}" ;
2021-01-19 18:51:24 -04:00
Definition . AppendChild ( Arguments ) ;
2021-06-08 19:06:41 -04:00
if ( CustomTool . Shortcut . Length > 1 )
{
XmlElement Shortcut = Document . CreateElement ( "Shortcut" ) ;
Shortcut . InnerText = CustomTool . Shortcut ;
Definition . AppendChild ( Shortcut ) ;
}
2021-01-19 18:51:24 -04:00
}
ToolDef . AppendChild ( Definition ) ;
2021-01-21 17:41:00 -04:00
if ( CustomTool . ShowConsole )
{
XmlElement Console = Document . CreateElement ( "Console" ) ;
{
XmlElement CloseOnExit = Document . CreateElement ( "CloseOnExit" ) ;
CloseOnExit . InnerText = "false" ;
Console . AppendChild ( CloseOnExit ) ;
}
ToolDef . AppendChild ( Console ) ;
}
2021-06-08 19:06:41 -04:00
if ( CustomTool . RefreshUI )
{
XmlElement Refresh = Document . CreateElement ( "Refresh" ) ;
Refresh . InnerText = CustomTool . RefreshUI ? "true" : "false" ;
ToolDef . AppendChild ( Refresh ) ;
}
2021-07-14 13:23:58 -04:00
if ( CustomTool . PromptForArgument )
{
XmlElement Prompt = Document . CreateElement ( "Prompt" ) ;
{
XmlElement PromptText = Document . CreateElement ( "PromptText" ) ;
PromptText . InnerText = CustomTool . PromptText . Length > 0 ? CustomTool . PromptText : "Argument" ;
Prompt . AppendChild ( PromptText ) ;
}
ToolDef . AppendChild ( Prompt ) ;
}
2021-01-19 18:51:24 -04:00
XmlElement AddToContext = Document . CreateElement ( "AddToContext" ) ;
AddToContext . InnerText = CustomTool . AddToContextMenu ? "true" : "false" ;
ToolDef . AppendChild ( AddToContext ) ;
}
2021-06-10 09:32:54 -04:00
FolderDefList . AppendChild ( ToolDef ) ;
2021-01-19 18:51:24 -04:00
}
2021-06-10 09:32:54 -04:00
FolderDefinition . AppendChild ( FolderDefList ) ;
Root . AppendChild ( FolderDefinition ) ;
}
}
static void RemoveCustomToolsFromFolders ( XmlElement RootNode , FileReference DotNetLocation , FileReference AssemblyLocation , ILogger Logger )
{
2022-05-24 07:14:12 -04:00
XmlNodeList ? CustomToolFolderList = RootNode . SelectNodes ( "CustomToolFolder" ) ;
if ( CustomToolFolderList = = null )
{
return ;
}
foreach ( XmlNode ? ChildNode in CustomToolFolderList )
2021-06-10 09:32:54 -04:00
{
if ( ChildNode ! = null )
{
bool RemoveFolder = false ;
XmlElement ? FolderRoot = ChildNode . SelectSingleNode ( "CustomToolDefList" ) as XmlElement ;
if ( FolderRoot ! = null )
{
XmlElement ? FolderNameNode = ChildNode . SelectSingleNode ( "Name" ) as XmlElement ;
string FolderNameString = "" ;
if ( FolderNameNode ! = null )
{
FolderNameString = FolderNameNode . InnerText ;
}
2021-12-13 17:09:39 -05:00
Logger . LogInformation ( "Removing Tools from folder {Folder}" , FolderNameString ) ;
2021-06-10 09:32:54 -04:00
RemoveFolder = RemoveCustomToolsFromNode ( FolderRoot , DotNetLocation , AssemblyLocation , Logger ) ;
}
if ( RemoveFolder )
{
// remove the folder itself.
RootNode . RemoveChild ( ChildNode ) ;
}
}
}
}
public static async Task < int > UpdateCustomToolRegistration ( bool bInstall , ILogger Logger )
{
DirectoryReference ? ConfigDir = DirectoryReference . GetSpecialFolder ( Environment . SpecialFolder . UserProfile ) ;
if ( ConfigDir = = null )
{
Logger . LogError ( "Unable to find config directory." ) ;
return 1 ;
}
2022-05-17 12:00:08 -04:00
FileReference ConfigFile ;
if ( OperatingSystem . IsMacOS ( ) )
{
ConfigFile = FileReference . Combine ( ConfigDir , "Library" , "Preferences" , "com.perforce.p4v" , "customtools.xml" ) ;
}
else
{
ConfigFile = FileReference . Combine ( ConfigDir , ".p4qt" , "customtools.xml" ) ;
}
2021-06-10 09:32:54 -04:00
XmlDocument Document = new XmlDocument ( ) ;
if ( ! TryLoadXmlDocument ( ConfigFile , Document ) )
{
DirectoryReference . CreateDirectory ( ConfigFile . Directory ) ;
using ( StreamWriter Writer = new StreamWriter ( ConfigFile . FullName ) )
{
await Writer . WriteLineAsync ( @"<?xml version=""1.0"" encoding=""UTF-8""?>" ) ;
await Writer . WriteLineAsync ( @"<!--perforce-xml-version=1.0-->" ) ;
await Writer . WriteLineAsync ( @"<CustomToolDefList varName=""customtooldeflist"">" ) ;
await Writer . WriteLineAsync ( @"</CustomToolDefList>" ) ;
}
Document . Load ( ConfigFile . FullName ) ;
}
2022-05-17 15:02:27 -04:00
// so we don't need to run SetupDotnet.sh every time we run a tool, point to the executing dotnet, assuming it will be usable later
FileReference DotNetLocation = new FileReference ( Environment . ProcessPath ! ) ;
2021-06-10 09:32:54 -04:00
FileReference AssemblyLocation = new FileReference ( Assembly . GetExecutingAssembly ( ) . GetOriginalLocation ( ) ) ;
XmlElement ? Root = Document . SelectSingleNode ( "CustomToolDefList" ) as XmlElement ;
if ( Root = = null )
{
Logger . LogError ( "Unknown schema for {ConfigFile}" , ConfigFile ) ;
return 1 ;
}
// Remove Custom tools at the root
RemoveCustomToolsFromNode ( Root , DotNetLocation , AssemblyLocation , Logger ) ;
// Remove Custom tools in folders, and the folders
RemoveCustomToolsFromFolders ( Root , DotNetLocation , AssemblyLocation , Logger ) ;
// Insert new entries
if ( bInstall )
{
2021-11-03 15:49:48 -04:00
InstallCommandsListInFolder ( "UERootHelpers" , false /*AddFolderToContextMenu*/ , RootHelperCommands , Document , DotNetLocation , AssemblyLocation , Logger ) ;
2022-04-14 04:26:33 -04:00
InstallCommandsListInFolder ( "UESubmit" , true /*AddFolderToContextMenu*/ , SubmissionCommands , Document , DotNetLocation , AssemblyLocation , Logger ) ;
2021-11-03 15:49:48 -04:00
InstallCommandsListInFolder ( "UEHelpers" , true /*AddFolderToContextMenu*/ , HelperCommands , Document , DotNetLocation , AssemblyLocation , Logger ) ;
InstallCommandsListInFolder ( "UEIntegrate" , true /*AddFolderToContextMenu*/ , IntegrateCommands , Document , DotNetLocation , AssemblyLocation , Logger ) ;
InstallCommandsListInFolder ( "UEHorde" , true /*AddFolderToContextMenu*/ , HordeCommands , Document , DotNetLocation , AssemblyLocation , Logger ) ;
2021-01-19 18:51:24 -04:00
}
// Save the new document
Document . Save ( ConfigFile . FullName ) ;
2021-01-21 17:41:00 -04:00
Logger . LogInformation ( "Written {ConfigFile}" , ConfigFile . FullName ) ;
2021-01-19 18:51:24 -04:00
return 0 ;
}
}
}