Files
UnrealEngineUWP/Engine/Source/Programs/AutomationTool/P4Utils.cs

2749 lines
90 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 class RequireP4Attribute : Attribute
{
}
public class DoesNotNeedP4CLAttribute : Attribute
{
}
public class P4Exception : AutomationException
{
public P4Exception() { }
public P4Exception(string Msg)
: base(Msg) { }
public P4Exception(string Msg, Exception InnerException)
: base(Msg, InnerException) { }
public P4Exception(string Format, params object[] Args)
: base(Format, Args) { }
}
[Flags]
public enum P4LineEnd
{
Local = 0,
Unix = 1,
Mac = 2,
Win = 3,
Share = 4,
}
[Flags]
public enum P4SubmitOption
{
SubmitUnchanged = 0,
RevertUnchanged = 1,
LeaveUnchanged = 2,
}
[Flags]
public enum P4ClientOption
{
None = 0,
NoAllWrite = 1,
NoClobber = 2,
NoCompress = 4,
NoModTime = 8,
NoRmDir = 16,
Unlocked = 32,
AllWrite = 64,
Clobber = 128,
Compress = 256,
Locked = 512,
ModTime = 1024,
RmDir = 2048,
}
public class P4ClientInfo
{
public string Name;
public string RootPath;
public string Host;
public string Owner;
public DateTime Access;
public P4LineEnd LineEnd;
public P4ClientOption Options;
public P4SubmitOption SubmitOptions;
public List<KeyValuePair<string, string>> View = new List<KeyValuePair<string, string>>();
public bool Matches(P4ClientInfo Other)
{
return Name == Other.Name
&& RootPath == Other.RootPath
&& Host == Other.Host
&& Owner == Other.Owner
&& LineEnd == Other.LineEnd
&& Options == Other.Options
&& SubmitOptions == Other.SubmitOptions
&& Enumerable.SequenceEqual(View, Other.View);
}
public override string ToString()
{
return Name;
}
}
public enum P4FileType
{
[Description("unknown")]
Unknown,
[Description("text")]
Text,
[Description("binary")]
Binary,
[Description("resource")]
Resource,
[Description("tempobj")]
Temp,
[Description("symlink")]
Symlink,
[Description("apple")]
Apple,
[Description("unicode")]
Unicode,
[Description("utf16")]
Utf16,
}
[Flags]
public enum P4FileAttributes
{
[Description("")]
None = 0,
[Description("u")]
Unicode = 1 << 0,
[Description("x")]
Executable = 1 << 1,
[Description("w")]
Writeable = 1 << 2,
[Description("m")]
LocalModTimes = 1 << 3,
[Description("k")]
RCS = 1 << 4,
[Description("l")]
Exclusive = 1 << 5,
[Description("D")]
DeltasPerRevision = 1 << 6,
[Description("F")]
Uncompressed = 1 << 7,
[Description("C")]
Compressed = 1 << 8,
[Description("X")]
Archive = 1 << 9,
[Description("S")]
Revisions = 1 << 10,
}
public enum P4Action
{
[Description("none")]
None,
[Description("add")]
Add,
[Description("edit")]
Edit,
[Description("delete")]
Delete,
[Description("branch")]
Branch,
[Description("move/add")]
MoveAdd,
[Description("move/delete")]
MoveDelete,
[Description("integrate")]
Integrate,
[Description("import")]
Import,
[Description("purge")]
Purge,
[Description("archive")]
Archive,
[Description("unknown")]
Unknown,
}
public struct P4FileStat
{
public P4FileType Type;
public P4FileAttributes Attributes;
public P4Action Action;
public string Change;
public bool IsOldType;
public P4FileStat(P4FileType Type, P4FileAttributes Attributes, P4Action Action)
{
this.Type = Type;
this.Attributes = Attributes;
this.Action = Action;
this.Change = String.Empty;
this.IsOldType = false;
}
public static readonly P4FileStat Invalid = new P4FileStat(P4FileType.Unknown, P4FileAttributes.None, P4Action.None);
public bool IsValid { get { return Type != P4FileType.Unknown; } }
}
public class P4WhereRecord
{
public bool bUnmap;
public string DepotFile;
public string ClientFile;
public string Path;
}
public class P4Spec
{
public List<KeyValuePair<string, string>> Sections;
/// <summary>
/// Default constructor.
/// </summary>
public P4Spec()
{
Sections = new List<KeyValuePair<string,string>>();
}
/// <summary>
/// Gets the current value of a field with the given name
/// </summary>
/// <param name="Name">Name of the field to search for</param>
/// <returns>The value of the field, or null if it does not exist</returns>
public string GetField(string Name)
{
int Idx = Sections.FindIndex(x => x.Key == Name);
return (Idx == -1)? null : Sections[Idx].Value;
}
/// <summary>
/// Sets the value of an existing field, or adds a new one with the given name
/// </summary>
/// <param name="Name">Name of the field to set</param>
/// <param name="Value">New value of the field</param>
public void SetField(string Name, string Value)
{
int Idx = Sections.FindIndex(x => x.Key == Name);
if(Idx == -1)
{
Sections.Add(new KeyValuePair<string,string>(Name, Value));
}
else
{
Sections[Idx] = new KeyValuePair<string,string>(Name, Value);
}
}
/// <summary>
/// Parses a spec (clientspec, branchspec, changespec) from an array of lines
/// </summary>
/// <param name="Lines">Text split into separate lines</param>
/// <returns>Array of section names and values</returns>
public static P4Spec FromString(string Text)
{
P4Spec Spec = new P4Spec();
string[] Lines = Text.Split('\n');
for(int LineIdx = 0; LineIdx < Lines.Length; LineIdx++)
{
if(Lines[LineIdx].EndsWith("\r"))
{
Lines[LineIdx] = Lines[LineIdx].Substring(0, Lines[LineIdx].Length - 1);
}
if(!String.IsNullOrWhiteSpace(Lines[LineIdx]) && !Lines[LineIdx].StartsWith("#"))
{
// Read the section name
int SeparatorIdx = Lines[LineIdx].IndexOf(':');
if(SeparatorIdx == -1 || !Char.IsLetter(Lines[LineIdx][0]))
{
throw new P4Exception("Invalid spec format at line {0}: \"{1}\"", LineIdx, Lines[LineIdx]);
}
// Get the section name
string SectionName = Lines[LineIdx].Substring(0, SeparatorIdx);
// Parse the section value
StringBuilder Value = new StringBuilder(Lines[LineIdx].Substring(SeparatorIdx + 1));
for(; LineIdx + 1 < Lines.Length; LineIdx++)
{
if(Lines[LineIdx + 1].Length == 0)
{
Value.AppendLine();
}
else if(Lines[LineIdx + 1][0] == '\t')
{
Value.AppendLine(Lines[LineIdx + 1].Substring(1));
}
else
{
break;
}
}
Spec.Sections.Add(new KeyValuePair<string,string>(SectionName, Value.ToString().TrimEnd()));
}
}
return Spec;
}
/// <summary>
/// Formats a P4 specification as a block of text
/// </summary>
/// <returns></returns>
public override string ToString()
{
StringBuilder Result = new StringBuilder();
foreach(KeyValuePair<string, string> Section in Sections)
{
if(Section.Value.Contains('\n'))
{
Result.AppendLine(Section.Key + ":\n\t" + Section.Value.Replace("\n", "\n\t"));
}
else
{
Result.AppendLine(Section.Key + ":\t" + Section.Value);
}
Result.AppendLine();
}
return Result.ToString();
}
}
public partial class CommandUtils
{
#region Environment Setup
static private P4Connection PerforceConnection;
static private P4Environment PerforceEnvironment;
/// <summary>
/// BuildEnvironment to use for this buildcommand. This is initialized by InitBuildEnvironment. As soon
/// as the script execution in ExecuteBuild begins, the BuildEnv is set up and ready to use.
/// </summary>
static public P4Connection P4
{
get
{
if (PerforceConnection == null)
{
throw new AutomationException("Attempt to use P4 before it was initialized or P4 support is disabled.");
}
return PerforceConnection;
}
}
/// <summary>
/// BuildEnvironment to use for this buildcommand. This is initialized by InitBuildEnvironment. As soon
/// as the script execution in ExecuteBuild begins, the BuildEnv is set up and ready to use.
/// </summary>
static public P4Environment P4Env
{
get
{
if (PerforceEnvironment == null)
{
throw new AutomationException("Attempt to use P4Environment before it was initialized or P4 support is disabled.");
}
return PerforceEnvironment;
}
}
/// <summary>
/// Initializes build environment. If the build command needs a specific env-var mapping or
/// has an extended BuildEnvironment, it must implement this method accordingly.
/// </summary>
static internal void InitP4Environment()
{
CheckP4Enabled();
// Temporary connection - will use only the currently set env vars to connect to P4
var DefaultConnection = new P4Connection(User: null, Client: null);
PerforceEnvironment = Automation.IsBuildMachine ? new P4Environment(DefaultConnection, CmdEnv) : new LocalP4Environment(DefaultConnection, CmdEnv);
}
/// <summary>
/// Initializes default source control connection.
/// </summary>
static internal void InitDefaultP4Connection()
{
CheckP4Enabled();
PerforceConnection = new P4Connection(User: P4Env.User, Client: P4Env.Client, ServerAndPort: P4Env.P4Port);
}
#endregion
/// <summary>
/// Check if P4 is supported.
/// </summary>
public static bool P4Enabled
{
get
{
if (!bP4Enabled.HasValue)
{
throw new AutomationException("Trying to access P4Enabled property before it was initialized.");
}
return (bool)bP4Enabled;
}
private set
{
bP4Enabled = value;
}
}
private static bool? bP4Enabled;
/// <summary>
/// Check if P4CL is required.
/// </summary>
public static bool P4CLRequired
{
get
{
if (!bP4CLRequired.HasValue)
{
throw new AutomationException("Trying to access P4CLRequired property before it was initialized.");
}
return (bool)bP4CLRequired;
}
private set
{
bP4CLRequired = value;
}
}
private static bool? bP4CLRequired;
/// <summary>
/// Throws an exception when P4 is disabled. This should be called in every P4 function.
/// </summary>
internal static void CheckP4Enabled()
{
if (P4Enabled == false)
{
throw new AutomationException("P4 is not enabled.");
}
}
/// <summary>
/// Checks whether commands are allowed to submit files into P4.
/// </summary>
public static bool AllowSubmit
{
get
{
if (!bAllowSubmit.HasValue)
{
throw new AutomationException("Trying to access AllowSubmit property before it was initialized.");
}
return (bool)bAllowSubmit;
}
private set
{
bAllowSubmit = value;
}
}
private static bool? bAllowSubmit;
/// <summary>
/// Sets up P4Enabled, AllowSubmit properties. Note that this does not intialize P4 environment.
/// </summary>
/// <param name="CommandsToExecute">Commands to execute</param>
/// <param name="Commands">Commands</param>
internal static void InitP4Support(List<CommandInfo> CommandsToExecute, CaselessDictionary<Type> Commands)
{
// Init AllowSubmit
// If we do not specify on the commandline if submitting is allowed or not, this is
// depending on whether we run locally or on a build machine.
Log("Initializing AllowSubmit.");
if (GlobalCommandLine.Submit || GlobalCommandLine.NoSubmit)
{
AllowSubmit = GlobalCommandLine.Submit;
}
else
{
AllowSubmit = Automation.IsBuildMachine;
}
Log("AllowSubmit={0}", AllowSubmit);
// Init P4Enabled
Log("Initializing P4Enabled.");
if (Automation.IsBuildMachine)
{
P4Enabled = !GlobalCommandLine.NoP4;
P4CLRequired = P4Enabled;
}
else
{
bool bRequireP4;
bool bRequireCL;
CheckIfCommandsRequireP4(CommandsToExecute, Commands, out bRequireP4, out bRequireCL);
P4Enabled = GlobalCommandLine.P4 || bRequireP4;
P4CLRequired = GlobalCommandLine.P4 || bRequireCL;
}
Log("P4Enabled={0}", P4Enabled);
Log("P4CLRequired={0}", P4CLRequired);
}
/// <summary>
/// Checks if any of the commands to execute has [RequireP4] attribute.
/// </summary>
/// <param name="CommandsToExecute">List of commands to be executed.</param>
/// <param name="Commands">Commands.</param>
private static void CheckIfCommandsRequireP4(List<CommandInfo> CommandsToExecute, CaselessDictionary<Type> Commands, out bool bRequireP4, out bool bRequireCL)
{
bRequireP4 = false;
bRequireCL = false;
foreach (var CommandInfo in CommandsToExecute)
{
Type Command;
if (Commands.TryGetValue(CommandInfo.CommandName, out Command))
{
var RequireP4Attributes = Command.GetCustomAttributes(typeof(RequireP4Attribute), true);
if (!CommandUtils.IsNullOrEmpty(RequireP4Attributes))
{
Log("Command {0} requires P4 functionality.", Command.Name);
bRequireP4 = true;
var DoesNotNeedP4CLAttributes = Command.GetCustomAttributes(typeof(DoesNotNeedP4CLAttribute), true);
if (CommandUtils.IsNullOrEmpty(DoesNotNeedP4CLAttributes))
{
bRequireCL = true;
}
}
}
}
}
}
/// <summary>
/// Class that stores labels info.
/// </summary>
public class P4Label
{
// The name of the label.
public string Name { get; private set; }
// The date of the label.
public DateTime Date { get; private set; }
public P4Label(string Name, DateTime Date)
{
this.Name = Name;
this.Date = Date;
}
}
/// <summary>
/// Perforce connection.
/// </summary>
public partial class P4Connection
{
/// <summary>
/// List of global options for this connection (client/user)
/// </summary>
private string GlobalOptions;
/// <summary>
/// List of global options for this connection (client/user)
/// </summary>
private string GlobalOptionsWithoutClient;
/// <summary>
/// Path where this connection's log is to go to
/// </summary>
public string LogPath { get; private set; }
/// <summary>
/// Initializes P4 connection
/// </summary>
/// <param name="User">Username (can be null, in which case the environment variable default will be used)</param>
/// <param name="Client">Workspace (can be null, in which case the environment variable default will be used)</param>
/// <param name="ServerAndPort">Server:Port (can be null, in which case the environment variable default will be used)</param>
/// <param name="P4LogPath">Log filename (can be null, in which case CmdEnv.LogFolder/p4.log will be used)</param>
public P4Connection(string User, string Client, string ServerAndPort = null, string P4LogPath = null)
{
var UserOpts = String.IsNullOrEmpty(User) ? "" : ("-u" + User + " ");
var ClientOpts = String.IsNullOrEmpty(Client) ? "" : ("-c" + Client + " ");
var ServerOpts = String.IsNullOrEmpty(ServerAndPort) ? "" : ("-p" + ServerAndPort + " ");
GlobalOptions = UserOpts + ClientOpts + ServerOpts;
GlobalOptionsWithoutClient = UserOpts + ServerOpts;
if (P4LogPath == null)
{
LogPath = CommandUtils.CombinePaths(CommandUtils.CmdEnv.LogFolder, String.Format("p4.log", Client));
}
else
{
LogPath = P4LogPath;
}
}
/// <summary>
/// Throws an exception when P4 is disabled. This should be called in every P4 function.
/// </summary>
internal static void CheckP4Enabled()
{
CommandUtils.CheckP4Enabled();
}
/// <summary>
/// Shortcut to Run but with P4.exe as the program name.
/// </summary>
/// <param name="CommandLine">Command line</param>
/// <param name="Input">Stdin</param>
/// <param name="AllowSpew">true for spew</param>
/// <returns>Exit code</returns>
public ProcessResult P4(string CommandLine, string Input = null, bool AllowSpew = true, bool WithClient = true, bool SpewIsVerbose = false)
{
CheckP4Enabled();
CommandUtils.ERunOptions RunOptions = AllowSpew ? CommandUtils.ERunOptions.AllowSpew : CommandUtils.ERunOptions.NoLoggingOfRunCommand;
if( SpewIsVerbose )
{
RunOptions |= CommandUtils.ERunOptions.SpewIsVerbose;
}
return CommandUtils.Run(HostPlatform.Current.P4Exe, (WithClient ? GlobalOptions : GlobalOptionsWithoutClient) + CommandLine, Input, Options:RunOptions);
}
/// <summary>
/// Calls p4 and returns the output.
/// </summary>
/// <param name="Output">Output of the comman.</param>
/// <param name="CommandLine">Commandline for p4.</param>
/// <param name="Input">Stdin input.</param>
/// <param name="AllowSpew">Whether the command should spew.</param>
/// <returns>True if succeeded, otherwise false.</returns>
public bool P4Output(out string Output, string CommandLine, string Input = null, bool AllowSpew = true, bool WithClient = true)
{
CheckP4Enabled();
Output = "";
var Result = P4(CommandLine, Input, AllowSpew, WithClient);
Output = Result.Output;
return Result == 0;
}
/// <summary>
/// Calls p4 command and writes the output to a logfile.
/// </summary>
/// <param name="CommandLine">Commandline to pass to p4.</param>
/// <param name="Input">Stdin input.</param>
/// <param name="AllowSpew">Whether the command is allowed to spew.</param>
public void LogP4(string CommandLine, string Input = null, bool AllowSpew = true, bool WithClient = true, bool SpewIsVerbose = false)
{
CheckP4Enabled();
string Output;
if (!LogP4Output(out Output, CommandLine, Input, AllowSpew, WithClient, SpewIsVerbose:SpewIsVerbose))
{
throw new P4Exception("p4.exe {0} failed.", CommandLine);
}
}
/// <summary>
/// Calls p4 and returns the output and writes it also to a logfile.
/// </summary>
/// <param name="Output">Output of the comman.</param>
/// <param name="CommandLine">Commandline for p4.</param>
/// <param name="Input">Stdin input.</param>
/// <param name="AllowSpew">Whether the command should spew.</param>
/// <returns>True if succeeded, otherwise false.</returns>
public bool LogP4Output(out string Output, string CommandLine, string Input = null, bool AllowSpew = true, bool WithClient = true, bool SpewIsVerbose = false)
{
CheckP4Enabled();
Output = "";
if (String.IsNullOrEmpty(LogPath))
{
CommandUtils.Log(TraceEventType.Error, "P4Utils.SetupP4() must be called before issuing Peforce commands");
return false;
}
var Result = P4(CommandLine, Input, AllowSpew, WithClient, SpewIsVerbose:SpewIsVerbose);
CommandUtils.WriteToFile(LogPath, CommandLine + "\n");
CommandUtils.WriteToFile(LogPath, Result.Output);
Output = Result.Output;
return Result == 0;
}
/// <summary>
/// Invokes p4 login command.
/// </summary>
public string GetAuthenticationToken()
{
string AuthenticationToken = null;
string Output;
string P4Passwd = InternalUtils.GetEnvironmentVariable("uebp_PASS", "", true) + '\n';
P4Output(out Output, "login -a -p", P4Passwd);
// Validate output.
const string PasswordPromptString = "Enter password: \r\n";
if (Output.Substring(0, PasswordPromptString.Length) == PasswordPromptString)
{
int AuthenticationResultStartIndex = PasswordPromptString.Length;
Regex TokenRegex = new Regex("[0-9A-F]{32}");
Match TokenMatch = TokenRegex.Match(Output, AuthenticationResultStartIndex);
if (TokenMatch.Success)
{
AuthenticationToken = Output.Substring(TokenMatch.Index, TokenMatch.Length);
}
}
return AuthenticationToken;
}
/// <summary>
/// Invokes p4 changes command.
/// </summary>
/// <param name="CommandLine">CommandLine to pass on to the command.</param>
public class ChangeRecord
{
public int CL = 0;
public string User = "";
public string UserEmail = "";
public string Summary = "";
public static int Compare(ChangeRecord A, ChangeRecord B)
{
return (A.CL < B.CL) ? -1 : (A.CL > B.CL) ? 1 : 0;
}
}
static Dictionary<string, string> UserToEmailCache = new Dictionary<string, string>();
public string UserToEmail(string User)
{
if (UserToEmailCache.ContainsKey(User))
{
return UserToEmailCache[User];
}
string Result = "";
try
{
var P4Result = P4(String.Format("user -o {0}", User), AllowSpew: false);
if (P4Result == 0)
{
var Tags = ParseTaggedP4Output(P4Result.Output);
Tags.TryGetValue("Email", out Result);
}
}
catch(Exception)
{
}
if (Result == "")
{
CommandUtils.Log(TraceEventType.Warning, "Could not find email for P4 user {0}", User);
}
UserToEmailCache.Add(User, Result);
return Result;
}
static Dictionary<string, List<ChangeRecord>> ChangesCache = new Dictionary<string, List<ChangeRecord>>();
public bool Changes(out List<ChangeRecord> ChangeRecords, string CommandLine, bool AllowSpew = true, bool UseCaching = false, bool LongComment = false, bool WithClient = false)
{
// If the user specified '-l' or '-L', the summary will appear on subsequent lines (no quotes) instead of the same line (surrounded by single quotes)
bool ContainsDashL = CommandLine.StartsWith("-L ", StringComparison.InvariantCultureIgnoreCase) ||
CommandLine.IndexOf(" -L ", StringComparison.InvariantCultureIgnoreCase) > 0;
bool bSummaryIsOnSameLine = !ContainsDashL;
if (bSummaryIsOnSameLine && LongComment)
{
CommandLine = "-L " + CommandLine;
bSummaryIsOnSameLine = false;
}
if (UseCaching && ChangesCache.ContainsKey(CommandLine))
{
ChangeRecords = ChangesCache[CommandLine];
return true;
}
ChangeRecords = new List<ChangeRecord>();
CheckP4Enabled();
try
{
// Change 1999345 on 2014/02/16 by buildmachine@BuildFarm_BUILD-23_buildmachine_++depot+UE4 'GUBP Node Shadow_LabelPromotabl'
string Output;
if (!LogP4Output(out Output, "changes " + CommandLine, null, AllowSpew, WithClient: WithClient))
{
throw new AutomationException("P4 returned failure.");
}
var Lines = Output.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
for(int LineIndex = 0; LineIndex < Lines.Length; ++LineIndex)
{
var Line = Lines[ LineIndex ];
// If we've hit a blank line, then we're done
if( String.IsNullOrEmpty( Line ) )
{
break;
}
ChangeRecord Change = new ChangeRecord();
string MatchChange = "Change ";
string MatchOn = " on ";
string MatchBy = " by ";
int ChangeAt = Line.IndexOf(MatchChange);
int OnAt = Line.IndexOf(MatchOn);
int ByAt = Line.IndexOf(MatchBy);
if (ChangeAt == 0 && OnAt > ChangeAt && ByAt > OnAt)
{
var ChangeString = Line.Substring(ChangeAt + MatchChange.Length, OnAt - ChangeAt - MatchChange.Length);
Change.CL = int.Parse(ChangeString);
if (Change.CL < 1990000)
{
throw new AutomationException("weird CL {0} in {1}", Change.CL, Line);
}
int AtAt = Line.IndexOf("@");
Change.User = Line.Substring(ByAt + MatchBy.Length, AtAt - ByAt - MatchBy.Length);
if( bSummaryIsOnSameLine )
{
int TickAt = Line.IndexOf("'");
int EndTick = Line.LastIndexOf("'");
if( TickAt > ByAt && EndTick > TickAt )
{
Change.Summary = Line.Substring(TickAt + 1, EndTick - TickAt - 1);
}
}
else
{
++LineIndex;
if( LineIndex >= Lines.Length )
{
throw new AutomationException("Was expecting a change summary to appear after Change header output from P4, but there were no more lines to read");
}
Line = Lines[ LineIndex ];
if( !String.IsNullOrEmpty( Line ) )
{
throw new AutomationException("Was expecting blank line after Change header output from P4, got {0}", Line);
}
++LineIndex;
for( ; LineIndex < Lines.Length; ++LineIndex )
{
Line = Lines[ LineIndex ];
int SummaryChangeAt = Line.IndexOf(MatchChange);
int SummaryOnAt = Line.IndexOf(MatchOn);
int SummaryByAt = Line.IndexOf(MatchBy);
if (SummaryChangeAt == 0 && SummaryOnAt > SummaryChangeAt && SummaryByAt > SummaryOnAt)
{
// OK, we found a new change. This isn't part of our summary. We're done with the summary. Back we go.
//CommandUtils.Log("Next summary is {0}", Line);
--LineIndex;
break;
}
// Summary lines are supposed to begin with a single tab character (even empty lines)
if( !String.IsNullOrEmpty( Line ) && Line[0] != '\t' )
{
throw new AutomationException("Was expecting every line of the P4 changes summary to start with a tab character or be totally empty");
}
// Remove the tab
var SummaryLine = Line;
if( Line.StartsWith( "\t" ) )
{
SummaryLine = Line.Substring( 1 );
}
// Add a CR if we already had some summary text
if( !String.IsNullOrEmpty( Change.Summary ) )
{
Change.Summary += "\n";
}
// Append the summary line!
Change.Summary += SummaryLine;
}
}
Change.UserEmail = UserToEmail(Change.User);
ChangeRecords.Add(Change);
}
else
{
throw new AutomationException("Output of 'p4 changes' was not formatted how we expected. Could not find 'Change', 'on' and 'by' in the output line: " + Line);
}
}
}
catch (Exception Ex)
{
CommandUtils.Log(System.Diagnostics.TraceEventType.Warning, "Unable to get P4 changes with {0}", CommandLine);
CommandUtils.Log(System.Diagnostics.TraceEventType.Warning, " Exception was {0}", LogUtils.FormatException(Ex));
return false;
}
ChangeRecords.Sort((A, B) => ChangeRecord.Compare(A, B));
if( ChangesCache.ContainsKey(CommandLine) )
{
ChangesCache[CommandLine] = ChangeRecords;
}
else
{
ChangesCache.Add(CommandLine, ChangeRecords);
}
return true;
}
public class DescribeRecord
{
public int CL = 0;
public string User = "";
public string UserEmail = "";
public string Summary = "";
public string Header = "";
public class DescribeFile
{
public string File;
public int Revision;
public string ChangeType;
}
public List<DescribeFile> Files = new List<DescribeFile>();
public static int Compare(DescribeRecord A, DescribeRecord B)
{
return (A.CL < B.CL) ? -1 : (A.CL > B.CL) ? 1 : 0;
}
}
/// <summary>
/// Wraps P4 describe
/// </summary>
/// <param name="Changelists">List of changelist numbers to query full descriptions for</param>
/// <param name="DescribeRecords">List of records we found. One for each changelist number. These will be sorted from oldest to newest.</param>
/// <param name="AllowSpew"></param>
/// <returns>True if everything went okay</returns>
public bool DescribeChangelists(List<int> Changelists, out List<DescribeRecord> DescribeRecords, bool AllowSpew = true)
{
DescribeRecords = new List<DescribeRecord>();
CheckP4Enabled();
try
{
// Change 234641 by This.User@WORKSPACE-C2Q-67_Dev on 2008/05/06 10:32:32
//
// Desc Line 1
//
// Affected files ...
//
// ... //depot/UnrealEngine3/Development/Src/Engine/Classes/ArrowComponent.uc#8 edit
// ... //depot/UnrealEngine3/Development/Src/Engine/Classes/DecalActorBase.uc#4 edit
string Output;
string CommandLine = "-s"; // Don't automatically diff the files
// Add changelists to the command-line
foreach( var Changelist in Changelists )
{
CommandLine += " " + Changelist.ToString();
}
if (!LogP4Output(out Output, "describe " + CommandLine, null, AllowSpew))
{
return false;
}
int ChangelistIndex = 0;
var Lines = Output.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
for (var LineIndex = 0; LineIndex < Lines.Length; ++LineIndex)
{
var Line = Lines[ LineIndex ];
// If we've hit a blank line, then we're done
if( String.IsNullOrEmpty( Line ) )
{
break;
}
string MatchChange = "Change ";
string MatchOn = " on ";
string MatchBy = " by ";
int ChangeAt = Line.IndexOf(MatchChange);
int OnAt = Line.IndexOf(MatchOn);
int ByAt = Line.IndexOf(MatchBy);
int AtAt = Line.IndexOf("@");
if (ChangeAt == 0 && OnAt > ChangeAt && ByAt < OnAt)
{
var ChangeString = Line.Substring(ChangeAt + MatchChange.Length, ByAt - ChangeAt - MatchChange.Length);
var CurrentChangelist = Changelists[ ChangelistIndex++ ];
if (!ChangeString.Equals( CurrentChangelist.ToString()))
{
throw new AutomationException("Was expecting changelists to be reported back in the same order we asked for them (CL {0} != {1})", ChangeString, CurrentChangelist.ToString());
}
var DescribeRecord = new DescribeRecord();
DescribeRecords.Add( DescribeRecord );
DescribeRecord.CL = CurrentChangelist;
DescribeRecord.User = Line.Substring(ByAt + MatchBy.Length, AtAt - ByAt - MatchBy.Length);
DescribeRecord.Header = Line;
++LineIndex;
if( LineIndex >= Lines.Length )
{
throw new AutomationException("Was expecting a change summary to appear after Change header output from P4, but there were no more lines to read");
}
Line = Lines[ LineIndex ];
if( !String.IsNullOrEmpty( Line ) )
{
throw new AutomationException("Was expecting blank line after Change header output from P4");
}
// Summary
++LineIndex;
for( ; LineIndex < Lines.Length; ++LineIndex )
{
Line = Lines[ LineIndex ];
if( String.IsNullOrEmpty( Line ) )
{
// Summaries end with a blank line (no tabs)
break;
}
// Summary lines are supposed to begin with a single tab character (even empty lines)
if( Line[0] != '\t' )
{
throw new AutomationException("Was expecting every line of the P4 changes summary to start with a tab character");
}
// Remove the tab
var SummaryLine = Line.Substring( 1 );
// Add a CR if we already had some summary text
if( !String.IsNullOrEmpty( DescribeRecord.Summary ) )
{
DescribeRecord.Summary += "\n";
}
// Append the summary line!
DescribeRecord.Summary += SummaryLine;
}
++LineIndex;
if( LineIndex >= Lines.Length )
{
throw new AutomationException("Was expecting 'Affected files' to appear after the summary output from P4, but there were no more lines to read");
}
// If the summary ends with an empty newline, it doesn't seem to be prefixed with a tab
while (LineIndex < Lines.Length && Lines[LineIndex].Length == 0)
{
LineIndex++;
}
Line = Lines[ LineIndex ];
string MatchAffectedFiles = "Affected files";
int AffectedFilesAt = Line.IndexOf(MatchAffectedFiles);
if( AffectedFilesAt == 0 )
{
++LineIndex;
if( LineIndex >= Lines.Length )
{
throw new AutomationException("Was expecting a list of files to appear after Affected Files header output from P4, but there were no more lines to read");
}
Line = Lines[ LineIndex ];
if( !String.IsNullOrEmpty( Line ) )
{
throw new AutomationException("Was expecting blank line after Affected Files header output from P4");
}
// Files
++LineIndex;
for( ; LineIndex < Lines.Length; ++LineIndex )
{
Line = Lines[ LineIndex ];
if( String.IsNullOrEmpty( Line ) )
{
// Summaries end with a blank line (no tabs)
break;
}
// File lines are supposed to begin with a "... " string
if( !Line.StartsWith( "... " ) )
{
throw new AutomationException("Was expecting every line of the P4 describe files to start with a tab character");
}
// Remove the "... " prefix
var FilesLine = Line.Substring( 4 );
var DescribeFile = new DescribeRecord.DescribeFile();
DescribeRecord.Files.Add( DescribeFile );
// Find the revision #
var RevisionNumberAt = FilesLine.LastIndexOf( "#" ) + 1;
var ChangeTypeAt = 1 + FilesLine.IndexOf( " ", RevisionNumberAt );
DescribeFile.File = FilesLine.Substring( 0, RevisionNumberAt - 1 );
string RevisionString = FilesLine.Substring( RevisionNumberAt, ChangeTypeAt - RevisionNumberAt );
DescribeFile.Revision = int.Parse( RevisionString );
DescribeFile.ChangeType = FilesLine.Substring( ChangeTypeAt );
}
}
else
{
throw new AutomationException("Output of 'p4 describe' was not formatted how we expected. Could not find 'Affected files' in the output line: " + Line);
}
DescribeRecord.UserEmail = UserToEmail(DescribeRecord.User);
}
else
{
throw new AutomationException("Output of 'p4 describe' was not formatted how we expected. Could not find 'Change', 'on' and 'by' in the output line: " + Line);
}
}
}
catch (Exception)
{
return false;
}
DescribeRecords.Sort((A, B) => DescribeRecord.Compare(A, B));
return true;
}
/// <summary>
/// Invokes p4 sync command.
/// </summary>
/// <param name="CommandLine">CommandLine to pass on to the command.</param>
public void Sync(string CommandLine, bool AllowSpew = true, bool SpewIsVerbose = false)
{
CheckP4Enabled();
LogP4("sync " + CommandLine, null, AllowSpew, SpewIsVerbose:SpewIsVerbose);
}
/// <summary>
/// Invokes p4 unshelve command.
/// </summary>
/// <param name="FromCL">Changelist to unshelve.</param>
/// <param name="ToCL">Changelist where the checked out files should be added.</param>
/// <param name="CommandLine">Commandline for the command.</param>
public void Unshelve(int FromCL, int ToCL, string CommandLine = "", bool SpewIsVerbose = false)
{
CheckP4Enabled();
LogP4("unshelve " + String.Format("-s {0} ", FromCL) + String.Format("-c {0} ", ToCL) + CommandLine, SpewIsVerbose: SpewIsVerbose);
}
/// <summary>
/// Invokes p4 unshelve command.
/// </summary>
/// <param name="FromCL">Changelist to unshelve.</param>
/// <param name="ToCL">Changelist where the checked out files should be added.</param>
/// <param name="CommandLine">Commandline for the command.</param>
public void Shelve(int FromCL, string CommandLine = "", bool AllowSpew = true)
{
CheckP4Enabled();
LogP4("shelve " + String.Format("-r -c {0} ", FromCL) + CommandLine, AllowSpew: AllowSpew);
}
/// <summary>
/// Deletes shelved files from a changelist
/// </summary>
/// <param name="FromCL">Changelist to unshelve.</param>
/// <param name="CommandLine">Commandline for the command.</param>
public void DeleteShelvedFiles(int FromCL, bool AllowSpew = true)
{
CheckP4Enabled();
string Output;
if (!LogP4Output(out Output, String.Format("shelve -d -c {0}", FromCL), AllowSpew: AllowSpew) && !Output.StartsWith("No shelved files in changelist to delete."))
{
throw new P4Exception("Couldn't unshelve files: {0}", Output);
}
}
/// <summary>
/// Invokes p4 edit command.
/// </summary>
/// <param name="CL">Changelist where the checked out files should be added.</param>
/// <param name="CommandLine">Commandline for the command.</param>
public void Edit(int CL, string CommandLine)
{
CheckP4Enabled();
LogP4("edit " + String.Format("-c {0} ", CL) + CommandLine);
}
/// <summary>
/// Invokes p4 edit command, no exceptions
/// </summary>
/// <param name="CL">Changelist where the checked out files should be added.</param>
/// <param name="CommandLine">Commandline for the command.</param>
public bool Edit_NoExceptions(int CL, string CommandLine)
{
try
{
CheckP4Enabled();
string Output;
if (!LogP4Output(out Output, "edit " + String.Format("-c {0} ", CL) + CommandLine, null, true))
{
return false;
}
if (Output.IndexOf("- opened for edit") < 0)
{
return false;
}
return true;
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// Invokes p4 add command.
/// </summary>
/// <param name="CL">Changelist where the files should be added to.</param>
/// <param name="CommandLine">Commandline for the command.</param>
public void Add(int CL, string CommandLine)
{
CheckP4Enabled();
LogP4("add " + String.Format("-c {0} ", CL) + CommandLine);
}
/// <summary>
/// Invokes p4 reconcile command.
/// </summary>
/// <param name="CL">Changelist to check the files out.</param>
/// <param name="CommandLine">Commandline for the command.</param>
public void Reconcile(int CL, string CommandLine, bool AllowSpew = true)
{
CheckP4Enabled();
LogP4("reconcile " + String.Format("-c {0} -ead -f ", CL) + CommandLine, AllowSpew: AllowSpew);
}
/// <summary>
/// Invokes p4 reconcile command.
/// </summary>
/// <param name="CL">Changelist to check the files out.</param>
/// <param name="CommandLine">Commandline for the command.</param>
public void ReconcilePreview(string CommandLine)
{
CheckP4Enabled();
LogP4("reconcile " + String.Format("-ead -n ") + CommandLine);
}
/// <summary>
/// Invokes p4 reconcile command.
/// Ignores files that were removed.
/// </summary>
/// <param name="CL">Changelist to check the files out.</param>
/// <param name="CommandLine">Commandline for the command.</param>
public void ReconcileNoDeletes(int CL, string CommandLine, bool AllowSpew = true)
{
CheckP4Enabled();
LogP4("reconcile " + String.Format("-c {0} -ea ", CL) + CommandLine, AllowSpew: AllowSpew);
}
/// <summary>
/// Invokes p4 resolve command.
/// Resolves all files by accepting yours and ignoring theirs.
/// </summary>
/// <param name="CL">Changelist to resolve.</param>
/// <param name="CommandLine">Commandline for the command.</param>
public void Resolve(int CL, string CommandLine)
{
CheckP4Enabled();
LogP4("resolve -ay " + String.Format("-c {0} ", CL) + CommandLine);
}
/// <summary>
/// Invokes revert command.
/// </summary>
/// <param name="CommandLine">Commandline for the command.</param>
public void Revert(string CommandLine)
{
CheckP4Enabled();
LogP4("revert " + CommandLine);
}
/// <summary>
/// Invokes revert command.
/// </summary>
/// <param name="CL">Changelist to revert</param>
/// <param name="CommandLine">Commandline for the command.</param>
public void Revert(int CL, string CommandLine = "", bool AllowSpew = true)
{
CheckP4Enabled();
LogP4("revert " + String.Format("-c {0} ", CL) + CommandLine, AllowSpew: AllowSpew);
}
/// <summary>
/// Reverts all unchanged file from the specified changelist.
/// </summary>
/// <param name="CL">Changelist to revert the unmodified files from.</param>
public void RevertUnchanged(int CL)
{
CheckP4Enabled();
// caution this is a really bad idea if you hope to force submit!!!
LogP4("revert -a " + String.Format("-c {0} ", CL));
}
/// <summary>
/// Reverts all files from the specified changelist.
/// </summary>
/// <param name="CL">Changelist to revert.</param>
public void RevertAll(int CL, bool SpewIsVerbose = false)
{
CheckP4Enabled();
LogP4("revert " + String.Format("-c {0} //...", CL), SpewIsVerbose: SpewIsVerbose);
}
/// <summary>
/// Submits the specified changelist.
/// </summary>
/// <param name="CL">Changelist to submit.</param>
/// <param name="SubmittedCL">Will be set to the submitted changelist number.</param>
/// <param name="Force">If true, the submit will be forced even if resolve is needed.</param>
/// <param name="RevertIfFail">If true, if the submit fails, revert the CL.</param>
public void Submit(int CL, out int SubmittedCL, bool Force = false, bool RevertIfFail = false)
{
CheckP4Enabled();
if (!CommandUtils.AllowSubmit)
{
throw new P4Exception("Submit is not allowed currently. Please use the -Submit switch to override that.");
}
SubmittedCL = 0;
int Retry = 0;
string LastCmdOutput = "none?";
while (Retry++ < 48)
{
bool Pending;
if (!ChangeExists(CL, out Pending))
{
throw new P4Exception("Change {0} does not exist.", CL);
}
if (!Pending)
{
throw new P4Exception("Change {0} was not pending.", CL);
}
bool isClPending = false;
if (ChangeFiles(CL, out isClPending, false).Count == 0)
{
CommandUtils.Log(TraceEventType.Information, "No edits left to commit after brutal submit resolve. Assuming another build committed same changes already and exiting as success.");
DeleteChange(CL);
// No changes to submit, no need to retry.
return;
}
string CmdOutput;
if (!LogP4Output(out CmdOutput, String.Format("submit -c {0}", CL)))
{
if (!Force)
{
throw new P4Exception("Change {0} failed to submit.\n{1}", CL, CmdOutput);
}
CommandUtils.Log(TraceEventType.Information, "**** P4 Returned\n{0}\n*******", CmdOutput);
LastCmdOutput = CmdOutput;
bool DidSomething = false;
string[] KnownProblems =
{
" - must resolve",
" - already locked by",
" - add of added file",
" - edit of deleted file",
};
bool AnyIssue = false;
foreach (var ProblemString in KnownProblems)
{
int ThisIndex = CmdOutput.IndexOf(ProblemString);
if (ThisIndex > 0)
{
AnyIssue = true;
break;
}
}
if (AnyIssue)
{
string Work = CmdOutput;
HashSet<string> AlreadyDone = new HashSet<string>();
while (Work.Length > 0)
{
string SlashSlashStr = "//";
int SlashSlash = Work.IndexOf(SlashSlashStr);
if (SlashSlash < 0)
{
break;
}
Work = Work.Substring(SlashSlash);
int MinMatch = Work.Length + 1;
foreach (var ProblemString in KnownProblems)
{
int ThisIndex = Work.IndexOf(ProblemString);
if (ThisIndex >= 0 && ThisIndex < MinMatch)
{
MinMatch = ThisIndex;
}
}
if (MinMatch > Work.Length)
{
break;
}
string File = Work.Substring(0, MinMatch).Trim();
if (AlreadyDone.Contains(File))
{
continue;
}
if (File.IndexOf(SlashSlashStr) != File.LastIndexOf(SlashSlashStr))
{
// this is some other line about the same line, we ignore it, removing the first // so we advance
Work = Work.Substring(SlashSlashStr.Length);
}
else
{
Work = Work.Substring(MinMatch);
CommandUtils.Log(TraceEventType.Information, "Brutal 'resolve' on {0} to force submit.\n", File);
Revert(CL, "-k " + CommandUtils.MakePathSafeToUseWithCommandLine(File)); // revert the file without overwriting the local one
Sync("-f -k " + CommandUtils.MakePathSafeToUseWithCommandLine(File + "#head"), false); // sync the file without overwriting local one
ReconcileNoDeletes(CL, CommandUtils.MakePathSafeToUseWithCommandLine(File)); // re-check out, if it changed, or add
DidSomething = true;
AlreadyDone.Add(File);
}
}
}
if (!DidSomething)
{
CommandUtils.Log(TraceEventType.Information, "Change {0} failed to submit for reasons we do not recognize.\n{1}\nWaiting and retrying.", CL, CmdOutput);
}
System.Threading.Thread.Sleep(30000);
}
else
{
LastCmdOutput = CmdOutput;
if (CmdOutput.Trim().EndsWith("submitted."))
{
if (CmdOutput.Trim().EndsWith(" and submitted."))
{
string EndStr = " and submitted.";
string ChangeStr = "renamed change ";
int Offset = CmdOutput.LastIndexOf(ChangeStr);
int EndOffset = CmdOutput.LastIndexOf(EndStr);
if (Offset >= 0 && Offset < EndOffset)
{
SubmittedCL = int.Parse(CmdOutput.Substring(Offset + ChangeStr.Length, EndOffset - Offset - ChangeStr.Length));
}
}
else
{
string EndStr = " submitted.";
string ChangeStr = "Change ";
int Offset = CmdOutput.LastIndexOf(ChangeStr);
int EndOffset = CmdOutput.LastIndexOf(EndStr);
if (Offset >= 0 && Offset < EndOffset)
{
SubmittedCL = int.Parse(CmdOutput.Substring(Offset + ChangeStr.Length, EndOffset - Offset - ChangeStr.Length));
}
}
CommandUtils.Log(TraceEventType.Information, "Submitted CL {0} which became CL {1}\n", CL, SubmittedCL);
}
if (SubmittedCL < CL)
{
throw new P4Exception("Change {0} submission seemed to succeed, but did not look like it.\n{1}", CL, CmdOutput);
}
// Change submitted OK! No need to retry.
return;
}
}
if (RevertIfFail)
{
CommandUtils.Log(TraceEventType.Error, "Submit CL {0} failed, reverting files\n", CL);
RevertAll(CL);
CommandUtils.Log(TraceEventType.Error, "Submit CL {0} failed, reverting files\n", CL);
}
throw new P4Exception("Change {0} failed to submit after 48 retries??.\n{1}", CL, LastCmdOutput);
}
/// <summary>
/// Creates a new changelist with the specified owner and description.
/// </summary>
/// <param name="Owner">Owner of the changelist.</param>
/// <param name="Description">Description of the changelist.</param>
/// <returns>Id of the created changelist.</returns>
public int CreateChange(string Owner = null, string Description = null, bool AllowSpew = false)
{
CheckP4Enabled();
var ChangeSpec = "Change: new" + "\n";
ChangeSpec += "Client: " + ((Owner != null) ? Owner : "") + "\n";
ChangeSpec += "Description: " + ((Description != null) ? Description.Replace("\n", "\n\t") : "(none)") + "\n";
string CmdOutput;
int CL = 0;
if(AllowSpew)
{
CommandUtils.Log(TraceEventType.Information, "Creating Change\n {0}\n", ChangeSpec);
}
if (LogP4Output(out CmdOutput, "change -i", Input: ChangeSpec, AllowSpew: AllowSpew))
{
string EndStr = " created.";
string ChangeStr = "Change ";
int Offset = CmdOutput.LastIndexOf(ChangeStr);
int EndOffset = CmdOutput.LastIndexOf(EndStr);
if (Offset >= 0 && Offset < EndOffset)
{
CL = int.Parse(CmdOutput.Substring(Offset + ChangeStr.Length, EndOffset - Offset - ChangeStr.Length));
}
}
if (CL <= 0)
{
throw new P4Exception("Failed to create Changelist. Owner: {0} Desc: {1}", Owner, Description);
}
else if(AllowSpew)
{
CommandUtils.Log(TraceEventType.Information, "Returned CL {0}\n", CL);
}
return CL;
}
/// <summary>
/// Updates a changelist with the given fields
/// </summary>
/// <param name="CL"></param>
/// <param name="NewOwner"></param>
/// <param name="NewDescription"></param>
/// <param name="SpewIsVerbose"></param>
public void UpdateChange(int CL, string NewOwner, string NewDescription, bool SpewIsVerbose = false)
{
CheckP4Enabled();
string CmdOutput;
if(!LogP4Output(out CmdOutput, String.Format("change -o {0}", CL), SpewIsVerbose: SpewIsVerbose))
{
throw new P4Exception("Couldn't describe changelist {0}", CL);
}
P4Spec Spec = P4Spec.FromString(CmdOutput);
if(NewOwner != null)
{
Spec.SetField("Client", NewOwner);
}
if(NewDescription != null)
{
Spec.SetField("Description", NewDescription);
}
if(!LogP4Output(out CmdOutput, "change -i", Input: Spec.ToString(), SpewIsVerbose: SpewIsVerbose))
{
throw new P4Exception("Failed to update spec for changelist {0}", CL);
}
if(!CmdOutput.TrimEnd().EndsWith(String.Format("Change {0} updated.", CL)))
{
throw new P4Exception("Unexpected output from p4 change -i: {0}", CmdOutput);
}
}
/// <summary>
/// Deletes the specified changelist.
/// </summary>
/// <param name="CL">Changelist to delete.</param>
/// <param name="RevertFiles">Indicates whether files in that changelist should be reverted.</param>
public void DeleteChange(int CL, bool RevertFiles = true, bool SpewIsVerbose = false, bool AllowSpew = true)
{
CheckP4Enabled();
if (RevertFiles)
{
RevertAll(CL, SpewIsVerbose: SpewIsVerbose);
}
string CmdOutput;
if (LogP4Output(out CmdOutput, String.Format("change -d {0}", CL), SpewIsVerbose: SpewIsVerbose, AllowSpew: AllowSpew))
{
string EndStr = " deleted.";
string ChangeStr = "Change ";
int Offset = CmdOutput.LastIndexOf(ChangeStr);
int EndOffset = CmdOutput.LastIndexOf(EndStr);
if (Offset == 0 && Offset < EndOffset)
{
return;
}
}
throw new P4Exception("Could not delete change {0} output follows\n{1}", CL, CmdOutput);
}
/// <summary>
/// Tries to delete the specified empty changelist.
/// </summary>
/// <param name="CL">Changelist to delete.</param>
/// <returns>True if the changelist was deleted, false otherwise.</returns>
public bool TryDeleteEmptyChange(int CL)
{
CheckP4Enabled();
string CmdOutput;
if (LogP4Output(out CmdOutput, String.Format("change -d {0}", CL)))
{
string EndStr = " deleted.";
string ChangeStr = "Change ";
int Offset = CmdOutput.LastIndexOf(ChangeStr);
int EndOffset = CmdOutput.LastIndexOf(EndStr);
if (Offset == 0 && Offset < EndOffset && !CmdOutput.Contains("can't be deleted."))
{
return true;
}
}
return false;
}
/// <summary>
/// Returns the changelist specification.
/// </summary>
/// <param name="CL">Changelist to get the specification from.</param>
/// <returns>Specification of the changelist.</returns>
public string ChangeOutput(int CL, bool AllowSpew = true)
{
CheckP4Enabled();
string CmdOutput;
if (LogP4Output(out CmdOutput, String.Format("change -o {0}", CL), AllowSpew: AllowSpew))
{
return CmdOutput;
}
throw new P4Exception("ChangeOutput failed {0} output follows\n{1}", CL, CmdOutput);
}
/// <summary>
/// Checks whether the specified changelist exists.
/// </summary>
/// <param name="CL">Changelist id.</param>
/// <param name="Pending">Whether it is a pending changelist.</param>
/// <returns>Returns whether the changelist exists.</returns>
public bool ChangeExists(int CL, out bool Pending, bool AllowSpew = true)
{
CheckP4Enabled();
string CmdOutput = ChangeOutput(CL, AllowSpew);
Pending = false;
if (CmdOutput.Length > 0)
{
string EndStr = " unknown.";
string ChangeStr = "Change ";
int Offset = CmdOutput.LastIndexOf(ChangeStr);
int EndOffset = CmdOutput.LastIndexOf(EndStr);
if (Offset == 0 && Offset < EndOffset)
{
CommandUtils.Log(TraceEventType.Information, "Change {0} does not exist", CL);
return false;
}
string StatusStr = "Status:";
int StatusOffset = CmdOutput.LastIndexOf(StatusStr);
string DescStr = "Description:";
int DescOffset = CmdOutput.LastIndexOf(DescStr);
if (StatusOffset < 1 || DescOffset < 1 || StatusOffset > DescOffset)
{
CommandUtils.Log(TraceEventType.Error, "Change {0} could not be parsed\n{1}", CL, CmdOutput);
return false;
}
string Status = CmdOutput.Substring(StatusOffset + StatusStr.Length, DescOffset - StatusOffset - StatusStr.Length).Trim();
CommandUtils.Log(TraceEventType.Information, "Change {0} exists ({1})", CL, Status);
Pending = (Status == "pending");
return true;
}
CommandUtils.Log(TraceEventType.Error, "Change exists failed {0} no output?", CL, CmdOutput);
return false;
}
/// <summary>
/// Returns a list of files contained in the specified changelist.
/// </summary>
/// <param name="CL">Changelist to get the files from.</param>
/// <param name="Pending">Whether the changelist is a pending one.</param>
/// <returns>List of the files contained in the changelist.</returns>
public List<string> ChangeFiles(int CL, out bool Pending, bool AllowSpew = true)
{
CheckP4Enabled();
var Result = new List<string>();
if (ChangeExists(CL, out Pending, AllowSpew))
{
string CmdOutput = ChangeOutput(CL, AllowSpew);
if (CmdOutput.Length > 0)
{
string FilesStr = "Files:";
int FilesOffset = CmdOutput.LastIndexOf(FilesStr);
if (FilesOffset < 0)
{
throw new P4Exception("Change {0} returned bad output\n{1}", CL, CmdOutput);
}
else
{
CmdOutput = CmdOutput.Substring(FilesOffset + FilesStr.Length);
while (CmdOutput.Length > 0)
{
string SlashSlashStr = "//";
int SlashSlash = CmdOutput.IndexOf(SlashSlashStr);
if (SlashSlash < 0)
{
break;
}
CmdOutput = CmdOutput.Substring(SlashSlash);
string HashStr = "#";
int Hash = CmdOutput.IndexOf(HashStr);
if (Hash < 0)
{
break;
}
string File = CmdOutput.Substring(0, Hash).Trim();
CmdOutput = CmdOutput.Substring(Hash);
Result.Add(File);
}
}
}
}
else
{
throw new P4Exception("Change {0} did not exist.", CL);
}
return Result;
}
/// <summary>
/// Returns the output from p4 opened
/// </summary>
/// <param name="CL">Changelist to get the specification from.</param>
/// <returns>Specification of the changelist.</returns>
public string OpenedOutput()
{
CheckP4Enabled();
string CmdOutput;
if (LogP4Output(out CmdOutput, "opened"))
{
return CmdOutput;
}
throw new P4Exception("OpenedOutput failed, output follows\n{0}", CmdOutput);
}
/// <summary>
/// Deletes the specified label.
/// </summary>
/// <param name="LabelName">Label to delete.</param>
public void DeleteLabel(string LabelName, bool AllowSpew = true)
{
CheckP4Enabled();
var CommandLine = "label -d " + LabelName;
// NOTE: We don't throw exceptions when trying to delete a label
string Output;
if (!LogP4Output(out Output, CommandLine, null, AllowSpew))
{
CommandUtils.Log(TraceEventType.Information, "Couldn't delete label '{0}'. It may not have existed in the first place.", LabelName);
}
}
/// <summary>
/// Creates a new label.
/// </summary>
/// <param name="Name">Name of the label.</param>
/// <param name="Options">Options for the label. Valid options are "locked", "unlocked", "autoreload" and "noautoreload".</param>
/// <param name="View">View mapping for the label.</param>
/// <param name="Owner">Owner of the label.</param>
/// <param name="Description">Description of the label.</param>
/// <param name="Date">Date of the label creation.</param>
/// <param name="Time">Time of the label creation</param>
public void CreateLabel(string Name, string Options, string View, string Owner = null, string Description = null, string Date = null, string Time = null)
{
CheckP4Enabled();
var LabelSpec = "Label: " + Name + "\n";
LabelSpec += "Owner: " + ((Owner != null) ? Owner : "") + "\n";
LabelSpec += "Description: " + ((Description != null) ? Description : "") + "\n";
if (Date != null)
{
LabelSpec += " Date: " + Date + "\n";
}
if (Time != null)
{
LabelSpec += " Time: " + Time + "\n";
}
LabelSpec += "Options: " + Options + "\n";
LabelSpec += "View: \n";
LabelSpec += " " + View;
CommandUtils.Log(TraceEventType.Information, "Creating Label\n {0}\n", LabelSpec);
LogP4("label -i", Input: LabelSpec);
}
/// <summary>
/// Invokes p4 tag command.
/// Associates a named label with a file revision.
/// </summary>
/// <param name="LabelName">Name of the label.</param>
/// <param name="FilePath">Path to the file.</param>
/// <param name="AllowSpew">Whether the command is allowed to spew.</param>
public void Tag(string LabelName, string FilePath, bool AllowSpew = true)
{
CheckP4Enabled();
LogP4("tag -l " + LabelName + " " + FilePath, null, AllowSpew);
}
/// <summary>
/// Syncs a label to the current content of the client.
/// </summary>
/// <param name="LabelName">Name of the label.</param>
/// <param name="AllowSpew">Whether the command is allowed to spew.</param>
public void LabelSync(string LabelName, bool AllowSpew = true, string FileToLabel = "")
{
CheckP4Enabled();
string Quiet = "";
if (!AllowSpew)
{
Quiet = "-q ";
}
if (FileToLabel == "")
{
LogP4("labelsync " + Quiet + "-l " + LabelName);
}
else
{
LogP4("labelsync " + Quiet + "-l" + LabelName + " " + FileToLabel);
}
}
/// <summary>
/// Syncs a label from another label.
/// </summary>
/// <param name="FromLabelName">Source label name.</param>
/// <param name="ToLabelName">Target label name.</param>
/// <param name="AllowSpew">Whether the command is allowed to spew.</param>
public void LabelToLabelSync(string FromLabelName, string ToLabelName, bool AllowSpew = true)
{
CheckP4Enabled();
string Quiet = "";
if (!AllowSpew)
{
Quiet = "-q ";
}
LogP4("labelsync -a " + Quiet + "-l " + ToLabelName + " //...@" + FromLabelName);
}
/// <summary>
/// Checks whether the specified label exists and has any files.
/// </summary>
/// <param name="Name">Name of the label.</param>
/// <returns>Whether there is an label with files.</returns>
public bool LabelExistsAndHasFiles(string Name)
{
CheckP4Enabled();
string Output;
return LogP4Output(out Output, "files -m 1 //...@" + Name);
}
/// <summary>
/// Returns the label description.
/// </summary>
/// <param name="Name">Name of the label.</param>
/// <param name="Description">Description of the label.</param>
/// <param name="AllowSpew">Whether to allow log spew</param>
/// <returns>Returns whether the label description could be retrieved.</returns>
public bool LabelDescription(string Name, out string Description, bool AllowSpew = true)
{
CheckP4Enabled();
string Output;
Description = "";
if (LogP4Output(out Output, "label -o " + Name, AllowSpew: AllowSpew))
{
string Desc = "Description:";
int Start = Output.LastIndexOf(Desc);
if (Start > 0)
{
Start += Desc.Length;
}
int End = Output.LastIndexOf("Options:");
if (Start > 0 && End > 0 && End > Start)
{
Description = Output.Substring(Start, End - Start).Replace("\n\t", "\n");
Description = Description.Trim();
return true;
}
}
return false;
}
/// <summary>
/// Reads a label spec
/// </summary>
/// <param name="Name">Label name</param>
/// <param name="AllowSpew">Whether to allow log spew</param>
public P4Spec ReadLabelSpec(string Name, bool AllowSpew = true)
{
string LabelSpec;
if(!LogP4Output(out LabelSpec, "label -o " + Name, AllowSpew: AllowSpew))
{
throw new P4Exception("Couldn't describe existing label '{0}', output was:\n", Name, LabelSpec);
}
return P4Spec.FromString(LabelSpec);
}
/// <summary>
/// Updates a label with a new spec
/// </summary>
/// <param name="Spec">Label specification</param>
/// <param name="AllowSpew">Whether to allow log spew</param>
public void UpdateLabelSpec(P4Spec Spec, bool AllowSpew = true)
{
LogP4("label -i", Input: Spec.ToString(), AllowSpew: AllowSpew);
}
/// <summary>
/// Updates a label description.
/// </summary>
/// <param name="Name">Name of the label</param>
/// <param name="Description">Description of the label.</param>
/// <param name="AllowSpew">Whether to allow log spew</param>
public void UpdateLabelDescription(string Name, string NewDescription, bool AllowSpew = true)
{
string LabelSpec;
if(!LogP4Output(out LabelSpec, "label -o " + Name, AllowSpew: AllowSpew))
{
throw new P4Exception("Couldn't describe existing label '{0}', output was:\n", Name, LabelSpec);
}
List<string> Lines = new List<string>(LabelSpec.Split('\n').Select(x => x.TrimEnd()));
// Find the description text, and remove it
int Idx = 0;
for(; Idx < Lines.Count; Idx++)
{
if(Lines[Idx].StartsWith("Description:"))
{
int EndIdx = Idx + 1;
while(EndIdx < Lines.Count && (Lines[EndIdx].Length == 0 || Char.IsWhiteSpace(Lines[EndIdx][0]) || Lines[EndIdx].IndexOf(':') == -1))
{
EndIdx++;
}
Lines.RemoveRange(Idx, EndIdx - Idx);
break;
}
}
// Insert the new description text
Lines.Insert(Idx, "Description: " + NewDescription.Replace("\n", "\n\t"));
LabelSpec = String.Join("\n", Lines);
// Update the label
LogP4("label -i", Input: LabelSpec, AllowSpew: AllowSpew);
}
/* Pattern to parse P4 changes command output. */
private static readonly Regex ChangesListOutputPattern = new Regex(@"^Change\s+(?<number>\d+)\s+.+$", RegexOptions.Compiled | RegexOptions.Multiline);
/// <summary>
/// Gets the latest CL number submitted to the depot. It equals to the @head.
/// </summary>
/// <returns>The head CL number.</returns>
public int GetLatestCLNumber()
{
CheckP4Enabled();
string Output;
if (!LogP4Output(out Output, "changes -s submitted -m1") || string.IsNullOrWhiteSpace(Output))
{
throw new InvalidOperationException("The depot should have at least one submitted changelist. Brand new depot?");
}
var Match = ChangesListOutputPattern.Match(Output);
if (!Match.Success)
{
throw new InvalidOperationException("The Perforce output is not in the expected format provided by 2014.1 documentation.");
}
return Int32.Parse(Match.Groups["number"].Value);
}
/* Pattern to parse P4 labels command output. */
static readonly Regex LabelsListOutputPattern = new Regex(@"^Label\s+(?<name>[\w\/\.-]+)\s+(?<date>\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2})\s+'(?<description>.+)'\s*$", RegexOptions.Compiled | RegexOptions.Multiline);
/// <summary>
/// Gets all labels satisfying given filter.
/// </summary>
/// <param name="Filter">Filter for label names.</param>
/// <param name="bCaseSensitive">Treat filter as case-sensitive.</param>
/// <returns></returns>
public P4Label[] GetLabels(string Filter, bool bCaseSensitive = true)
{
var LabelList = new List<P4Label>();
string Output;
if (P4Output(out Output, "labels -t " + (bCaseSensitive ? "-e" : "-E") + Filter, null, false))
{
foreach (Match LabelMatch in LabelsListOutputPattern.Matches(Output))
{
LabelList.Add(new P4Label(LabelMatch.Groups["name"].Value,
DateTime.ParseExact(
LabelMatch.Groups["date"].Value, "yyyy/MM/dd HH:mm:ss",
System.Globalization.CultureInfo.InvariantCulture)
));
}
}
return LabelList.ToArray();
}
/// <summary>
/// Validate label for some content.
/// </summary>
/// <returns>True if label exists and has at least one file tagged. False otherwise.</returns>
public bool ValidateLabelContent(string LabelName)
{
string Output;
if (P4Output(out Output, "files -m 1 @" + LabelName, null, false))
{
if (Output.StartsWith("//depot"))
{
// If it starts with depot path then label has at least one file tagged in it.
return true;
}
}
else
{
throw new InvalidOperationException("For some reason P4 files failed.");
}
return false;
}
/// <summary>
/// returns the full name of a label. //depot/UE4/TEST-GUBP-Promotable-GameName-CL-CLNUMBER
/// </summary>
/// <param name="BuildNamePrefix">Label Prefix</param>
public string FullLabelName(P4Environment Env, string BuildNamePrefix)
{
CheckP4Enabled();
var Label = Env.LabelPrefix + BuildNamePrefix + "-CL-" + Env.ChangelistString;
CommandUtils.Log("Label prefix {0}", BuildNamePrefix);
CommandUtils.Log("Full Label name {0}", Label);
return Label;
}
/// <summary>
/// Creates a downstream label.
/// </summary>
/// <param name="BuildNamePrefix">Label Prefix</param>
public void MakeDownstreamLabel(P4Environment Env, string BuildNamePrefix, List<string> Files = null)
{
CheckP4Enabled();
string DOWNSTREAM_LabelPrefix = CommandUtils.GetEnvVar("DOWNSTREAM_LabelPrefix");
if (!String.IsNullOrEmpty(DOWNSTREAM_LabelPrefix))
{
BuildNamePrefix = DOWNSTREAM_LabelPrefix;
}
if (String.IsNullOrEmpty(BuildNamePrefix))
{
throw new P4Exception("Need a downstream label");
}
{
CommandUtils.Log("Making downstream label");
var Label = FullLabelName(Env, BuildNamePrefix);
CommandUtils.Log("Deleting old label {0} (if any)...", Label);
DeleteLabel(Label, false);
CommandUtils.Log("Creating new label...");
CreateLabel(
Name: Label,
Description: "BVT Time " + CommandUtils.CmdEnv.TimestampAsString + " CL " + Env.ChangelistString,
Options: "unlocked noautoreload",
View: CommandUtils.CombinePaths(PathSeparator.Depot, Env.BuildRootP4, "...")
);
if (Files == null)
{
CommandUtils.Log("Adding all files to new label {0}...", Label);
LabelSync(Label, false);
}
else
{
CommandUtils.Log("Adding build products to new label {0}...", Label);
foreach (string LabelFile in Files)
{
LabelSync(Label, false, LabelFile);
}
}
}
}
/// <summary>
/// Creates a downstream label.
/// </summary>
/// <param name="BuildNamePrefix">Label Prefix</param>
public void MakeDownstreamLabelFromLabel(P4Environment Env, string BuildNamePrefix, string CopyFromBuildNamePrefix)
{
CheckP4Enabled();
string DOWNSTREAM_LabelPrefix = CommandUtils.GetEnvVar("DOWNSTREAM_LabelPrefix");
if (!String.IsNullOrEmpty(DOWNSTREAM_LabelPrefix))
{
BuildNamePrefix = DOWNSTREAM_LabelPrefix;
}
if (String.IsNullOrEmpty(BuildNamePrefix) || String.IsNullOrEmpty(CopyFromBuildNamePrefix))
{
throw new P4Exception("Need a downstream label");
}
{
CommandUtils.Log("Making downstream label");
var Label = FullLabelName(Env, BuildNamePrefix);
CommandUtils.Log("Deleting old label {0} (if any)...", Label);
DeleteLabel(Label, false);
CommandUtils.Log("Creating new label...");
CreateLabel(
Name: Label,
Description: "BVT Time " + CommandUtils.CmdEnv.TimestampAsString + " CL " + Env.ChangelistString,
Options: "unlocked noautoreload",
View: CommandUtils.CombinePaths(PathSeparator.Depot, Env.BuildRootP4, "...")
);
LabelToLabelSync(FullLabelName(Env, CopyFromBuildNamePrefix), Label, false);
}
}
/// <summary>
/// Given a file path in the depot, returns the local disk mapping for the current view
/// </summary>
/// <param name="DepotFile">The full file path in depot naming form</param>
/// <returns>The file's first reported path on disk or null if no mapping was found</returns>
public string DepotToLocalPath(string DepotFile, bool AllowSpew = true)
{
P4WhereRecord[] Records = Where(DepotFile, AllowSpew);
if (Records != null)
{
foreach (P4WhereRecord Record in Records)
{
if (!Record.bUnmap)
{
return Record.Path;
}
}
}
return null;
}
/// <summary>
/// Determines the mappings for a depot file in the workspace, without that file having to exist.
/// NOTE: This function originally allowed multiple depot paths at once. The "file(s) not in client view" messages are written to stderr
/// rather than stdout, and buffering them separately garbles the output when they're merged together.
/// </summary>
/// <param name="DepotFile">Depot path</param>
/// <param name="AllowSpew">Allows logging</param>
/// <returns>List of records describing the file's mapping. Usually just one, but may be more.</returns>
public P4WhereRecord[] Where(string DepotFile, bool AllowSpew = true)
{
CheckP4Enabled();
// P4 where outputs missing entries
string Command = String.Format("-z tag where \"{0}\"", DepotFile);
// Run the command.
string Output;
if (!LogP4Output(out Output, Command, AllowSpew: AllowSpew))
{
throw new P4Exception("p4.exe {0} failed.", Command);
}
// Copy the results into the local paths lookup. Entries may occur more than once, and entries may be missing from the client view, or deleted in the client view.
string[] Lines = Output.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
// Check for the file not existing
if(Lines.Length == 1 && Lines[0].EndsWith(" - file(s) not in client view."))
{
return null;
}
// Parse it into records
List<P4WhereRecord> Records = new List<P4WhereRecord>();
for (int LineIdx = 0; LineIdx < Lines.Length; )
{
P4WhereRecord Record = new P4WhereRecord();
// Parse an optional "... unmap"
if (Lines[LineIdx].Trim() == "... unmap")
{
Record.bUnmap = true;
LineIdx++;
}
// Parse "... depotFile <depot path>"
const string DepotFilePrefix = "... depotFile ";
if (LineIdx >= Lines.Length || !Lines[LineIdx].StartsWith(DepotFilePrefix))
{
throw new AutomationException("Unexpected output from p4 where: {0}", String.Join("\n", Lines.Skip(LineIdx)));
}
Record.DepotFile = Lines[LineIdx++].Substring(DepotFilePrefix.Length).Trim();
// Parse "... clientFile <client path>"
const string ClientFilePrefix = "... clientFile ";
if (LineIdx >= Lines.Length || !Lines[LineIdx].StartsWith(ClientFilePrefix))
{
throw new AutomationException("Unexpected output from p4 where: {0}", String.Join("\n", Lines.Skip(LineIdx)));
}
Record.ClientFile = Lines[LineIdx++].Substring(ClientFilePrefix.Length).Trim();
// Parse "... path <path to file>"
const string PathPrefix = "... path ";
if (LineIdx >= Lines.Length || !Lines[LineIdx].StartsWith(PathPrefix))
{
throw new AutomationException("Unexpected output from p4 where: {0}", String.Join("\n", Lines.Skip(LineIdx)));
}
Record.Path = Lines[LineIdx++].Substring(PathPrefix.Length).Trim();
// Add it to the output list
Records.Add(Record);
}
return Records.ToArray();
}
/// <summary>
/// Gets file stats.
/// </summary>
/// <param name="Filename">Filenam</param>
/// <returns>File stats (invalid if the file does not exist in P4)</returns>
public P4FileStat FStat(string Filename)
{
CheckP4Enabled();
string Output;
string Command = "fstat " + CommandUtils.MakePathSafeToUseWithCommandLine(Filename);
if (!LogP4Output(out Output, Command))
{
throw new P4Exception("p4.exe {0} failed.", Command);
}
P4FileStat Stat = P4FileStat.Invalid;
if (Output.Contains("no such file(s)") == false)
{
Output = Output.Replace("\r", "");
var FormLines = Output.Split('\n');
foreach (var Line in FormLines)
{
var StatAttribute = Line.StartsWith("... ") ? Line.Substring(4) : Line;
var StatPair = StatAttribute.Split(' ');
if (StatPair.Length == 2 && !String.IsNullOrEmpty(StatPair[1]))
{
switch (StatPair[0])
{
case "type":
// Use type (current CL if open) if possible
ParseFileType(StatPair[1], ref Stat);
break;
case "headType":
if (Stat.Type == P4FileType.Unknown)
{
ParseFileType(StatPair[1], ref Stat);
}
break;
case "action":
Stat.Action = ParseAction(StatPair[1]);
break;
case "change":
Stat.Change = StatPair[1];
break;
}
}
}
if (Stat.IsValid == false)
{
throw new AutomationException("Unable to parse fstat result for {0} (unknown file type).", Filename);
}
}
return Stat;
}
/// <summary>
/// Set file attributes (additively)
/// </summary>
/// <param name="Filename">File to change the attributes of.</param>
/// <param name="Attributes">Attributes to set.</param>
public void ChangeFileType(string Filename, P4FileAttributes Attributes, string Changelist = null)
{
CommandUtils.Log("ChangeFileType({0}, {1}, {2})", Filename, Attributes, String.IsNullOrEmpty(Changelist) ? "null" : Changelist);
var Stat = FStat(Filename);
if (String.IsNullOrEmpty(Changelist))
{
Changelist = (Stat.Action != P4Action.None) ? Stat.Change : "default";
}
// Only update attributes if necessary
if ((Stat.Attributes & Attributes) != Attributes)
{
var CmdLine = String.Format("{0} -c {1} -t {2} {3}",
(Stat.Action != P4Action.None) ? "reopen" : "open",
Changelist, FileAttributesToString(Attributes | Stat.Attributes), CommandUtils.MakePathSafeToUseWithCommandLine(Filename));
LogP4(CmdLine);
}
}
/// <summary>
/// Parses P4 forms and stores them as a key/value pairs.
/// </summary>
/// <param name="Output">P4 command output (must be a form).</param>
/// <returns>Parsed output.</returns>
public CaselessDictionary<string> ParseTaggedP4Output(string Output)
{
var Tags = new CaselessDictionary<string>();
var Lines = Output.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
string DelayKey = "";
int DelayIndex = 0;
foreach (var Line in Lines)
{
var TrimmedLine = Line.Trim();
if (TrimmedLine.StartsWith("#") == false)
{
if (DelayKey != "")
{
if (Line.StartsWith("\t"))
{
if (DelayIndex > 0)
{
Tags.Add(String.Format("{0}{1}", DelayKey, DelayIndex), TrimmedLine);
}
else
{
Tags.Add(DelayKey, TrimmedLine);
}
DelayIndex++;
continue;
}
DelayKey = "";
DelayIndex = 0;
}
var KeyEndIndex = TrimmedLine.IndexOf(':');
if (KeyEndIndex >= 0)
{
var Key = TrimmedLine.Substring(0, KeyEndIndex);
var Value = TrimmedLine.Substring(KeyEndIndex + 1).Trim();
if (Value == "")
{
DelayKey = Key;
}
else
{
Tags.Add(Key, Value);
}
}
}
}
return Tags;
}
/// <summary>
/// Checks if the client exists in P4.
/// </summary>
/// <param name="ClientName">Client name</param>
/// <returns>True if the client exists.</returns>
public bool DoesClientExist(string ClientName, bool Quiet = false)
{
CheckP4Enabled();
if(!Quiet)
{
CommandUtils.Log("Checking if client {0} exists", ClientName);
}
var P4Result = P4(String.Format("-c {0} where //...", ClientName), AllowSpew: false, WithClient: false);
return P4Result.Output.IndexOf("unknown - use 'client' command", StringComparison.InvariantCultureIgnoreCase) < 0 && P4Result.Output.IndexOf("doesn't exist", StringComparison.InvariantCultureIgnoreCase) < 0;
}
/// <summary>
/// Gets client info.
/// </summary>
/// <param name="ClientName">Name of the client.</param>
/// <returns></returns>
public P4ClientInfo GetClientInfo(string ClientName, bool Quiet = false)
{
CheckP4Enabled();
if(!Quiet)
{
CommandUtils.Log("Getting info for client {0}", ClientName);
}
if (!DoesClientExist(ClientName, Quiet))
{
return null;
}
return GetClientInfoInternal(ClientName);
}
/// <summary>
/// Parses a string with enum values separated with spaces.
/// </summary>
/// <param name="ValueText"></param>
/// <param name="EnumType"></param>
/// <returns></returns>
private static object ParseEnumValues(string ValueText, Type EnumType)
{
ValueText = ValueText.Replace(' ', ',');
return Enum.Parse(EnumType, ValueText, true);
}
/// <summary>
/// Gets client info (does not check if the client exists)
/// </summary>
/// <param name="ClientName">Name of the client.</param>
/// <returns></returns>
public P4ClientInfo GetClientInfoInternal(string ClientName)
{
P4ClientInfo Info = new P4ClientInfo();
var P4Result = P4(String.Format("client -o {0}", ClientName), AllowSpew: false, WithClient: false);
if (P4Result == 0)
{
var Tags = ParseTaggedP4Output(P4Result.Output);
Info.Name = ClientName;
Tags.TryGetValue("Host", out Info.Host);
Tags.TryGetValue("Root", out Info.RootPath);
if (!String.IsNullOrEmpty(Info.RootPath))
{
Info.RootPath = CommandUtils.ConvertSeparators(PathSeparator.Default, Info.RootPath);
}
Tags.TryGetValue("Owner", out Info.Owner);
string AccessTime;
Tags.TryGetValue("Access", out AccessTime);
if (!String.IsNullOrEmpty(AccessTime))
{
DateTime.TryParse(AccessTime, out Info.Access);
}
else
{
Info.Access = DateTime.MinValue;
}
string LineEnd;
Tags.TryGetValue("LineEnd", out LineEnd);
if (!String.IsNullOrEmpty(LineEnd))
{
Info.LineEnd = (P4LineEnd)ParseEnumValues(LineEnd, typeof(P4LineEnd));
}
string ClientOptions;
Tags.TryGetValue("Options", out ClientOptions);
if (!String.IsNullOrEmpty(ClientOptions))
{
Info.Options = (P4ClientOption)ParseEnumValues(ClientOptions, typeof(P4ClientOption));
}
string SubmitOptions;
Tags.TryGetValue("SubmitOptions", out SubmitOptions);
if (!String.IsNullOrEmpty(SubmitOptions))
{
Info.SubmitOptions = (P4SubmitOption)ParseEnumValues(SubmitOptions, typeof(P4SubmitOption));
}
string ClientMappingRoot = "//" + ClientName;
foreach (var Pair in Tags)
{
if (Pair.Key.StartsWith("View", StringComparison.InvariantCultureIgnoreCase))
{
string Mapping = Pair.Value;
int ClientStartIndex = Mapping.IndexOf(ClientMappingRoot, StringComparison.InvariantCultureIgnoreCase);
if (ClientStartIndex > 0)
{
var ViewPair = new KeyValuePair<string, string>(
Mapping.Substring(0, ClientStartIndex - 1),
Mapping.Substring(ClientStartIndex + ClientMappingRoot.Length));
Info.View.Add(ViewPair);
}
}
}
}
else
{
throw new AutomationException("p4 client -o {0} failed!", ClientName);
}
return Info;
}
/// <summary>
/// Gets all clients owned by the user.
/// </summary>
/// <param name="UserName"></param>
/// <returns>List of clients owned by the user.</returns>
public P4ClientInfo[] GetClientsForUser(string UserName, string PathUnderClientRoot = null)
{
CheckP4Enabled();
var ClientList = new List<P4ClientInfo>();
// Get all clients for this user
var P4Result = P4(String.Format("clients -u {0}", UserName), AllowSpew: false, WithClient: false);
if (P4Result != 0)
{
throw new AutomationException("p4 clients -u {0} failed.", UserName);
}
// Parse output.
var Lines = P4Result.Output.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
foreach (string Line in Lines)
{
var Tokens = Line.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
P4ClientInfo Info = null;
// Retrieve the client name and info.
for (int TokenIndex = 0; TokenIndex < Tokens.Length; ++TokenIndex)
{
if (Tokens[TokenIndex] == "Client")
{
var ClientName = Tokens[++TokenIndex];
Info = GetClientInfoInternal(ClientName);
break;
}
}
if (Info == null || String.IsNullOrEmpty(Info.Name) || String.IsNullOrEmpty(Info.RootPath))
{
throw new AutomationException("Failed to retrieve p4 client info for user {0}. Unable to set up local environment", UserName);
}
bool bAddClient = true;
// Filter the client out if the specified path is not under the client root
if (!String.IsNullOrEmpty(PathUnderClientRoot) && !String.IsNullOrEmpty(Info.RootPath))
{
var ClientRootPathWithSlash = Info.RootPath;
if (!ClientRootPathWithSlash.EndsWith("\\") && !ClientRootPathWithSlash.EndsWith("/"))
{
ClientRootPathWithSlash = CommandUtils.ConvertSeparators(PathSeparator.Default, ClientRootPathWithSlash + "/");
}
bAddClient = PathUnderClientRoot.StartsWith(ClientRootPathWithSlash, StringComparison.CurrentCultureIgnoreCase);
}
if (bAddClient)
{
ClientList.Add(Info);
}
}
return ClientList.ToArray();
}
/// <summary>
/// Deletes a client.
/// </summary>
/// <param name="Name">Client name.</param>
/// <param name="Force">Forces the operation (-f)</param>
public void DeleteClient(string Name, bool Force = false)
{
CheckP4Enabled();
LogP4(String.Format("client -d {0} {1}", (Force ? "-f" : ""), Name), WithClient: false);
}
/// <summary>
/// Creates a new client.
/// </summary>
/// <param name="ClientSpec">Client specification.</param>
/// <returns></returns>
public P4ClientInfo CreateClient(P4ClientInfo ClientSpec, bool AllowSpew = true)
{
string SpecInput = "Client: " + ClientSpec.Name + Environment.NewLine;
SpecInput += "Owner: " + ClientSpec.Owner + Environment.NewLine;
SpecInput += "Host: " + ClientSpec.Host + Environment.NewLine;
SpecInput += "Root: " + ClientSpec.RootPath + Environment.NewLine;
SpecInput += "Options: " + ClientSpec.Options.ToString().ToLowerInvariant().Replace(",", "") + Environment.NewLine;
SpecInput += "SubmitOptions: " + ClientSpec.SubmitOptions.ToString().ToLowerInvariant().Replace(",", "") + Environment.NewLine;
SpecInput += "LineEnd: " + ClientSpec.LineEnd.ToString().ToLowerInvariant() + Environment.NewLine;
SpecInput += "View:" + Environment.NewLine;
foreach (var Mapping in ClientSpec.View)
{
SpecInput += "\t" + Mapping.Key + " //" + ClientSpec.Name + Mapping.Value + Environment.NewLine;
}
if(AllowSpew) CommandUtils.Log(SpecInput);
LogP4("client -i", SpecInput, AllowSpew: AllowSpew, WithClient: false);
return ClientSpec;
}
/// <summary>
/// Lists immediate sub-directories of the specified directory.
/// </summary>
/// <param name="CommandLine"></param>
/// <returns>List of sub-directories of the specified direcories.</returns>
public List<string> Dirs(string CommandLine)
{
CheckP4Enabled();
var DirsCmdLine = String.Format("dirs {0}", CommandLine);
var P4Result = P4(DirsCmdLine, AllowSpew: false);
if (P4Result != 0)
{
throw new AutomationException("{0} failed.", DirsCmdLine);
}
var Result = new List<string>();
var Lines = P4Result.Output.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
foreach (string Line in Lines)
{
if (!Line.Contains("no such file"))
{
Result.Add(Line);
}
}
return Result;
}
/// <summary>
/// Gets the contents of a particular file in the depot without syncing it
/// </summary>
/// <param name="DepotPath">Depot path to the file (with revision/range if necessary)</param>
/// <returns>Contents of the file</returns>
public string Print(string DepotPath, bool AllowSpew = true)
{
string Output;
if(!P4Output(out Output, "print -q " + DepotPath, AllowSpew: AllowSpew, WithClient: false))
{
throw new AutomationException("p4 print {0} failed", DepotPath);
}
if(!Output.Trim().Contains("\n") && Output.Contains("no such file(s)"))
{
throw new AutomationException("p4 print {0} failed", DepotPath);
}
return Output;
}
#region Utilities
private static object[] OldStyleBinaryFlags = new object[]
{
P4FileAttributes.Uncompressed,
P4FileAttributes.Executable,
P4FileAttributes.Compressed,
P4FileAttributes.RCS
};
private static void ParseFileType(string Filetype, ref P4FileStat Stat)
{
var AllFileTypes = GetEnumValuesAndKeywords(typeof(P4FileType));
var AllAttributes = GetEnumValuesAndKeywords(typeof(P4FileAttributes));
Stat.Type = P4FileType.Unknown;
Stat.Attributes = P4FileAttributes.None;
// Parse file flags
var OldFileFlags = GetEnumValuesAndKeywords(typeof(P4FileAttributes), OldStyleBinaryFlags);
foreach (var FileTypeFlag in OldFileFlags)
{
if ((!String.IsNullOrEmpty(FileTypeFlag.Value) && Char.ToLowerInvariant(FileTypeFlag.Value[0]) == Char.ToLowerInvariant(Filetype[0]))
// @todo: This is a nasty hack to get .ipa files to work - RobM plz fix?
|| (FileTypeFlag.Value == "F" && Filetype == "ubinary"))
{
Stat.IsOldType = true;
Stat.Attributes |= (P4FileAttributes)FileTypeFlag.Key;
break;
}
}
if (Stat.IsOldType)
{
Filetype = Filetype.Substring(1);
}
// Parse file type
var TypeAndAttributes = Filetype.Split('+');
foreach (var FileType in AllFileTypes)
{
if (FileType.Value == TypeAndAttributes[0])
{
Stat.Type = (P4FileType)FileType.Key;
break;
}
}
// Parse attributes
if (TypeAndAttributes.Length > 1 && !String.IsNullOrEmpty(TypeAndAttributes[1]))
{
var FileAttributes = TypeAndAttributes[1];
for (int AttributeIndex = 0; AttributeIndex < FileAttributes.Length; ++AttributeIndex)
{
char Attr = FileAttributes[AttributeIndex];
foreach (var FileAttribute in AllAttributes)
{
if (!String.IsNullOrEmpty(FileAttribute.Value) && FileAttribute.Value[0] == Attr)
{
Stat.Attributes |= (P4FileAttributes)FileAttribute.Key;
break;
}
}
}
}
}
private static P4Action ParseAction(string Action)
{
P4Action Result = P4Action.Unknown;
var AllActions = GetEnumValuesAndKeywords(typeof(P4Action));
foreach (var ActionKeyword in AllActions)
{
if (ActionKeyword.Value == Action)
{
Result = (P4Action)ActionKeyword.Key;
break;
}
}
return Result;
}
private static KeyValuePair<object, string>[] GetEnumValuesAndKeywords(Type EnumType)
{
var Values = Enum.GetValues(EnumType);
KeyValuePair<object, string>[] ValuesAndKeywords = new KeyValuePair<object, string>[Values.Length];
int ValueIndex = 0;
foreach (var Value in Values)
{
ValuesAndKeywords[ValueIndex++] = new KeyValuePair<object, string>(Value, GetEnumDescription(EnumType, Value));
}
return ValuesAndKeywords;
}
private static KeyValuePair<object, string>[] GetEnumValuesAndKeywords(Type EnumType, object[] Values)
{
KeyValuePair<object, string>[] ValuesAndKeywords = new KeyValuePair<object, string>[Values.Length];
int ValueIndex = 0;
foreach (var Value in Values)
{
ValuesAndKeywords[ValueIndex++] = new KeyValuePair<object, string>(Value, GetEnumDescription(EnumType, Value));
}
return ValuesAndKeywords;
}
private static string GetEnumDescription(Type EnumType, object Value)
{
var MemberInfo = EnumType.GetMember(Value.ToString());
var Atributes = MemberInfo[0].GetCustomAttributes(typeof(DescriptionAttribute), false);
return ((DescriptionAttribute)Atributes[0]).Description;
}
private static string FileAttributesToString(P4FileAttributes Attributes)
{
var AllAttributes = GetEnumValuesAndKeywords(typeof(P4FileAttributes));
string Text = "";
foreach (var Attr in AllAttributes)
{
var AttrValue = (P4FileAttributes)Attr.Key;
if ((Attributes & AttrValue) == AttrValue)
{
Text += Attr.Value;
}
}
if (String.IsNullOrEmpty(Text) == false)
{
Text = "+" + Text;
}
return Text;
}
#endregion
}
}