// Copyright Epic Games, Inc. All Rights Reserved.
using EpicGames.Core;
using EpicGames.Perforce;
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;
using System.Reflection;
using System.Collections;
using UnrealBuildBase;
using Microsoft.Extensions.Logging;
using static AutomationTool.CommandUtils;
namespace AutomationTool
{
///
/// Declares that the command type requires P4Environment.
///
public class RequireP4Attribute : Attribute
{
}
///
/// Declares that the command type does not access Changelist or CodeChangelist from P4Environment.
///
public class DoesNotNeedP4CLAttribute : Attribute
{
}
public class P4Exception : AutomationException
{
public P4Exception(string Msg)
: base(Msg) { }
public P4Exception(string Msg, Exception InnerException)
: base(InnerException, Msg) { }
public P4Exception(string Format, params object[] Args)
: base(Format, Args) { }
}
public enum P4LineEnd
{
Local = 0,
Unix = 1,
Mac = 2,
Win = 3,
Share = 4,
}
[Flags]
public enum P4SubmitOption
{
SubmitUnchanged = 1,
RevertUnchanged = 2,
LeaveUnchanged = 4,
Reopen = 8
}
[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,
NoAltSync = 4096,
AltSync = 8192,
}
public class P4ClientInfo
{
public string Name;
public string RootPath;
public string Host;
public string Owner;
public string Stream;
public DateTime Access;
public P4LineEnd LineEnd;
public P4ClientOption Options;
public P4SubmitOption SubmitOptions = P4SubmitOption.SubmitUnchanged;
public List> View = new List>();
public bool Matches(P4ClientInfo Other)
{
return Name == Other.Name
&& RootPath == Other.RootPath
&& Host == Other.Host
&& Owner == Other.Owner
&& Stream == Other.Stream
&& LineEnd == Other.LineEnd
&& Options == Other.Options
&& SubmitOptions == Other.SubmitOptions
&& (!String.IsNullOrEmpty(Stream) || 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,
[Description("utf8")]
Utf8,
}
[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 P4HaveRecord
{
public string DepotFile;
public string ClientFile;
public int Revision;
public P4HaveRecord(string DepotFile, string ClientFile, int Revision)
{
this.DepotFile = DepotFile;
this.ClientFile = ClientFile;
this.Revision = Revision;
}
}
public class P4Spec
{
public List> Sections;
///
/// Default constructor.
///
public P4Spec()
{
Sections = new List>();
}
///
/// Gets the current value of a field with the given name
///
/// Name of the field to search for
/// The value of the field, or null if it does not exist
public string GetField(string Name)
{
int Idx = Sections.FindIndex(x => x.Key == Name);
return (Idx == -1)? null : Sections[Idx].Value;
}
///
/// Sets the value of an existing field, or adds a new one with the given name
///
/// Name of the field to set
/// New value of the field
public void SetField(string Name, string Value)
{
int Idx = Sections.FindIndex(x => x.Key == Name);
if(Idx == -1)
{
Sections.Add(new KeyValuePair(Name, Value));
}
else
{
Sections[Idx] = new KeyValuePair(Name, Value);
}
}
///
/// Parses a spec (clientspec, branchspec, changespec) from an array of lines
///
/// Text split into separate lines
/// Array of section names and values
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(SectionName, Value.ToString().TrimEnd()));
}
}
return Spec;
}
///
/// Formats a P4 specification as a block of text
///
///
public override string ToString()
{
StringBuilder Result = new StringBuilder();
foreach(KeyValuePair 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();
}
}
///
/// Describes the action performed by the user when resolving the integration
///
public enum P4IntegrateAction
{
///
/// file did not previously exist; it was created as a copy of partner-file
///
[Description("branch from")]
BranchFrom,
///
/// partner-file did not previously exist; it was created as a copy of file.
///
[Description("branch into")]
BranchInto,
///
/// file was integrated from partner-file, accepting merge.
///
[Description("merge from")]
MergeFrom,
///
/// file was integrated into partner-file, accepting merge.
///
[Description("merge into")]
MergeInto,
///
/// file was integrated from partner-file, accepting theirs and deleting the original.
///
[Description("moved from")]
MovedFrom,
///
/// file was integrated into partner-file, accepting theirs and creating partner-file if it did not previously exist.
///
[Description("moved into")]
MovedInto,
///
/// file was integrated from partner-file, accepting theirs.
///
[Description("copy from")]
CopyFrom,
///
/// file was integrated into partner-file, accepting theirs.
///
[Description("copy into")]
CopyInto,
///
/// file was integrated from partner-file, accepting yours.
///
[Description("ignored")]
Ignored,
///
/// file was integrated into partner-file, accepting yours.
///
[Description("ignored by")]
IgnoredBy,
///
/// file was integrated from partner-file, and partner-file had been previously deleted.
///
[Description("delete from")]
DeleteFrom,
///
/// file was integrated into partner-file, and file had been previously deleted.
///
[Description("delete into")]
DeleteInto,
///
/// file was integrated from partner-file, and file was edited within the p4 resolve process.
///
[Description("edit from")]
EditFrom,
///
/// file was integrated into partner-file, and partner-file was reopened for edit before submission.
///
[Description("edit into")]
EditInto,
///
/// file was integrated from a deleted partner-file, and partner-file was reopened for add (that is, someone restored a deleted file by syncing back to a pre-deleted revision and adding the file).
///
[Description("add from")]
AddFrom,
///
/// file was integrated into previously nonexistent partner-file, and partner-file was reopened for add before submission.
///
[Description("add into")]
AddInto,
///
/// file was reverted to a previous revision
///
[Description("undid")]
Undid,
///
/// file was reverted to a previous revision
///
[Description("undone by")]
UndoneBy
}
///
/// Stores integration information for a file revision
///
public class P4IntegrationRecord
{
///
/// The integration action performed for this file
///
public readonly P4IntegrateAction Action;
///
/// The partner file for this integration
///
public readonly string OtherFile;
///
/// Min revision of the partner file for this integration
///
public readonly int StartRevisionNumber;
///
/// Max revision of the partner file for this integration
///
public readonly int EndRevisionNumber;
///
/// Constructor
///
/// The integration action
/// The partner file involved in the integration
/// Starting revision of the partner file for the integration (exclusive)
/// Ending revision of the partner file for the integration (inclusive)
public P4IntegrationRecord(P4IntegrateAction Action, string OtherFile, int StartRevisionNumber, int EndRevisionNumber)
{
this.Action = Action;
this.OtherFile = OtherFile;
this.StartRevisionNumber = StartRevisionNumber;
this.EndRevisionNumber = EndRevisionNumber;
}
///
/// Summarize this record for display in the debugger
///
/// Formatted integration record
public override string ToString()
{
if(StartRevisionNumber + 1 == EndRevisionNumber)
{
return String.Format("{0} {1}#{2}", Action, OtherFile, EndRevisionNumber);
}
else
{
return String.Format("{0} {1}#{2},#{3}", Action, OtherFile, StartRevisionNumber + 1, EndRevisionNumber);
}
}
}
///
/// Stores a revision record for a file
///
public class P4RevisionRecord
{
///
/// The revision number of this file
///
public readonly int RevisionNumber;
///
/// The changelist responsible for this revision of the file
///
public readonly int ChangeNumber;
///
/// Action performed to the file in this revision
///
public readonly P4Action Action;
///
/// Type of the file
///
public readonly string Type;
///
/// Timestamp of this modification
///
public readonly DateTime DateTime;
///
/// Author of the changelist
///
public readonly string UserName;
///
/// Client that submitted this changelist
///
public readonly string ClientName;
///
/// Size of the file, or -1 if not specified
///
public readonly long FileSize;
///
/// Digest of the file, or null if not specified
///
public readonly string Digest;
///
/// Description of this changelist
///
public readonly string Description;
///
/// Integration records for this revision
///
public readonly P4IntegrationRecord[] Integrations;
///
/// Constructor
///
/// Revision number of the file
/// Number of the changelist that submitted this revision
/// Action performed to the file in this changelist
/// Type of the file
/// Timestamp for the change
/// User that submitted the change
/// Client that submitted the change
/// Size of the file, or -1 if not specified
/// Digest of the file, or null if not specified
/// Description of the changelist
/// Integrations performed to the file
public P4RevisionRecord(int RevisionNumber, int ChangeNumber, P4Action Action, string Type, DateTime DateTime, string UserName, string ClientName, long FileSize, string Digest, string Description, P4IntegrationRecord[] Integrations)
{
this.RevisionNumber = RevisionNumber;
this.ChangeNumber = ChangeNumber;
this.Action = Action;
this.Type = Type;
this.DateTime = DateTime;
this.UserName = UserName;
this.ClientName = ClientName;
this.Description = Description;
this.FileSize = FileSize;
this.Digest = Digest;
this.Integrations = Integrations;
}
///
/// Format this record for display in the debugger
///
/// Summary of this revision
public override string ToString()
{
return String.Format("#{0} change {1} {2} on {3} by {4}@{5}", RevisionNumber, ChangeNumber, Action, DateTime, UserName, ClientName);
}
}
///
/// Record output by the filelog command
///
public class P4FileRecord
{
///
/// Path to the file in the depot
///
public string DepotPath;
///
/// Revisions of this file
///
public P4RevisionRecord[] Revisions;
///
/// Constructor
///
/// The depot path of the file
/// Revisions of the file
public P4FileRecord(string DepotPath, P4RevisionRecord[] Revisions)
{
this.DepotPath = DepotPath;
this.Revisions = Revisions;
}
///
/// Return the depot path of the file for display in the debugger
///
/// Path to the file
public override string ToString()
{
return DepotPath;
}
}
///
/// Options for the filelog command
///
[Flags]
public enum P4FileLogOptions
{
///
/// No options
///
None = 0,
///
/// Display file content history instead of file name history.
///
ContentHistory = 1,
///
/// Follow file history across branches.
///
FollowAcrossBranches = 2,
///
/// List long output, with the full text of each changelist description.
///
FullDescriptions = 4,
///
/// List long output, with the full text of each changelist description truncated at 250 characters.
///
LongDescriptions = 8,
///
/// When used with the ContentHistory option, do not follow content of promoted task streams.
///
DoNotFollowPromotedTaskStreams = 16,
///
/// Display a shortened form of output by ignoring non-contributory integrations
///
IgnoreNonContributoryIntegrations = 32,
}
///
/// Type of a Perforce stream
///
public enum P4StreamType
{
///
/// A mainline stream
///
Mainline,
///
/// A development stream
///
Development,
///
/// A release stream
///
Release,
///
/// A virtual stream
///
Virtual,
///
/// A task stream
///
Task,
}
///
/// Options for a stream definition
///
[Flags]
public enum P4StreamOptions
{
///
/// The stream is locked
///
Locked = 1,
///
/// Only the owner may submit to the stream
///
OwnerSubmit = 4,
///
/// Integrations from this stream to its parent are expected
///
ToParent = 4,
///
/// Integrations from this stream from its parent are expected
///
FromParent = 8,
///
/// Undocumented?
///
MergeDown = 16,
}
///
/// Contains information about a stream, as returned by the 'p4 streams' command
///
[DebuggerDisplay("{Stream}")]
public class P4StreamRecord
{
///
/// Path to the stream
///
public string Stream;
///
/// Last time the stream definition was updated
///
public DateTime Update;
///
/// Last time the stream definition was accessed
///
public DateTime Access;
///
/// Owner of this stream
///
public string Owner;
///
/// Name of the stream. This may be modified after the stream is initially created, but it's underlying depot path will not change.
///
public string Name;
///
/// The parent stream
///
public string Parent;
///
/// Type of the stream
///
public P4StreamType Type;
///
/// User supplied description of the stream
///
public string Description;
///
/// Options for the stream definition
///
public P4StreamOptions Options;
///
/// Whether this stream is more stable than the parent stream
///
public Nullable FirmerThanParent;
///
/// Whether changes from this stream flow to the parent stream
///
public bool ChangeFlowsToParent;
///
/// Whether changes from this stream flow from the parent stream
///
public bool ChangeFlowsFromParent;
///
/// The mainline branch associated with this stream
///
public string BaseParent;
///
/// Constructor
///
/// Path to the stream
/// Last time the stream definition was updated
/// Last time the stream definition was accessed
/// Owner of this stream
/// Name of the stream. This may be modified after the stream is initially created, but it's underlying depot path will not change.
/// The parent stream
/// Type of the stream
/// User supplied description of the stream
/// Options for the stream definition
/// Whether this stream is more stable than the parent stream
/// Whether changes from this stream flow to the parent stream
/// Whether changes from this stream flow from the parent stream
/// The mainline branch associated with this stream
public P4StreamRecord(string Stream, DateTime Update, DateTime Access, string Owner, string Name, string Parent, P4StreamType Type, string Description, P4StreamOptions Options, Nullable FirmerThanParent, bool ChangeFlowsToParent, bool ChangeFlowsFromParent, string BaseParent)
{
this.Stream = Stream;
this.Update = Update;
this.Owner = Owner;
this.Name = Name;
this.Parent = Parent;
this.Type = Type;
this.Description = Description;
this.Options = Options;
this.FirmerThanParent = FirmerThanParent;
this.ChangeFlowsToParent = ChangeFlowsToParent;
this.ChangeFlowsFromParent = ChangeFlowsFromParent;
this.BaseParent = BaseParent;
}
///
/// Return the path of this stream for display in the debugger
///
/// Path to this stream
public override string ToString()
{
return Stream;
}
}
///
/// Error severity codes. Taken from the p4java documentation.
///
public enum P4SeverityCode
{
Empty = 0,
Info = 1,
Warning = 2,
Failed = 3,
Fatal = 4,
}
///
/// Generic error codes that can be returned by the Perforce server. Taken from the p4java documentation.
///
public enum P4GenericCode
{
None = 0,
Usage = 1,
Unknown = 2,
Context = 3,
Illegal = 4,
NotYet = 5,
Protect = 6,
Empty = 17,
Fault = 33,
Client = 34,
Admin = 35,
Config = 36,
Upgrade = 37,
Comm = 38,
TooBig = 39,
}
///
/// Represents a error return value from Perforce.
///
public class P4ReturnCode
{
///
/// The value of the "code" field returned by the server
///
public string Code;
///
/// The severity of this error
///
public P4SeverityCode Severity;
///
/// The generic error code associated with this message
///
public P4GenericCode Generic;
///
/// The message text
///
public string Message;
///
/// Constructor
///
/// The value of the "code" field returned by the server
/// The severity of this error
/// The generic error code associated with this message
/// The message text
public P4ReturnCode(string Code, P4SeverityCode Severity, P4GenericCode Generic, string Message)
{
this.Code = Code;
this.Severity = Severity;
this.Generic = Generic;
this.Message = Message;
}
///
/// Formats this error for display in the debugger
///
/// String representation of this object
public override string ToString()
{
return String.Format("{0}: {1} (Generic={2})", Code, Message, Generic);
}
}
public partial class CommandUtils
{
static private P4Connection PerforceConnection;
static private P4Environment PerforceEnvironment;
static private IPerforceSettings PerforceSettings;
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;
}
}
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;
}
}
static public IPerforceSettings P4Settings
{
get
{
if (PerforceSettings == null)
{
throw new AutomationException("Attempt to use P4Settings before it was initialized or P4 support is disabled.");
}
return PerforceSettings;
}
}
///
/// Initializes build environment. If the build command needs a specific env-var mapping or
/// has an extended BuildEnvironment, it must implement this method accordingly.
///
static internal void InitP4Environment()
{
// Temporary connection - will use only the currently set env vars to connect to P4
PerforceEnvironment = new P4Environment(CmdEnv);
PerforceSettings Settings = new PerforceSettings(PerforceEnvironment.ServerAndPort, PerforceEnvironment.User);
Settings.PreferNativeClient = true;
Settings.ClientName = PerforceEnvironment.Client;
PerforceSettings = Settings;
}
///
/// Initializes default source control connection.
///
static internal void InitDefaultP4Connection()
{
PerforceConnection = new P4Connection(User: P4Env.User, Client: P4Env.Client, ServerAndPort: P4Env.ServerAndPort);
}
///
/// Check if P4 is supported.
///
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;
///
/// Check if P4CL is required.
///
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;
///
/// Checks whether commands are allowed to submit files into P4.
///
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;
///
/// Sets up P4Enabled, AllowSubmit properties. Note that this does not initialize P4 environment.
///
/// Commands to execute
/// Commands
internal static void InitP4Support(List CommandsToExecute, Dictionary 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.
Logger.LogDebug("Initializing AllowSubmit.");
if (GlobalCommandLine.Submit || GlobalCommandLine.NoSubmit)
{
AllowSubmit = GlobalCommandLine.Submit;
}
else
{
AllowSubmit = Automation.IsBuildMachine;
}
Logger.LogDebug("AllowSubmit={AllowSubmit}", AllowSubmit);
// Init P4Enabled
Logger.LogDebug("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;
}
Logger.LogDebug("P4Enabled={P4Enabled}", P4Enabled);
Logger.LogDebug("P4CLRequired={P4CLRequired}", P4CLRequired);
}
///
/// Checks if any of the commands to execute has [RequireP4] attribute.
///
/// List of commands to be executed.
/// Commands.
///
///
private static void CheckIfCommandsRequireP4(List CommandsToExecute, Dictionary 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))
{
if(!GlobalCommandLine.P4)
{
Logger.LogInformation("Command {CommandName} requires P4 functionality.", Command.Name);
}
bRequireP4 = true;
var DoesNotNeedP4CLAttributes = Command.GetCustomAttributes(typeof(DoesNotNeedP4CLAttribute), true);
if (CommandUtils.IsNullOrEmpty(DoesNotNeedP4CLAttributes))
{
bRequireCL = true;
}
}
}
}
}
}
///
/// Class that stores labels info.
///
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;
}
}
///
/// Perforce connection.
///
public partial class P4Connection
{
///
/// List of global options for this connection (client/user)
///
private string GlobalOptions;
///
/// List of global options for this connection (client/user)
///
private string GlobalOptionsWithoutClient;
///
/// Path where this connection's log is to go to
///
public string LogPath { get; private set; }
///
/// Initializes P4 connection
///
/// Username (can be null, in which case the environment variable default will be used)
/// Workspace (can be null, in which case the environment variable default will be used)
/// Server:Port (can be null, in which case the environment variable default will be used)
/// Log filename (can be null, in which case CmdEnv.LogFolder/p4.log will be used)
/// Additional global options to include on every p4 command line
public P4Connection(string User, string Client, string ServerAndPort = null, string P4LogPath = null, string AdditionalOpts = null)
{
var UserOpts = String.IsNullOrEmpty(User) ? "" : ("-u" + User + " ");
var ClientOpts = String.IsNullOrEmpty(Client) ? "" : ("-c" + Client + " ");
var ServerOpts = String.IsNullOrEmpty(ServerAndPort) ? "" : ("-p" + ServerAndPort + " ");
AdditionalOpts = String.IsNullOrEmpty(AdditionalOpts) ? "" : AdditionalOpts + " ";
GlobalOptions = UserOpts + ClientOpts + ServerOpts + AdditionalOpts;
GlobalOptionsWithoutClient = UserOpts + ServerOpts + AdditionalOpts;
if (P4LogPath == null)
{
LogPath = CommandUtils.CombinePaths(CommandUtils.CmdEnv.LogFolder, String.Format("p4.log", Client));
}
else
{
LogPath = P4LogPath;
}
}
///
/// A filter that suppresses all output od stdout/stderr
///
///
///
static string NoSpewFilter(string Message)
{
return null;
}
///
/// Shortcut to Run but with P4.exe as the program name.
///
/// Command line
/// Stdin
/// true for spew
///
///
/// Exit code
public IProcessResult P4(string CommandLine, string Input = null, bool AllowSpew = true, bool WithClient = true, bool SpewIsVerbose = false)
{
return P4("", CommandLine, Input, AllowSpew, WithClient, SpewIsVerbose);
}
///
/// Shortcut to Run but with P4.exe as the program name.
///
/// Extra global options just for this command
/// Command line
/// Stdin
/// true for spew
///
///
/// Exit code
public IProcessResult P4(string ExtraGlobalOptions, string CommandLine, string Input, bool AllowSpew = true, bool WithClient = true, bool SpewIsVerbose = false)
{
CommandLine = CommandLine.Trim();
// we need the first token to be a command ("files") and not a global option ("-c foo")
if (CommandLine.StartsWith("-"))
{
throw new AutomationException("Fix your call to P4 to put global options into the GlobalOptions parameter. The first token should be a p4 command: {0}", CommandLine);
}
CommandUtils.ERunOptions RunOptions = AllowSpew ? CommandUtils.ERunOptions.AllowSpew : CommandUtils.ERunOptions.NoLoggingOfRunCommand;
if( SpewIsVerbose )
{
RunOptions |= CommandUtils.ERunOptions.SpewIsVerbose;
}
var SpewDelegate = AllowSpew ? null : new ProcessResult.SpewFilterCallbackType(NoSpewFilter);
IProcessResult Result;
// if there's a star anywhere in the commandline, p4.exe, when parsing command line on Windows, will internally perform a find-files to expand the *,
// and Windows thinks a p4 path is a UNC path (//Depot/Stream/Foo/*/bar.txt). It has been seen that this can stall for for seconds (over 20
// seconds potentially). This can be seen with just "dir \\fake\server" on a Windows command prompt, and some machines will take forever
if (CommandLine.Contains("*"))
{
// we can bypass the problem by putting the params for the command into a file, and using the -x optjon. So:
// p4 -c clientspec files //Depot/Stream/Foo/*/bar.txt
// would be converted to this awkward format (with params BEFORE the command):
// p4 -c clientspec -x tempfile.txt files
// where tempfile.txt contains:
// //Depot/Stream/Foo/*/bar.txt
// pull the command out ("files" in the above case)
string CommandToken = CommandLine.Trim().Split(" ".ToCharArray())[0];
// make a temp file, and write the params, minus the command, to it
string ParamsFile = Path.Combine(Path.GetTempPath(), Path.GetTempFileName());
File.WriteAllText(ParamsFile, CommandLine.Substring(CommandToken.Length + 1));
// run with -x
string FinalCommandline = string.Format("{0} {1} -x \"{3}\" {2}", (WithClient ? GlobalOptions : GlobalOptionsWithoutClient), ExtraGlobalOptions, CommandToken, ParamsFile).Trim();
Result = CommandUtils.Run(HostPlatform.Current.P4Exe, FinalCommandline, Input, Options: RunOptions, SpewFilterCallback: SpewDelegate);
// delete the temp file
File.Delete(ParamsFile);
}
else
{
string FinalCommandline = string.Format("{0} {1} {2}", (WithClient ? GlobalOptions : GlobalOptionsWithoutClient), ExtraGlobalOptions, CommandLine);
Result = CommandUtils.Run(HostPlatform.Current.P4Exe, FinalCommandline, Input, Options: RunOptions, SpewFilterCallback: SpewDelegate);
}
return Result;
}
///
/// Calls p4 and returns the output.
///
/// Output of the command.
///
/// Commandline for p4.
/// Stdin input.
/// Whether the command should spew.
///
/// True if succeeded, otherwise false.
public bool P4Output(out string Output, string ExtraGlobalOptions, string CommandLine, string Input = null, bool AllowSpew = true, bool WithClient = true)
{
Output = "";
var Result = P4(ExtraGlobalOptions, CommandLine, Input, AllowSpew, WithClient);
Output = Result.Output;
return Result.ExitCode == 0;
}
///
/// Calls p4 and returns the output.
///
/// Output of the command.
///
/// Commandline for p4.
/// Stdin input.
/// Whether the command should spew.
///
/// True if succeeded, otherwise false.
public bool P4Output(out string[] OutputLines, string ExtraGlobalOptions, string CommandLine, string Input = null, bool AllowSpew = true, bool WithClient = true)
{
string Output;
bool bResult = P4Output(out Output, ExtraGlobalOptions, CommandLine, Input, AllowSpew, WithClient);
List Lines = new List();
for(int Idx = 0; Idx < Output.Length; )
{
int EndIdx = Output.IndexOf('\n', Idx);
if(EndIdx == -1)
{
Lines.Add(Output.Substring(Idx));
break;
}
if(EndIdx > Idx && Output[EndIdx - 1] == '\r')
{
Lines.Add(Output.Substring(Idx, EndIdx - Idx - 1));
}
else
{
Lines.Add(Output.Substring(Idx, EndIdx - Idx));
}
Idx = EndIdx + 1;
}
OutputLines = Lines.ToArray();
return bResult;
}
///
/// Calls p4 command and writes the output to a logfile.
///
///
/// Commandline to pass to p4.
/// Stdin input.
/// Whether the command is allowed to spew.
///
///
public void LogP4(string ExtraGlobalOptions, string CommandLine, string Input = null, bool AllowSpew = true, bool WithClient = true, bool SpewIsVerbose = false)
{
string Output;
if (!LogP4Output(out Output, ExtraGlobalOptions, CommandLine, Input, AllowSpew, WithClient, SpewIsVerbose:SpewIsVerbose))
{
throw new P4Exception("p4.exe {0} failed. {1}", CommandLine, Output);
}
}
///
/// Calls p4 and returns the output and writes it also to a logfile.
///
/// Output of the comman.
///
/// Commandline for p4.
/// Stdin input.
/// Whether the command should spew.
///
///
/// True if succeeded, otherwise false.
public bool LogP4Output(out string Output, string ExtraGlobalOptions, string CommandLine, string Input = null, bool AllowSpew = true, bool WithClient = true, bool SpewIsVerbose = false)
{
Output = "";
if (String.IsNullOrEmpty(LogPath))
{
Logger.LogError("P4Utils.SetupP4() must be called before issuing Peforce commands");
return false;
}
var Result = P4(ExtraGlobalOptions, CommandLine, Input, AllowSpew, WithClient, SpewIsVerbose:SpewIsVerbose);
CommandUtils.WriteToFile(LogPath, CommandLine + "\n");
CommandUtils.WriteToFile(LogPath, Result.Output);
Output = Result.Output;
return Result.ExitCode == 0;
}
///
/// Execute a Perforce command and parse the output as marshalled Python objects. This is more robustly defined than the text-based tagged output
/// format, because it avoids ambiguity when returned fields can have newlines.
///
/// Command line to execute Perforce with
/// Whether to include client information on the command line
public List> P4TaggedOutput(string CommandLine, bool WithClient = true)
{
// Execute Perforce, consuming the binary output into a memory stream
MemoryStream MemoryStream = new MemoryStream();
using (Process Process = new Process())
{
Process.StartInfo.FileName = HostPlatform.Current.P4Exe;
Process.StartInfo.Arguments = String.Format("-G {0} {1}", WithClient? GlobalOptions : GlobalOptionsWithoutClient, CommandLine);
Process.StartInfo.RedirectStandardError = true;
Process.StartInfo.RedirectStandardOutput = true;
Process.StartInfo.RedirectStandardInput = false;
Process.StartInfo.UseShellExecute = false;
Process.StartInfo.CreateNoWindow = true;
Process.Start();
Process.StandardOutput.BaseStream.CopyTo(MemoryStream);
Process.WaitForExit();
}
// Move back to the start of the memory stream
MemoryStream.Position = 0;
// Parse the records
List> Records = new List>();
using (BinaryReader Reader = new BinaryReader(MemoryStream, Encoding.UTF8))
{
while(Reader.BaseStream.Position < Reader.BaseStream.Length)
{
// Check that a dictionary follows
byte Temp = Reader.ReadByte();
if(Temp != '{')
{
throw new P4Exception("Unexpected data while parsing marshalled output - expected '{'");
}
// Read all the fields in the record
Dictionary Record = new Dictionary();
for(;;)
{
// Read the next field type. Perforce only outputs string records. A '0' character indicates the end of the dictionary.
byte KeyFieldType = Reader.ReadByte();
if(KeyFieldType == '0')
{
break;
}
else if(KeyFieldType != 's')
{
throw new P4Exception("Unexpected key field type while parsing marshalled output ({0}) - expected 's'", (int)KeyFieldType);
}
// Read the key
int KeyLength = Reader.ReadInt32();
string Key = Encoding.UTF8.GetString(Reader.ReadBytes(KeyLength));
// Read the value type.
byte ValueFieldType = Reader.ReadByte();
if(ValueFieldType == 'i')
{
// An integer
string Value = Reader.ReadInt32().ToString();
Record.Add(Key, Value);
}
else if(ValueFieldType == 's')
{
// A string
int ValueLength = Reader.ReadInt32();
string Value = Encoding.UTF8.GetString(Reader.ReadBytes(ValueLength));
Record.Add(Key, Value);
}
else
{
throw new P4Exception("Unexpected value field type while parsing marshalled output ({0}) - expected 's'", (int)ValueFieldType);
}
}
Records.Add(Record);
}
}
return Records;
}
///
/// Checks that the raw record data includes the given return code, or creates a ReturnCode value if it doesn't
///
/// The raw record data
/// The expected code value
/// Output variable for receiving the return code if it doesn't match
public static bool VerifyReturnCode(Dictionary RawRecord, string ExpectedCode, out P4ReturnCode OtherReturnCode)
{
// Parse the code field
string Code;
if(!RawRecord.TryGetValue("code", out Code))
{
Code = "unknown";
}
// Check whether it matches what we expect
if(Code == ExpectedCode)
{
OtherReturnCode = null;
return true;
}
else
{
string Severity;
if(!RawRecord.TryGetValue("severity", out Severity))
{
Severity = ((int)P4SeverityCode.Empty).ToString();
}
string Generic;
if(!RawRecord.TryGetValue("generic", out Generic))
{
Generic = ((int)P4GenericCode.None).ToString();
}
string Message;
if(!RawRecord.TryGetValue("data", out Message))
{
Message = "No description available.";
}
OtherReturnCode = new P4ReturnCode(Code, (P4SeverityCode)int.Parse(Severity), (P4GenericCode)int.Parse(Generic), Message.TrimEnd());
return false;
}
}
///
/// Invokes p4 login command.
///
public string GetAuthenticationToken()
{
string AuthenticationToken = null;
string Output;
string P4Passwd = InternalUtils.GetEnvironmentVariable("uebp_PASS", "", true);
if (Automation.IsBuildMachine && string.IsNullOrEmpty(P4Passwd))
{
return AuthenticationToken;
}
P4Output(out Output, "", "login -a -p", P4Passwd + '\n');
// 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;
}
///
/// Invokes p4 changes command.
///
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;
}
public override string ToString()
{
return String.Format("CL {0}: {1}", CL, Summary);
}
}
static Dictionary UserToEmailCache = new Dictionary();
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.ExitCode == 0)
{
var Tags = ParseTaggedP4Output(P4Result.Output);
Tags.TryGetValue("Email", out Result);
}
}
catch(Exception)
{
}
if (Result == "")
{
Logger.LogWarning("Could not find email for P4 user {User}", User);
}
UserToEmailCache.Add(User, Result);
return Result;
}
static Dictionary> ChangesCache = new Dictionary>();
public bool Changes(out List 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();
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);
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)
{
Logger.LogWarning("Unable to get P4 changes with {CommandLine}", CommandLine);
Logger.LogWarning(" Exception was {Arg0}", 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 override string ToString()
{
return String.Format("{0}#{1} ({2})", File, Revision, ChangeType);
}
}
public List Files = new List();
public static int Compare(DescribeRecord A, DescribeRecord B)
{
return (A.CL < B.CL) ? -1 : (A.CL > B.CL) ? 1 : 0;
}
public override string ToString()
{
return String.Format("CL {0}: {1}", CL, Summary);
}
}
///
/// Wraps P4 describe
///
/// Changelist numbers to query full descriptions for
/// Describe record for the given changelist.
///
///
/// True if everything went okay
public bool DescribeChangelist(int Changelist, out DescribeRecord DescribeRecord, bool AllowSpew = true, bool bShelvedFiles = false)
{
List DescribeRecords;
if(!DescribeChangelists(new List{ Changelist }, out DescribeRecords, AllowSpew, bShelvedFiles))
{
DescribeRecord = null;
return false;
}
else if(DescribeRecords.Count != 1)
{
DescribeRecord = null;
return false;
}
else
{
DescribeRecord = DescribeRecords[0];
return true;
}
}
///
/// Wraps P4 describe
///
/// List of changelist numbers to query full descriptions for
/// List of records we found. One for each changelist number. These will be sorted from oldest to newest.
///
/// Whether to display shelved files
/// True if everything went okay
public bool DescribeChangelists(List Changelists, out List DescribeRecords, bool AllowSpew = true, bool bShelvedFiles = false)
{
DescribeRecords = new List();
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
if(bShelvedFiles)
{
CommandLine += " -S";
}
// 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(Line.Length > 0)
{
// Stop once we reach a line that doesn't begin with a tab. It's possible (through changelist descriptions that contain embedded newlines, like \r\r\n on Windows) to get
// empty lines that don't begin with a tab as we expect.
if(Line[0] != '\t')
{
break;
}
// 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;
}
}
// Remove any trailing newlines from the end of the summary
DescribeRecord.Summary = DescribeRecord.Summary.TrimEnd('\n');
Line = Lines[ LineIndex ];
string MatchAffectedFiles = bShelvedFiles? "Shelved files" : "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;
}
///
/// Invokes p4 sync command.
///
/// CommandLine to pass on to the command.
///
///
///
///
public void Sync(string CommandLine, bool AllowSpew = true, bool SpewIsVerbose = false, int Retries=0, int MaxWait=0)
{
string SyncCommandLine = "sync " + CommandLine;
string ExtraGlobalOptions = "";
if (MaxWait > 0)
{
ExtraGlobalOptions = string.Format("-vnet.maxwait={0} {1}", MaxWait, ExtraGlobalOptions);
}
if (Retries > 0)
{
ExtraGlobalOptions = string.Format("-r{0} {1}", Retries, ExtraGlobalOptions);
}
LogP4(ExtraGlobalOptions, SyncCommandLine, null, AllowSpew, SpewIsVerbose:SpewIsVerbose);
}
///
/// Invokes p4 preview sync command and gets a list of preview synced files.
///
/// Files that have been preview synced with the command
/// CommandLine to pass on to the command.
///
///
/// Whether preview sync is successful
public bool PreviewSync(out List FilesPreviewSynced, string CommandLine, bool AllowSpew = true, bool SpewIsVerbose = false)
{
FilesPreviewSynced = new List();
try
{
string Output;
LogP4Output(out Output, "", "sync -n " + CommandLine, null, AllowSpew, SpewIsVerbose: SpewIsVerbose);
string UpToDateOutput = String.Format("{0} - file(s) up-to-date.\r\n", CommandLine);
if (Output == UpToDateOutput)
{
return true;
}
var Lines = Output.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
foreach(var line in Lines)
{
// Line example: //Fortnite/Main/FortniteGame/Content/Backend/Calendars/athena-sales.ics#11 - updating D:\Build\UE4-Fortnite\FortniteGame\Content\Backend\Calendars\athena-sales.ics
var splittedLine = line.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if(splittedLine.Length > 3)
{
FilesPreviewSynced.Add(splittedLine[3]);
}
}
}
catch (Exception Ex)
{
Logger.LogWarning("Unable to preview sync P4 changes with {CommandLine}", CommandLine);
Logger.LogWarning(" Exception was {Arg0}", LogUtils.FormatException(Ex));
return false;
}
return true;
}
///
/// Invokes p4 unshelve command.
///
/// Changelist to unshelve.
/// Changelist where the checked out files should be added.
/// Commandline for the command.
///
public void Unshelve(int FromCL, int ToCL, string CommandLine = "", bool SpewIsVerbose = false)
{
LogP4("", "unshelve " + String.Format("-s {0} ", FromCL) + String.Format("-c {0} ", ToCL) + CommandLine, SpewIsVerbose: SpewIsVerbose);
}
///
/// Invokes p4 shelve command.
///
/// Changelist to unshelve.
/// Commandline for the command.
///
public void Shelve(int FromCL, string CommandLine = "", bool AllowSpew = true)
{
LogP4("", "shelve " + String.Format("-r -c {0} ", FromCL) + CommandLine, AllowSpew: AllowSpew);
}
///
/// Invokes p4 shelve command, without reverting existing shelf first (overwrites any existing shelved file)
/// This means that any files that are already shelved, but not in the FromCL being shelved, they will still exist
///
/// Changelist to unshelve.
/// Commandline for the command.
///
public void ShelveNoRevert(int FromCL, string CommandLine = "", bool AllowSpew = true)
{
LogP4("", "shelve " + String.Format("-f -c {0} ", FromCL) + CommandLine, AllowSpew: AllowSpew);
}
///
/// Deletes shelved files from a changelist
///
/// Changelist to unshelve.
///
public void DeleteShelvedFiles(int FromCL, bool AllowSpew = true)
{
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);
}
}
///
/// Invoke a command on a list of files, breaking up into multiple commands so as not to blow the max commandline length
///
/// The command, like "p4 edit -c 1000"
///
///
public void BatchedCommand(string Command, List Files, bool AllowSpew = true)
{
// using the limit of ProcessStartInfo.Arguments
const int MaxCommandLineLength = 32699;
StringBuilder CommandLine = new StringBuilder();
for (int Idx = 0; Idx < Files.Count; Idx++)
{
if (CommandLine.Length + Files[Idx].Length + 3 > MaxCommandLineLength)
{
LogP4("", Command + CommandLine.ToString(), AllowSpew:AllowSpew);
CommandLine.Clear();
}
CommandLine.AppendFormat(" \"{0}\"", Files[Idx]);
}
if (CommandLine.Length > 0)
{
LogP4("", Command + CommandLine.ToString(), AllowSpew: AllowSpew);
}
}
///
/// Invokes p4 reopen command.
///
/// Changelist where the checked out files should be moved to.
/// Commandline for the command
///
public void Reopen(int CL, string CommandLine, bool AllowSpew = true)
{
LogP4("", $"reopen -c {CL} {CommandLine}", AllowSpew: AllowSpew);
}
///
/// Invokes p4 reopen command with a list of files. IMPORTANT: because of commandline limits
/// the file list will be broken up across multiple commands. This will fail if a "move" operation
/// (that is made up of two files) is split between commandlines - they must be moved in the
/// same operation
///
/// Changelist where the checked out files should be moved to.
/// List of files to be moved
///
public void Reopen(int CL, List Files, bool AllowSpew = true)
{
BatchedCommand($"reopen -c {CL}", Files, AllowSpew: AllowSpew);
}
///
/// Invokes p4 edit command.
///
/// Changelist where the checked out files should be added.
/// Commandline for the command.
///
public void Edit(int CL, string CommandLine, bool AllowSpew = true)
{
LogP4("", "edit " + String.Format("-c {0} ", CL) + CommandLine, AllowSpew: AllowSpew);
}
///
/// Invokes p4 edit command with a list of files.
///
/// Changelist where the checked out files should be added.
///
///
public void Edit(int CL, List Files, bool AllowSpew = true)
{
BatchedCommand($"edit -c {CL}", Files, AllowSpew: AllowSpew);
}
///
/// Invokes p4 edit command, no exceptions
///
/// Changelist where the checked out files should be added.
/// Commandline for the command.
public bool Edit_NoExceptions(int CL, string CommandLine)
{
try
{
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;
}
}
///
/// Invokes p4 add command.
///
/// Changelist where the files should be added to.
/// Commandline for the command.
public void Add(int CL, string CommandLine)
{
LogP4("", "add " + String.Format("-c {0} ", CL) + CommandLine);
}
///
/// Invokes p4 add command with a list of files.
///
/// Changelist where the checked out files should be added.
/// The list of files to add.
///
public void Add(int CL, List Files, bool AllowSpew = true)
{
BatchedCommand($"add -c {CL}", Files, AllowSpew: AllowSpew);
}
///
/// Invokes p4 delete command.
///
/// Changelist where the files should be added to.
/// Commandline for the command.
public void Delete(int CL, string CommandLine)
{
LogP4("", "delete " + String.Format("-c {0} ", CL) + CommandLine);
}
///
/// Invokes p4 delete command with a list of files.
///
/// Changelist where the checked out files should be added.
/// List of files to be deleted.
///
public void Delete(int CL, List Files, bool AllowSpew = true)
{
BatchedCommand($"delete -c {CL}", Files, AllowSpew: AllowSpew);
}
///
/// Invokes p4 reconcile command.
///
/// Changelist to check the files out.
/// Commandline for the command.
///
public void Reconcile(int CL, string CommandLine, bool AllowSpew = true)
{
LogP4("", "reconcile " + String.Format("-c {0} -ead -f ", CL) + CommandLine, AllowSpew: AllowSpew);
}
///
/// Invokes p4 reconcile command.
///
/// Commandline for the command.
public void ReconcilePreview(string CommandLine)
{
LogP4("", "reconcile " + String.Format("-ead -n ") + CommandLine);
}
///
/// Invokes p4 reconcile command.
/// Ignores files that were removed.
///
/// Changelist to check the files out.
/// Commandline for the command.
///
public void ReconcileNoDeletes(int CL, string CommandLine, bool AllowSpew = true)
{
LogP4("", "reconcile " + String.Format("-c {0} -ea ", CL) + CommandLine, AllowSpew: AllowSpew);
}
///
/// Invokes p4 resolve command.
/// Resolves all files by accepting yours and ignoring theirs.
///
/// Changelist to resolve.
/// Commandline for the command.
public void Resolve(int CL, string CommandLine)
{
LogP4("", "resolve -ay " + String.Format("-c {0} ", CL) + CommandLine);
}
///
/// Invokes revert command.
///
/// Commandline for the command.
///
public void Revert(string CommandLine, bool AllowSpew = true)
{
LogP4("", "revert " + CommandLine, AllowSpew: AllowSpew);
}
///
/// Invokes revert command.
///
/// Changelist to revert
/// Commandline for the command.
///
public void Revert(int CL, string CommandLine = "", bool AllowSpew = true)
{
LogP4("", "revert " + String.Format("-c {0} ", CL) + CommandLine, AllowSpew: AllowSpew);
}
///
/// Invokes p4 revert command with a list of files.
///
/// Changelist where the checked out files should be reverted.
/// List of files to be reverted.
///
public void Revert(int CL, List Files, bool AllowSpew = true)
{
BatchedCommand($"revert -c {CL}", Files, AllowSpew: AllowSpew);
}
///
/// Reverts all unchanged file from the specified changelist.
///
/// Changelist to revert the unmodified files from.
public void RevertUnchanged(int CL)
{
// caution this is a really bad idea if you hope to force submit!!!
LogP4("", "revert -a " + String.Format("-c {0} ", CL));
}
///
/// Reverts all files from the specified changelist.
///
/// Changelist to revert.
///
public void RevertAll(int CL, bool SpewIsVerbose = false)
{
LogP4("", "revert " + String.Format("-c {0} //...", CL), SpewIsVerbose: SpewIsVerbose);
}
///
/// Submits the specified changelist.
///
/// Changelist to submit.
/// If true, the submit will be forced even if resolve is needed.
/// If true, if the submit fails, revert the CL.
public void Submit(int CL, bool Force = false, bool RevertIfFail = false)
{
int SubmittedCL;
Submit(CL, out SubmittedCL, Force, RevertIfFail);
}
///
/// Submits the specified changelist.
///
/// Changelist to submit.
/// Will be set to the submitted changelist number.
/// If true, the submit will be forced even if resolve is needed.
/// If true, if the submit fails, revert the CL.
public void Submit(int CL, out int SubmittedCL, bool Force = false, bool RevertIfFail = false)
{
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)
{
Logger.LogInformation("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} -f submitunchanged", CL)))
{
if (!Force)
{
throw new P4Exception("Change {0} failed to submit.\n{1}", CL, CmdOutput);
}
Logger.LogInformation("**** P4 Returned\n{CmdOutput}\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 AlreadyDone = new HashSet();
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 (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);
if (AlreadyDone.Contains(File))
{
continue;
}
Logger.LogInformation("Brutal 'resolve' on {File} 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)
{
Logger.LogInformation("Change {CL} failed to submit for reasons we do not recognize.\n{CmdOutput}\nWaiting and retrying.", CL, CmdOutput);
}
System.Threading.Thread.Sleep(30000);
}
else
{
LastCmdOutput = CmdOutput;
Regex SubmitRegex = new Regex(@"Change \d+ renamed change (?\d+) and submitted");
Match SubmitMatch = SubmitRegex.Match(CmdOutput);
if (!SubmitMatch.Success)
{
SubmitRegex = new Regex(@"Change (?\d+) submitted");
SubmitMatch = SubmitRegex.Match(CmdOutput);
}
if (SubmitMatch.Success)
{
SubmittedCL = int.Parse(SubmitMatch.Groups["number"].Value);
Logger.LogInformation("Submitted CL {CL} which became CL {SubmittedCL}\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)
{
Logger.LogError("Submit CL {CL} failed, reverting files\n", CL);
RevertAll(CL);
Logger.LogError("Submit CL {CL} failed, reverting files\n", CL);
}
throw new P4Exception("Change {0} failed to submit after 48 retries??.\n{1}", CL, LastCmdOutput);
}
///
/// Creates a new changelist with the specified owner and description.
///
/// Owner of the changelist.
/// Description of the changelist.
///
///
///
/// Id of the created changelist.
public int CreateChange(string Owner = null, string Description = null, string User = null, string Type = null, bool AllowSpew = false)
{
var ChangeSpec = "Change: new" + "\n";
ChangeSpec += "Client: " + ((Owner != null) ? Owner : "") + "\n";
if(User != null)
{
ChangeSpec += "User: " + User + "\n";
}
if(Type != null)
{
ChangeSpec += "Type: " + Type + "\n";
}
ChangeSpec += "Description: " + ((Description != null) ? Description.Replace("\n", "\n\t") : "(none)") + "\n";
string CmdOutput;
int CL = 0;
if(AllowSpew)
{
Logger.LogInformation("Creating Change\n {ChangeSpec}\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)
{
Logger.LogInformation("Returned CL {CL}\n", CL);
}
return CL;
}
///
/// Updates a changelist with the given fields
///
///
///
///
///
public void UpdateChange(int CL, string NewClient, string NewDescription, bool SpewIsVerbose = false)
{
UpdateChange(CL, null, NewClient, NewDescription, SpewIsVerbose);
}
///
/// Updates a changelist with the given fields
///
///
///
///
///
///
public void UpdateChange(int CL, string NewUser, string NewClient, string NewDescription, bool SpewIsVerbose = false)
{
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 (NewUser != null)
{
Spec.SetField("User", NewUser);
}
if (NewClient != null)
{
Spec.SetField("Client", NewClient);
}
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);
}
}
///
/// Deletes the specified changelist.
///
/// Changelist to delete.
/// Indicates whether files in that changelist should be reverted.
///
///
public void DeleteChange(int CL, bool RevertFiles = true, bool SpewIsVerbose = false, bool AllowSpew = true)
{
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);
}
///
/// Tries to delete the specified empty changelist.
///
/// Changelist to delete.
/// True if the changelist was deleted, false otherwise.
public bool TryDeleteEmptyChange(int CL)
{
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;
}
///
/// Returns the changelist specification.
///
/// Changelist to get the specification from.
///
/// Specification of the changelist.
public string ChangeOutput(int CL, bool AllowSpew = true)
{
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);
}
///
/// Checks whether the specified changelist exists.
///
/// Changelist id.
/// Whether it is a pending changelist.
///
/// Returns whether the changelist exists.
public bool ChangeExists(int CL, out bool Pending, bool AllowSpew = true)
{
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)
{
if (AllowSpew)
{
Logger.LogInformation("Change {CL} does not exist", CL);
}
return false;
}
string StatusStr = "Status:";
int StatusOffset = CmdOutput.LastIndexOf(StatusStr);
if (StatusOffset < 1)
{
Logger.LogError("Change {CL} could not be parsed\n{CmdOutput}", CL, CmdOutput);
return false;
}
string Status = CmdOutput.Substring(StatusOffset + StatusStr.Length).TrimStart().Split('\n')[0].TrimEnd();
if (AllowSpew)
{
Logger.LogInformation("Change {CL} exists ({Status})", CL, Status);
}
Pending = (Status == "pending");
return true;
}
Logger.LogError("Change exists failed {CL} no output?", CL);
return false;
}
///
/// Returns a list of files contained in the specified changelist.
///
/// Changelist to get the files from.
/// Whether the changelist is a pending one.
///
/// List of the files contained in the changelist.
public List ChangeFiles(int CL, out bool Pending, bool AllowSpew = true)
{
var Result = new List();
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;
}
///
/// Returns the output from p4 opened
///
/// Specification of the changelist.
public string OpenedOutput()
{
string CmdOutput;
if (LogP4Output(out CmdOutput, "", "opened"))
{
return CmdOutput;
}
throw new P4Exception("OpenedOutput failed, output follows\n{0}", CmdOutput);
}
///
/// Deletes the specified label.
///
/// Label to delete.
///
public void DeleteLabel(string LabelName, bool AllowSpew = true)
{
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))
{
Logger.LogInformation("Couldn't delete label '{LabelName}'. It may not have existed in the first place.", LabelName);
}
}
///
/// Creates a new label.
///
/// Name of the label.
/// Options for the label. Valid options are "locked", "unlocked", "autoreload" and "noautoreload".
/// View mapping for the label.
/// Owner of the label.
/// Description of the label.
/// Date of the label creation.
/// Time of the label creation
public void CreateLabel(string Name, string Options, string View, string Owner = null, string Description = null, string Date = null, string Time = null)
{
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;
Logger.LogInformation("Creating Label\n {LabelSpec}\n", LabelSpec);
LogP4("", "label -i", Input: LabelSpec);
}
///
/// Invokes p4 tag command.
/// Associates a named label with a file revision.
///
/// Name of the label.
/// Path to the file.
/// Whether the command is allowed to spew.
public void Tag(string LabelName, string FilePath, bool AllowSpew = true)
{
LogP4("", "tag -l " + LabelName + " " + FilePath, null, AllowSpew);
}
///
/// Syncs a label to the current content of the client.
///
/// Name of the label.
/// Whether the command is allowed to spew.
///
public void LabelSync(string LabelName, bool AllowSpew = true, string FileToLabel = "")
{
string Quiet = "";
if (!AllowSpew)
{
Quiet = "-q ";
}
if (FileToLabel == "")
{
LogP4("", "labelsync " + Quiet + "-l " + LabelName);
}
else
{
LogP4("", "labelsync " + Quiet + "-l" + LabelName + " " + FileToLabel);
}
}
///
/// Syncs a label from another label.
///
/// Source label name.
/// Target label name.
/// Whether the command is allowed to spew.
public void LabelToLabelSync(string FromLabelName, string ToLabelName, bool AllowSpew = true)
{
string Quiet = "";
if (!AllowSpew)
{
Quiet = "-q ";
}
LogP4("", "labelsync -a " + Quiet + "-l " + ToLabelName + " //...@" + FromLabelName);
}
///
/// Checks whether the specified label exists and has any files.
///
/// Name of the label.
/// Whether there is an label with files.
public bool LabelExistsAndHasFiles(string Name)
{
string Output;
return LogP4Output(out Output, "", "files -m 1 //...@" + Name);
}
///
/// Returns the label description.
///
/// Name of the label.
/// Description of the label.
/// Whether to allow log spew
/// Returns whether the label description could be retrieved.
public bool LabelDescription(string Name, out string Description, bool AllowSpew = true)
{
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;
}
///
/// Reads a label spec
///
/// Label name
/// Whether to allow log spew
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);
}
///
/// Updates a label with a new spec
///
/// Label specification
/// Whether to allow log spew
public void UpdateLabelSpec(P4Spec Spec, bool AllowSpew = true)
{
LogP4("", "label -i", Input: Spec.ToString(), AllowSpew: AllowSpew);
}
///
/// Updates a label description.
///
/// Name of the label
/// Description of the label.
/// Whether to allow log spew
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 Lines = new List(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+(?\d+)\s+.+$", RegexOptions.Compiled | RegexOptions.Multiline);
///
/// Gets the latest CL number submitted to the depot. It equals to the @head.
///
/// The head CL number.
public int GetLatestCLNumber()
{
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+(?[\w\/\.-]+)\s+(?\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2})\s+'(?.+)'\s*$", RegexOptions.Compiled | RegexOptions.Multiline);
///
/// Gets all labels satisfying given filter.
///
/// Filter for label names.
/// Treat filter as case-sensitive.
///
public P4Label[] GetLabels(string Filter, bool bCaseSensitive = true)
{
var LabelList = new List();
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();
}
///
/// Validate label for some content.
///
/// True if label exists and has at least one file tagged. False otherwise.
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;
}
///
/// Given a file path in the depot, returns the local disk mapping for the current view
///
/// The full file path in depot naming form
///
/// The file's first reported path on disk or null if no mapping was found
public string DepotToLocalPath(string DepotFile, bool AllowSpew = true)
{
// P4 where outputs missing entries
string Command = String.Format("fstat \"{0}\"", DepotFile);
// Run the command.
string[] Lines;
if (!P4Output(out Lines, "-z tag", Command, AllowSpew: AllowSpew))
{
throw new P4Exception("p4.exe {0} failed.", Command);
}
// Find the line containing the client file prefix
const string ClientFilePrefix = "... clientFile ";
foreach(string Line in Lines)
{
if(Line.StartsWith(ClientFilePrefix))
{
return Line.Substring(ClientFilePrefix.Length);
}
}
return null;
}
///
/// Given a set of file paths in the depot, returns the local disk mapping for the current view
///
/// The full file paths in depot naming form
///
/// The file's first reported path on disk or null if no mapping was found
public string[] DepotToLocalPaths(string[] DepotFiles, bool AllowSpew = true)
{
const int BatchSize = 20;
// Parse the output from P4
List Lines = new List();
for(int Idx = 0; Idx < DepotFiles.Length; Idx += BatchSize)
{
// Build the argument list
StringBuilder Command = new StringBuilder("fstat ");
for(int ArgIdx = Idx; ArgIdx < Idx + BatchSize && ArgIdx < DepotFiles.Length; ArgIdx++)
{
Command.AppendFormat(" {0}", CommandUtils.MakePathSafeToUseWithCommandLine(DepotFiles[ArgIdx]));
}
// Run the command.
string[] Output;
if (!P4Output(out Output, "-z tag", Command.ToString(), AllowSpew: AllowSpew))
{
throw new P4Exception("p4.exe {0} failed.", Command);
}
// Append it to the combined output
Lines.AddRange(Output);
}
// Parse all the error lines. These may occur out of sequence due to stdout/stderr buffering.
for(int LineIdx = 0; LineIdx < Lines.Count; LineIdx++)
{
if(Lines[LineIdx].Length > 0 && !Lines[LineIdx].StartsWith("... "))
{
throw new AutomationException("Unexpected output from p4.exe fstat: {0}", Lines[LineIdx]);
}
}
// Parse the output lines
string[] LocalFiles = new string[DepotFiles.Length];
for(int FileIdx = 0, LineIdx = 0; FileIdx < DepotFiles.Length; FileIdx++)
{
string DepotFile = DepotFiles[FileIdx];
if(LineIdx == Lines.Count)
{
throw new AutomationException("Unexpected end of output looking for file record for {0}", DepotFile);
}
else
{
// We've got a file record; try to parse the matching fields
for(; LineIdx < Lines.Count && Lines[LineIdx].Length > 0; LineIdx++)
{
const string DepotFilePrefix = "... depotFile ";
if(Lines[LineIdx].StartsWith(DepotFilePrefix) && !Lines[LineIdx].Substring(DepotFilePrefix.Length).Equals(DepotFile, StringComparison.InvariantCultureIgnoreCase))
{
throw new AutomationException("Expected file record for '{0}'; received output '{1}'", DepotFile, Lines[LineIdx]);
}
const string ClientFilePrefix = "... clientFile ";
if(Lines[LineIdx].StartsWith(ClientFilePrefix))
{
LocalFiles[FileIdx] = Lines[LineIdx].Substring(ClientFilePrefix.Length);
}
}
// Skip any blank lines
while(LineIdx < Lines.Count && Lines[LineIdx].Length == 0)
{
LineIdx++;
}
}
}
return LocalFiles;
}
///
/// 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.
///
/// Depot path
/// Allows logging
/// List of records describing the file's mapping. Usually just one, but may be more.
public P4WhereRecord[] Where(string DepotFile, bool AllowSpew = true)
{
// P4 where outputs missing entries
string Command = String.Format("where \"{0}\"", DepotFile);
// Run the command.
string Output;
if (!LogP4Output(out Output, "-z tag", 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 Records = new List();
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 "
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 "
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 "
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();
}
///
/// Determines whether a file exists in the depot.
///
/// Depot path
///
/// List of records describing the file's mapping. Usually just one, but may be more.
public bool FileExistsInDepot(string DepotFile, bool AllowSpew = true)
{
string CommandLine = String.Format("fstat {0}", CommandUtils.MakePathSafeToUseWithCommandLine(DepotFile));
string Output;
if(!LogP4Output(out Output, "-z tag", CommandLine, AllowSpew: false) || !Output.Contains("headRev"))
{
return false;
}
return true;
}
///
/// Gets file stats.
///
/// Filenam
/// File stats (invalid if the file does not exist in P4)
public P4FileStat FStat(string Filename)
{
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;
}
///
/// Set file attributes (additively)
///
/// File to change the attributes of.
/// Attributes to set.
///
public void ChangeFileType(string Filename, P4FileAttributes Attributes, string Changelist = null)
{
Logger.LogDebug("ChangeFileType({Filename}, {Attributes}, {Arg2})", 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);
}
}
///
/// Parses P4 forms and stores them as a key/value pairs.
///
/// P4 command output (must be a form).
/// Parsed output.
public Dictionary ParseTaggedP4Output(string Output)
{
var Tags = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
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 BaseKey = TrimmedLine.Substring(0, KeyEndIndex);
// Uniquify the key before adding anything to the dictionary. P4 info can sometimes return multiple fields with identical names (eg. 'Broker address', 'Broker version')
DelayIndex = 0;
var Key = BaseKey;
while(Tags.ContainsKey(Key))
{
DelayIndex++;
Key = String.Format("{0}{1}", BaseKey, DelayIndex);
}
var Value = TrimmedLine.Substring(KeyEndIndex + 1).Trim();
if (Value == "")
{
DelayKey = BaseKey;
}
else
{
Tags.Add(Key, Value);
}
}
}
}
return Tags;
}
///
/// Formats a tagged record as a string
///
/// The record to format
/// Single string containing the record
public static string FormatTaggedOutput(Dictionary Record)
{
StringBuilder Result = new StringBuilder();
foreach (KeyValuePair Pair in Record)
{
if (Result.Length > 0)
{
Result.Append('\n');
}
Result.AppendFormat("{0}: {1}", Pair.Key, Pair.Value);
}
return Result.ToString();
}
///
/// Checks if the client exists in P4.
///
/// Client name
///
/// True if the client exists.
public bool DoesClientExist(string ClientName, bool Quiet = false)
{
if(!Quiet)
{
Logger.LogDebug("Checking if client {ClientName} exists", ClientName);
}
var P4Result = P4(String.Format("-c {0}", ClientName), "where //...", Input: null, AllowSpew: false, WithClient: false);
return P4Result.Output.IndexOf("unknown - use 'client' command", StringComparison.InvariantCultureIgnoreCase) < 0 && P4Result.Output.IndexOf("doesn't exist", StringComparison.InvariantCultureIgnoreCase) < 0;
}
///
/// Gets client info.
///
/// Name of the client.
///
///
public P4ClientInfo GetClientInfo(string ClientName, bool Quiet = false)
{
if(!Quiet)
{
Logger.LogDebug("Getting info for client {ClientName}", ClientName);
}
if (!DoesClientExist(ClientName, Quiet))
{
return null;
}
return GetClientInfoInternal(ClientName);
}
///
/// Parses a string with enum values separated with spaces.
///
///
///
///
private static object ParseEnumValues(string ValueText, Type EnumType)
{
ValueText = new Regex("[+ ]").Replace(ValueText, ",");
return Enum.Parse(EnumType, ValueText, true);
}
///
/// Gets client info (does not check if the client exists)
///
/// Name of the client.
///
public P4ClientInfo GetClientInfoInternal(string ClientName)
{
P4ClientInfo Info = new P4ClientInfo();
var P4Result = P4(String.Format("client -o {0}", ClientName), AllowSpew: false, WithClient: false);
if (P4Result.ExitCode == 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);
Tags.TryGetValue("Stream", out Info.Stream);
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(
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;
}
///
/// Gets all clients owned by the user.
///
///
///
///
/// List of clients owned by the user.
public P4ClientInfo[] GetClientsForUser(string UserName, string PathUnderClientRoot = null, string AllowedStream = null)
{
var ClientList = new List();
// Get all clients for this user
string P4Command = String.Format("clients -u {0}", UserName);
// filter by Stream if desired
if (AllowedStream != null)
{
P4Command += " -S " + AllowedStream;
}
string Output;
if (!P4Output(out Output, "-ztag -F \"Client %client% Root %Root%\"", P4Command, AllowSpew: false, WithClient: false))
{
throw new AutomationException("p4 clients -u {0} failed.", UserName);
}
// Parse output.
Regex OutputSplitter = new Regex(@"Client (?.+) Root (?.*)");
var Lines = Output.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
foreach (string Line in Lines)
{
Match RegexMatch = OutputSplitter.Match(Line);
string ClientName = RegexMatch.Groups["client"].Value;
string RootPath = RegexMatch.Groups["root"].Value;
if (String.IsNullOrEmpty(ClientName) || String.IsNullOrEmpty(RootPath))
{
throw new AutomationException("Failed to retrieve p4 client info for user {0}. Unable to set up local environment", UserName);
}
if (IsValidRootPathForFile(RootPath, PathUnderClientRoot))
{
P4ClientInfo Info = GetClientInfoInternal(ClientName);
if (Info == null)
{
throw new AutomationException("Failed to retrieve p4 client info for user {0}. Unable to set up local environment", UserName);
}
ClientList.Add(Info);
}
}
return ClientList.ToArray();
}
public bool IsValidClientForFile(P4ClientInfo Info, string PathUnderClientRoot)
{
return IsValidRootPathForFile(Info.RootPath, PathUnderClientRoot);
}
public bool IsValidRootPathForFile(string RootPath, string PathUnderClientRoot)
{
// Filter the client out if the specified path is not under the client root
bool bAddClient = true;
if (!String.IsNullOrEmpty(PathUnderClientRoot) && !String.IsNullOrEmpty(RootPath))
{
var ClientRootPathWithSlash = RootPath;
if (!ClientRootPathWithSlash.EndsWith("\\") && !ClientRootPathWithSlash.EndsWith("/"))
{
ClientRootPathWithSlash = CommandUtils.ConvertSeparators(PathSeparator.Default, ClientRootPathWithSlash + "/");
}
bAddClient = PathUnderClientRoot.StartsWith(ClientRootPathWithSlash, StringComparison.CurrentCultureIgnoreCase);
}
return bAddClient;
}
///
/// Deletes a client.
///
/// Client name.
/// Forces the operation (-f)
///
public void DeleteClient(string Name, bool Force = false, bool AllowSpew = true)
{
LogP4("", String.Format("client -d {0} {1}", (Force ? "-f" : ""), Name), WithClient: false, AllowSpew: AllowSpew);
}
///
/// Creates a new client.
///
/// Client specification.
///
///
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;
if (ClientSpec.Options != P4ClientOption.None)
{
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;
if(ClientSpec.Stream != null)
{
SpecInput += "Stream: " + ClientSpec.Stream + Environment.NewLine;
}
else
{
SpecInput += "View:" + Environment.NewLine;
foreach (var Mapping in ClientSpec.View)
{
SpecInput += "\t" + Mapping.Key + " //" + ClientSpec.Name + Mapping.Value + Environment.NewLine;
}
}
Logger.LogDebug("{Text}", SpecInput);
LogP4("", "client -i", SpecInput, AllowSpew: AllowSpew, WithClient: false);
return ClientSpec;
}
///
/// Lists immediate sub-directories of the specified directory.
///
///
/// List of sub-directories of the specified directories.
public List Dirs(string CommandLine)
{
var DirsCmdLine = String.Format("dirs {0}", CommandLine);
var P4Result = P4(DirsCmdLine, AllowSpew: false);
if (P4Result.ExitCode != 0)
{
throw new AutomationException("{0} failed.", DirsCmdLine);
}
var Result = new List();
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;
}
///
/// Takes P4 output from files or opened and returns a dictionary of paths/actions
///
///
///
protected Dictionary MapFileOutputToActions(string P4Output)
{
Dictionary Results = new Dictionary();
string[] Lines = P4Output.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
// # - change 16479539 (text)
Regex OutputSplitter = new Regex(@"(?.+)#(?\d+|none) \- (?[a-zA-Z/]+) .+");
foreach (string Line in Lines)
{
if (!Line.Contains("no such file") && OutputSplitter.IsMatch(Line))
{
Match RegexMatch = OutputSplitter.Match(Line);
string Filename = RegexMatch.Groups["filename"].Value;
string Action = RegexMatch.Groups["action"].Value;
Results.Add(Filename, Action);
}
}
return Results;
}
///
/// Run 'p4 files [cmdline]'and return a list of the files in the changelist (files being deleted are excluded)
///
///
/// List of files in the specified directory.
public List Files(string CommandLine)
{
List DeleteActions = new List { "delete", "move/delete", "archive", "purge" };
string FilesCmdLine = String.Format("files {0}", CommandLine);
IProcessResult P4Result = P4(FilesCmdLine, AllowSpew: false);
if (P4Result.ExitCode != 0)
{
throw new AutomationException("{0} failed.", FilesCmdLine);
}
List Result = new List();
Dictionary FileActions = MapFileOutputToActions(P4Result.Output);
foreach (var KV in FileActions)
{
if (!DeleteActions.Contains(KV.Value))
{
Result.Add(KV.Key);
}
}
return Result;
}
///
/// Run 'p4 opened [cmdline]'and return a list of the files in the changelist (files being deleted are excluded)
///
///
/// List of files in the specified directory.
public List Opened(string CommandLine)
{
List DeleteActions = new List { "delete", "move/delete", "archive", "purge" };
string FilesCmdLine = String.Format("opened {0}", CommandLine);
IProcessResult P4Result = P4(FilesCmdLine, AllowSpew: false);
if (P4Result.ExitCode != 0)
{
throw new AutomationException("{0} failed.", FilesCmdLine);
}
List Result = new List();
Dictionary FileActions = MapFileOutputToActions(P4Result.Output);
foreach (var KV in FileActions)
{
if (!DeleteActions.Contains(KV.Value))
{
Result.Add(KV.Key);
}
}
return Result;
}
///
/// Gets the contents of a particular file in the depot without syncing it
///
/// Depot path to the file (with revision/range if necessary)
///
/// Contents of the file
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;
}
///
/// Gets the contents of a particular file in the depot and writes it to a local file without syncing it
///
/// Depot path to the file (with revision/range if necessary)
/// Output file to write to
/// If true, any output from p4.exe will be logged.
public void PrintToFile(string DepotPath, string FileName, bool AllowSpew = true)
{
string Output;
if(!P4Output(out Output, "", "print -q -o \"" + FileName + "\" " + DepotPath, AllowSpew: AllowSpew, WithClient: false))
{
throw new AutomationException("p4 print {0} failed{1}{2}", DepotPath, Environment.NewLine, Output);
}
if(!Output.Trim().Contains("\n") && Output.Contains("no such file(s)"))
{
throw new AutomationException("p4 print {0} failed{1}{2}", DepotPath, Environment.NewLine, Output);
}
}
///
/// Runs the 'interchanges' command on a stream, to determine a list of changelists that have not been integrated to its parent (or vice-versa, if bReverse is set).
///
/// The name of the stream, eg. //UE4/Dev-Animation
/// If true, returns changes that have not been merged from the parent stream into this one.
/// List of changelist numbers that are pending integration
public List StreamInterchanges(string StreamName, bool bReverse)
{
string Output;
if(!P4Output(out Output, "", String.Format("interchanges {0}-S {1} -F", bReverse? "-r " : "", StreamName), Input:null, AllowSpew:false))
{
throw new AutomationException("Couldn't get unintegrated stream changes from {0}", StreamName);
}
List Changelists = new List();
if(!Output.StartsWith("All revision(s) already integrated"))
{
foreach(string Line in Output.Split('\n'))
{
string[] Tokens = Line.Split(new char[]{ ' ' }, StringSplitOptions.RemoveEmptyEntries);
if(Tokens.Length > 0)
{
int Changelist;
if(Tokens[0] != "Change" || !int.TryParse(Tokens[1], out Changelist))
{
throw new AutomationException("Unexpected output from p4 interchanges: {0}", Line);
}
Changelists.Add(Changelist);
}
}
}
return Changelists;
}
///
/// Execute the 'filelog' command
///
/// Options for the command
/// List of file specifications to query
/// List of file records
public P4FileRecord[] FileLog(P4FileLogOptions Options, params string[] FileSpecs)
{
return FileLog(-1, -1, Options, FileSpecs);
}
///
/// Execute the 'filelog' command
///
/// Number of changelists to show. Ignored if zero or negative.
/// Options for the command
/// List of file specifications to query
/// List of file records
public P4FileRecord[] FileLog(int MaxChanges, P4FileLogOptions Options, params string[] FileSpecs)
{
return FileLog(-1, MaxChanges, Options, FileSpecs);
}
///
/// Execute the 'filelog' command
///
/// Show only files modified by this changelist. Ignored if zero or negative.
/// Number of changelists to show. Ignored if zero or negative.
/// Options for the command
/// List of file specifications to query
/// List of file records
public P4FileRecord[] FileLog(int ChangeNumber, int MaxChanges, P4FileLogOptions Options, params string[] FileSpecs)
{
P4FileRecord[] Records;
P4ReturnCode ReturnCode = TryFileLog(ChangeNumber, MaxChanges, Options, FileSpecs, out Records);
if(ReturnCode != null)
{
if(ReturnCode.Generic == P4GenericCode.Empty)
{
return new P4FileRecord[0];
}
else
{
throw new P4Exception(ReturnCode.ToString());
}
}
return Records;
}
///
/// Execute the 'filelog' command
///
/// Show only files modified by this changelist. Ignored if zero or negative.
/// Number of changelists to show. Ignored if zero or negative.
/// Options for the command
/// List of file specifications to query
///
/// List of file records
public P4ReturnCode TryFileLog(int ChangeNumber, int MaxChanges, P4FileLogOptions Options, string[] FileSpecs, out P4FileRecord[] OutRecords)
{
// Build the argument list
List Arguments = new List();
if(ChangeNumber > 0)
{
Arguments.Add(String.Format("-c {0}", ChangeNumber));
}
if((Options & P4FileLogOptions.ContentHistory) != 0)
{
Arguments.Add("-h");
}
if((Options & P4FileLogOptions.FollowAcrossBranches) != 0)
{
Arguments.Add("-i");
}
if((Options & P4FileLogOptions.FullDescriptions) != 0)
{
Arguments.Add("-l");
}
if((Options & P4FileLogOptions.LongDescriptions) != 0)
{
Arguments.Add("-L");
}
if(MaxChanges > 0)
{
Arguments.Add(String.Format("-m {0}", MaxChanges));
}
if((Options & P4FileLogOptions.DoNotFollowPromotedTaskStreams) != 0)
{
Arguments.Add("-p");
}
if((Options & P4FileLogOptions.IgnoreNonContributoryIntegrations) != 0)
{
Arguments.Add("-s");
}
// Always include times to simplify parsing
Arguments.Add("-t");
// Add the file arguments
foreach(string FileSpec in FileSpecs)
{
Arguments.Add(CommandUtils.MakePathSafeToUseWithCommandLine(FileSpec));
}
// Format the full command line
string CommandLine = String.Format("filelog {0}", String.Join(" ", Arguments));
// Get the output
List> RawRecords = P4TaggedOutput(CommandLine);
// Parse all the output
List Records = new List();
foreach(Dictionary RawRecord in RawRecords)
{
// Make sure the record has the correct return value
P4ReturnCode OtherReturnCode;
if(!VerifyReturnCode(RawRecord, "stat", out OtherReturnCode))
{
OutRecords = null;
return OtherReturnCode;
}
// Get the depot path for this revision
string DepotPath = RawRecord["depotFile"];
// Parse the revisions
List Revisions = new List();
for(;;)
{
string RevisionSuffix = String.Format("{0}", Revisions.Count);
string RevisionNumberText;
if(!RawRecord.TryGetValue("rev" + RevisionSuffix, out RevisionNumberText))
{
break;
}
int RevisionNumber = int.Parse(RevisionNumberText);
int RevisionChangeNumber = int.Parse(RawRecord["change" + RevisionSuffix]);
P4Action Action = ParseActionText(RawRecord["action" + RevisionSuffix]);
DateTime DateTime = UnixEpoch + TimeSpan.FromSeconds(long.Parse(RawRecord["time" + RevisionSuffix]));
string Type = RawRecord["type" + RevisionSuffix];
string UserName = RawRecord["user" + RevisionSuffix];
string ClientName = RawRecord["client" + RevisionSuffix];
long FileSize = RawRecord.ContainsKey("fileSize" + RevisionSuffix)? long.Parse(RawRecord["fileSize" + RevisionSuffix]) : -1;
string Digest = RawRecord.ContainsKey("digest" + RevisionSuffix)? RawRecord["digest" + RevisionSuffix] : null;
string Description = RawRecord["desc" + RevisionSuffix];
// Parse all the following integration info
List Integrations = new List();
for(;;)
{
string IntegrationSuffix = String.Format("{0},{1}", Revisions.Count, Integrations.Count);
string HowText;
if(!RawRecord.TryGetValue("how" + IntegrationSuffix, out HowText))
{
break;
}
P4IntegrateAction IntegrateAction = ParseIntegrateActionText(HowText);
string OtherFile = RawRecord["file" + IntegrationSuffix];
string StartRevisionText = RawRecord["srev" + IntegrationSuffix];
string EndRevisionText = RawRecord["erev" + IntegrationSuffix];
int StartRevisionNumber = (StartRevisionText == "#none")? 0 : int.Parse(StartRevisionText.Substring(1));
int EndRevisionNumber = int.Parse(EndRevisionText.Substring(1));
Integrations.Add(new P4IntegrationRecord(IntegrateAction, OtherFile, StartRevisionNumber, EndRevisionNumber));
}
// Add the revision
Revisions.Add(new P4RevisionRecord(RevisionNumber, RevisionChangeNumber, Action, Type, DateTime, UserName, ClientName, FileSize, Digest, Description, Integrations.ToArray()));
}
// Add the file record
Records.Add(new P4FileRecord(DepotPath, Revisions.ToArray()));
}
OutRecords = Records.ToArray();
return null;
}
static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
///
/// Enumerates all streams in a depot
///
/// The path for streams to enumerate (eg. "//UE5/...")
/// List of streams matching the given criteria
public List Streams(string StreamPath)
{
return Streams(StreamPath, -1, null, false);
}
///
/// Enumerates all streams in a depot
///
/// The path for streams to enumerate (eg. "//UE5/...")
/// Maximum number of results to return
/// Additional filter to be applied to the results
/// Whether to enumerate unloaded workspaces
/// List of streams matching the given criteria
public List Streams(string StreamPath, int MaxResults, string Filter, bool bUnloaded)
{
// Build the command line
StringBuilder CommandLine = new StringBuilder("streams");
if(bUnloaded)
{
CommandLine.Append(" -U");
}
if(Filter != null)
{
CommandLine.AppendFormat(" -F \"{0}\"", Filter);
}
if(MaxResults > 0)
{
CommandLine.AppendFormat(" -m {0}", MaxResults);
}
CommandLine.AppendFormat(" \"{0}\"", StreamPath);
// Execute the command
List> RawRecords = P4TaggedOutput(CommandLine.ToString(), false);
// Parse the output
List Records = new List();
foreach(Dictionary RawRecord in RawRecords)
{
// Make sure the record has the correct return value
P4ReturnCode OtherReturnCode;
if(!VerifyReturnCode(RawRecord, "stat", out OtherReturnCode))
{
throw new P4Exception(OtherReturnCode.ToString());
}
// Parse the fields
string Stream = RawRecord["Stream"];
DateTime Update = UnixEpoch + TimeSpan.FromSeconds(long.Parse(RawRecord["Update"]));
DateTime Access = UnixEpoch + TimeSpan.FromSeconds(long.Parse(RawRecord["Access"]));
string Owner = RawRecord["Owner"];
string Name = RawRecord["Name"];
string Parent = RawRecord["Parent"];
P4StreamType Type = (P4StreamType)Enum.Parse(typeof(P4StreamType), RawRecord["Type"], true);
string Description = RawRecord["desc"];
P4StreamOptions Options = ParseStreamOptions(RawRecord["Options"]);
Nullable FirmerThanParent = ParseNullableBool(RawRecord["firmerThanParent"]);
bool ChangeFlowsToParent = bool.Parse(RawRecord["changeFlowsToParent"]);
bool ChangeFlowsFromParent = bool.Parse(RawRecord["changeFlowsFromParent"]);
string BaseParent = RawRecord["baseParent"];
// Add the new stream record
Records.Add(new P4StreamRecord(Stream, Update, Access, Owner, Name, Parent, Type, Description, Options, FirmerThanParent, ChangeFlowsToParent, ChangeFlowsFromParent, BaseParent));
}
return Records;
}
static readonly Regex HaveFilesOutputPattern = new Regex(@"(?.+)#(?\d+) - (?.+)", RegexOptions.Compiled);
///
/// Get a information about synced files (p4 have) matching the given pattern.
/// The information for each file includes the local path, depot path and the synced revision number.
///
/// Local or depot path e.g. .../Source/...
/// Collection of information about synced files.
public List HaveFiles(string Pattern)
{
List Files = new ();
string CommandToRun = $"have \"{Pattern}\"";
string CommandLine = $"{GlobalOptions} {CommandToRun}";
// We run the process manually because 'p4 have' may generate a very long output (even hundreds of MB)
// and gathering all of this into a single string is wasteful. Instead, we process the output line by line
// as it arrives. Neither P4Output nor CommandUtils.Run allow achieving this.
Process Process = new Process();
Process.StartInfo.FileName = HostPlatform.Current.P4Exe;
Process.StartInfo.Arguments = CommandLine;
Process.StartInfo.UseShellExecute = false;
Process.StartInfo.RedirectStandardOutput = true;
Process.StartInfo.StandardOutputEncoding = Encoding.UTF8;
Process.StartInfo.RedirectStandardError = false;
Process.StartInfo.RedirectStandardInput = false;
Process.StartInfo.CreateNoWindow = true;
Process.OutputDataReceived += (s, e) =>
{
if (e.Data != null)
{
Match Match = HaveFilesOutputPattern.Match(e.Data);
if (Match.Success)
{
Files.Add(new P4HaveRecord(Match.Groups["depot"].Value, Match.Groups["client"].Value, Int32.Parse(Match.Groups["revision"].Value)));
}
}
};
Process.Start();
Process.BeginOutputReadLine();
Process.WaitForExit();
if (Process.ExitCode != 0)
{
throw new P4Exception($"p4.exe {CommandLine} failed with code {Process.ExitCode}.");
}
return Files;
}
///
/// Parse a nullable boolean
///
/// Text to parse. May be "true", "false", or "n/a".
/// The parsed boolean
static Nullable ParseNullableBool(string Text)
{
switch(Text)
{
case "true":
return true;
case "false":
return false;
case "n/a":
return null;
default:
throw new P4Exception("Invalid value for nullable bool: {0}", Text);
}
}
///
/// Parse a list of stream option flags
///
/// Text to parse
/// Flags for the stream options
static P4StreamOptions ParseStreamOptions(string Text)
{
P4StreamOptions Options = 0;
foreach(string Option in Text.Split(' '))
{
switch(Option)
{
case "locked":
Options |= P4StreamOptions.Locked;
break;
case "ownersubmit":
Options |= P4StreamOptions.OwnerSubmit;
break;
case "toparent":
Options |= P4StreamOptions.ToParent;
break;
case "fromparent":
Options |= P4StreamOptions.FromParent;
break;
case "mergedown":
Options |= P4StreamOptions.MergeDown;
break;
case "unlocked":
case "allsubmit":
case "notoparent":
case "nofromparent":
case "mergeany":
break;
default:
throw new P4Exception("Unknown stream option '{0}'", Option);
}
}
return Options;
}
static Dictionary GetEnumLookup()
{
Dictionary Lookup = new Dictionary();
foreach(T Value in Enum.GetValues(typeof(T)))
{
foreach(MemberInfo Member in typeof(T).GetMember(Value.ToString()))
{
string Description = Member.GetCustomAttribute().Description;
Lookup.Add(Description, Value);
}
}
return Lookup;
}
static Lazy> DescriptionToAction = new Lazy>(() => GetEnumLookup());
static P4Action ParseActionText(string ActionText)
{
P4Action Action;
if(!DescriptionToAction.Value.TryGetValue(ActionText, out Action))
{
throw new P4Exception("Invalid action '{0}'", Action);
}
return Action;
}
static Lazy> DescriptionToIntegrationAction = new Lazy>(() => GetEnumLookup());
static P4IntegrateAction ParseIntegrateActionText(string ActionText)
{
P4IntegrateAction Action;
if(!DescriptionToIntegrationAction.Value.TryGetValue(ActionText, out Action))
{
throw new P4Exception("Invalid integration action '{0}'", ActionText);
}
return Action;
}
static DateTime ParseDateTime(string DateTimeText)
{
return DateTime.ParseExact(DateTimeText, "yyyy/MM/dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture);
}
///
/// For a given file (and revision, potentially), returns where it was integrated from. Useful in conjunction with files in a P4DescribeRecord, with action = "integrate".
///
/// The file to check. May have a revision specifier at the end (eg. //depot/UE4/foo.cpp#2)
/// The file that it was integrated from, without a revision specifier
public string GetIntegrationSource(string DepotPath)
{
string Output;
if(P4Output(out Output, "", "filelog -m 1 \"" + DepotPath + "\"", Input:null, AllowSpew:false))
{
foreach(string Line in Output.Split('\n').Select(x => x.Trim()))
{
const string MergePrefix = "... ... merge from ";
if(Line.StartsWith(MergePrefix))
{
return Line.Substring(MergePrefix.Length, Line.LastIndexOf('#') - MergePrefix.Length);
}
const string CopyPrefix = "... ... copy from ";
if(Line.StartsWith(CopyPrefix))
{
return Line.Substring(CopyPrefix.Length, Line.LastIndexOf('#') - CopyPrefix.Length);
}
const string EditPrefix = "... ... edit from ";
if (Line.StartsWith(EditPrefix))
{
return Line.Substring(EditPrefix.Length, Line.LastIndexOf('#') - EditPrefix.Length);
}
}
}
return null;
}
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;
}
}
}
}
}
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