Files
UnrealEngine/Engine/Source/Programs/AutomationTool/AutomationUtils/P4Utils.cs
2025-05-18 13:04:45 +08:00

4512 lines
147 KiB
C#

// 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
{
/// <summary>
/// Declares that the command type requires P4Environment.
/// </summary>
public class RequireP4Attribute : Attribute
{
}
/// <summary>
/// Declares that the command type does not access Changelist or CodeChangelist from P4Environment.
/// </summary>
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<KeyValuePair<string, string>> View = new List<KeyValuePair<string, string>>();
public bool Matches(P4ClientInfo Other)
{
return Name == Other.Name
&& RootPath == Other.RootPath
&& Host == Other.Host
&& Owner == Other.Owner
&& 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<KeyValuePair<string, string>> Sections;
/// <summary>
/// Default constructor.
/// </summary>
public P4Spec()
{
Sections = new List<KeyValuePair<string,string>>();
}
/// <summary>
/// Gets the current value of a field with the given name
/// </summary>
/// <param name="Name">Name of the field to search for</param>
/// <returns>The value of the field, or null if it does not exist</returns>
public string GetField(string Name)
{
int Idx = Sections.FindIndex(x => x.Key == Name);
return (Idx == -1)? null : Sections[Idx].Value;
}
/// <summary>
/// Sets the value of an existing field, or adds a new one with the given name
/// </summary>
/// <param name="Name">Name of the field to set</param>
/// <param name="Value">New value of the field</param>
public void SetField(string Name, string Value)
{
int Idx = Sections.FindIndex(x => x.Key == Name);
if(Idx == -1)
{
Sections.Add(new KeyValuePair<string,string>(Name, Value));
}
else
{
Sections[Idx] = new KeyValuePair<string,string>(Name, Value);
}
}
/// <summary>
/// Parses a spec (clientspec, branchspec, changespec) from an array of lines
/// </summary>
/// <param name="Text">Text split into separate lines</param>
/// <returns>Array of section names and values</returns>
public static P4Spec FromString(string Text)
{
P4Spec Spec = new P4Spec();
string[] Lines = Text.Split('\n');
for(int LineIdx = 0; LineIdx < Lines.Length; LineIdx++)
{
if(Lines[LineIdx].EndsWith("\r"))
{
Lines[LineIdx] = Lines[LineIdx].Substring(0, Lines[LineIdx].Length - 1);
}
if(!String.IsNullOrWhiteSpace(Lines[LineIdx]) && !Lines[LineIdx].StartsWith("#"))
{
// Read the section name
int SeparatorIdx = Lines[LineIdx].IndexOf(':');
if(SeparatorIdx == -1 || !Char.IsLetter(Lines[LineIdx][0]))
{
throw new P4Exception("Invalid spec format at line {0}: \"{1}\"", LineIdx, Lines[LineIdx]);
}
// Get the section name
string SectionName = Lines[LineIdx].Substring(0, SeparatorIdx);
// Parse the section value
StringBuilder Value = new StringBuilder(Lines[LineIdx].Substring(SeparatorIdx + 1));
for(; LineIdx + 1 < Lines.Length; LineIdx++)
{
if(Lines[LineIdx + 1].Length == 0)
{
Value.AppendLine();
}
else if(Lines[LineIdx + 1][0] == '\t')
{
Value.AppendLine(Lines[LineIdx + 1].Substring(1));
}
else
{
break;
}
}
Spec.Sections.Add(new KeyValuePair<string,string>(SectionName, Value.ToString().TrimEnd()));
}
}
return Spec;
}
/// <summary>
/// Formats a P4 specification as a block of text
/// </summary>
/// <returns></returns>
public override string ToString()
{
StringBuilder Result = new StringBuilder();
foreach(KeyValuePair<string, string> Section in Sections)
{
if(Section.Value.Contains('\n'))
{
Result.AppendLine(Section.Key + ":\n\t" + Section.Value.Replace("\n", "\n\t"));
}
else
{
Result.AppendLine(Section.Key + ":\t" + Section.Value);
}
Result.AppendLine();
}
return Result.ToString();
}
}
/// <summary>
/// Describes the action performed by the user when resolving the integration
/// </summary>
public enum P4IntegrateAction
{
/// <summary>
/// file did not previously exist; it was created as a copy of partner-file
/// </summary>
[Description("branch from")]
BranchFrom,
/// <summary>
/// partner-file did not previously exist; it was created as a copy of file.
/// </summary>
[Description("branch into")]
BranchInto,
/// <summary>
/// file was integrated from partner-file, accepting merge.
/// </summary>
[Description("merge from")]
MergeFrom,
/// <summary>
/// file was integrated into partner-file, accepting merge.
/// </summary>
[Description("merge into")]
MergeInto,
/// <summary>
/// file was integrated from partner-file, accepting theirs and deleting the original.
/// </summary>
[Description("moved from")]
MovedFrom,
/// <summary>
/// file was integrated into partner-file, accepting theirs and creating partner-file if it did not previously exist.
/// </summary>
[Description("moved into")]
MovedInto,
/// <summary>
/// file was integrated from partner-file, accepting theirs.
/// </summary>
[Description("copy from")]
CopyFrom,
/// <summary>
/// file was integrated into partner-file, accepting theirs.
/// </summary>
[Description("copy into")]
CopyInto,
/// <summary>
/// file was integrated from partner-file, accepting yours.
/// </summary>
[Description("ignored")]
Ignored,
/// <summary>
/// file was integrated into partner-file, accepting yours.
/// </summary>
[Description("ignored by")]
IgnoredBy,
/// <summary>
/// file was integrated from partner-file, and partner-file had been previously deleted.
/// </summary>
[Description("delete from")]
DeleteFrom,
/// <summary>
/// file was integrated into partner-file, and file had been previously deleted.
/// </summary>
[Description("delete into")]
DeleteInto,
/// <summary>
/// file was integrated from partner-file, and file was edited within the p4 resolve process.
/// </summary>
[Description("edit from")]
EditFrom,
/// <summary>
/// file was integrated into partner-file, and partner-file was reopened for edit before submission.
/// </summary>
[Description("edit into")]
EditInto,
/// <summary>
/// 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).
/// </summary>
[Description("add from")]
AddFrom,
/// <summary>
/// file was integrated into previously nonexistent partner-file, and partner-file was reopened for add before submission.
/// </summary>
[Description("add into")]
AddInto,
/// <summary>
/// file was reverted to a previous revision
/// </summary>
[Description("undid")]
Undid,
/// <summary>
/// file was reverted to a previous revision
/// </summary>
[Description("undone by")]
UndoneBy
}
/// <summary>
/// Stores integration information for a file revision
/// </summary>
public class P4IntegrationRecord
{
/// <summary>
/// The integration action performed for this file
/// </summary>
public readonly P4IntegrateAction Action;
/// <summary>
/// The partner file for this integration
/// </summary>
public readonly string OtherFile;
/// <summary>
/// Min revision of the partner file for this integration
/// </summary>
public readonly int StartRevisionNumber;
/// <summary>
/// Max revision of the partner file for this integration
/// </summary>
public readonly int EndRevisionNumber;
/// <summary>
/// Constructor
/// </summary>
/// <param name="Action">The integration action</param>
/// <param name="OtherFile">The partner file involved in the integration</param>
/// <param name="StartRevisionNumber">Starting revision of the partner file for the integration (exclusive)</param>
/// <param name="EndRevisionNumber">Ending revision of the partner file for the integration (inclusive)</param>
public P4IntegrationRecord(P4IntegrateAction Action, string OtherFile, int StartRevisionNumber, int EndRevisionNumber)
{
this.Action = Action;
this.OtherFile = OtherFile;
this.StartRevisionNumber = StartRevisionNumber;
this.EndRevisionNumber = EndRevisionNumber;
}
/// <summary>
/// Summarize this record for display in the debugger
/// </summary>
/// <returns>Formatted integration record</returns>
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);
}
}
}
/// <summary>
/// Stores a revision record for a file
/// </summary>
public class P4RevisionRecord
{
/// <summary>
/// The revision number of this file
/// </summary>
public readonly int RevisionNumber;
/// <summary>
/// The changelist responsible for this revision of the file
/// </summary>
public readonly int ChangeNumber;
/// <summary>
/// Action performed to the file in this revision
/// </summary>
public readonly P4Action Action;
/// <summary>
/// Type of the file
/// </summary>
public readonly string Type;
/// <summary>
/// Timestamp of this modification
/// </summary>
public readonly DateTime DateTime;
/// <summary>
/// Author of the changelist
/// </summary>
public readonly string UserName;
/// <summary>
/// Client that submitted this changelist
/// </summary>
public readonly string ClientName;
/// <summary>
/// Size of the file, or -1 if not specified
/// </summary>
public readonly long FileSize;
/// <summary>
/// Digest of the file, or null if not specified
/// </summary>
public readonly string Digest;
/// <summary>
/// Description of this changelist
/// </summary>
public readonly string Description;
/// <summary>
/// Integration records for this revision
/// </summary>
public readonly P4IntegrationRecord[] Integrations;
/// <summary>
/// Constructor
/// </summary>
/// <param name="RevisionNumber">Revision number of the file</param>
/// <param name="ChangeNumber">Number of the changelist that submitted this revision</param>
/// <param name="Action">Action performed to the file in this changelist</param>
/// <param name="Type">Type of the file</param>
/// <param name="DateTime">Timestamp for the change</param>
/// <param name="UserName">User that submitted the change</param>
/// <param name="ClientName">Client that submitted the change</param>
/// <param name="FileSize">Size of the file, or -1 if not specified</param>
/// <param name="Digest">Digest of the file, or null if not specified</param>
/// <param name="Description">Description of the changelist</param>
/// <param name="Integrations">Integrations performed to the file</param>
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;
}
/// <summary>
/// Format this record for display in the debugger
/// </summary>
/// <returns>Summary of this revision</returns>
public override string ToString()
{
return String.Format("#{0} change {1} {2} on {3} by {4}@{5}", RevisionNumber, ChangeNumber, Action, DateTime, UserName, ClientName);
}
}
/// <summary>
/// Record output by the filelog command
/// </summary>
public class P4FileRecord
{
/// <summary>
/// Path to the file in the depot
/// </summary>
public string DepotPath;
/// <summary>
/// Revisions of this file
/// </summary>
public P4RevisionRecord[] Revisions;
/// <summary>
/// Constructor
/// </summary>
/// <param name="DepotPath">The depot path of the file</param>
/// <param name="Revisions">Revisions of the file</param>
public P4FileRecord(string DepotPath, P4RevisionRecord[] Revisions)
{
this.DepotPath = DepotPath;
this.Revisions = Revisions;
}
/// <summary>
/// Return the depot path of the file for display in the debugger
/// </summary>
/// <returns>Path to the file</returns>
public override string ToString()
{
return DepotPath;
}
}
/// <summary>
/// Options for the filelog command
/// </summary>
[Flags]
public enum P4FileLogOptions
{
/// <summary>
/// No options
/// </summary>
None = 0,
/// <summary>
/// Display file content history instead of file name history.
/// </summary>
ContentHistory = 1,
/// <summary>
/// Follow file history across branches.
/// </summary>
FollowAcrossBranches = 2,
/// <summary>
/// List long output, with the full text of each changelist description.
/// </summary>
FullDescriptions = 4,
/// <summary>
/// List long output, with the full text of each changelist description truncated at 250 characters.
/// </summary>
LongDescriptions = 8,
/// <summary>
/// When used with the ContentHistory option, do not follow content of promoted task streams.
/// </summary>
DoNotFollowPromotedTaskStreams = 16,
/// <summary>
/// Display a shortened form of output by ignoring non-contributory integrations
/// </summary>
IgnoreNonContributoryIntegrations = 32,
}
/// <summary>
/// Type of a Perforce stream
/// </summary>
public enum P4StreamType
{
/// <summary>
/// A mainline stream
/// </summary>
Mainline,
/// <summary>
/// A development stream
/// </summary>
Development,
/// <summary>
/// A release stream
/// </summary>
Release,
/// <summary>
/// A virtual stream
/// </summary>
Virtual,
/// <summary>
/// A task stream
/// </summary>
Task,
}
/// <summary>
/// Options for a stream definition
/// </summary>
[Flags]
public enum P4StreamOptions
{
/// <summary>
/// The stream is locked
/// </summary>
Locked = 1,
/// <summary>
/// Only the owner may submit to the stream
/// </summary>
OwnerSubmit = 4,
/// <summary>
/// Integrations from this stream to its parent are expected
/// </summary>
ToParent = 4,
/// <summary>
/// Integrations from this stream from its parent are expected
/// </summary>
FromParent = 8,
/// <summary>
/// Undocumented?
/// </summary>
MergeDown = 16,
}
/// <summary>
/// Contains information about a stream, as returned by the 'p4 streams' command
/// </summary>
[DebuggerDisplay("{Stream}")]
public class P4StreamRecord
{
/// <summary>
/// Path to the stream
/// </summary>
public string Stream;
/// <summary>
/// Last time the stream definition was updated
/// </summary>
public DateTime Update;
/// <summary>
/// Last time the stream definition was accessed
/// </summary>
public DateTime Access;
/// <summary>
/// Owner of this stream
/// </summary>
public string Owner;
/// <summary>
/// Name of the stream. This may be modified after the stream is initially created, but it's underlying depot path will not change.
/// </summary>
public string Name;
/// <summary>
/// The parent stream
/// </summary>
public string Parent;
/// <summary>
/// Type of the stream
/// </summary>
public P4StreamType Type;
/// <summary>
/// User supplied description of the stream
/// </summary>
public string Description;
/// <summary>
/// Options for the stream definition
/// </summary>
public P4StreamOptions Options;
/// <summary>
/// Whether this stream is more stable than the parent stream
/// </summary>
public Nullable<bool> FirmerThanParent;
/// <summary>
/// Whether changes from this stream flow to the parent stream
/// </summary>
public bool ChangeFlowsToParent;
/// <summary>
/// Whether changes from this stream flow from the parent stream
/// </summary>
public bool ChangeFlowsFromParent;
/// <summary>
/// The mainline branch associated with this stream
/// </summary>
public string BaseParent;
/// <summary>
/// Constructor
/// </summary>
/// <param name="Stream">Path to the stream</param>
/// <param name="Update">Last time the stream definition was updated</param>
/// <param name="Access">Last time the stream definition was accessed</param>
/// <param name="Owner">Owner of this stream</param>
/// <param name="Name">Name of the stream. This may be modified after the stream is initially created, but it's underlying depot path will not change.</param>
/// <param name="Parent">The parent stream</param>
/// <param name="Type">Type of the stream</param>
/// <param name="Description">User supplied description of the stream</param>
/// <param name="Options">Options for the stream definition</param>
/// <param name="FirmerThanParent">Whether this stream is more stable than the parent stream</param>
/// <param name="ChangeFlowsToParent">Whether changes from this stream flow to the parent stream</param>
/// <param name="ChangeFlowsFromParent">Whether changes from this stream flow from the parent stream</param>
/// <param name="BaseParent">The mainline branch associated with this stream</param>
public P4StreamRecord(string Stream, DateTime Update, DateTime Access, string Owner, string Name, string Parent, P4StreamType Type, string Description, P4StreamOptions Options, Nullable<bool> 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;
}
/// <summary>
/// Return the path of this stream for display in the debugger
/// </summary>
/// <returns>Path to this stream</returns>
public override string ToString()
{
return Stream;
}
}
/// <summary>
/// Error severity codes. Taken from the p4java documentation.
/// </summary>
public enum P4SeverityCode
{
Empty = 0,
Info = 1,
Warning = 2,
Failed = 3,
Fatal = 4,
}
/// <summary>
/// Generic error codes that can be returned by the Perforce server. Taken from the p4java documentation.
/// </summary>
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,
}
/// <summary>
/// Represents a error return value from Perforce.
/// </summary>
public class P4ReturnCode
{
/// <summary>
/// The value of the "code" field returned by the server
/// </summary>
public string Code;
/// <summary>
/// The severity of this error
/// </summary>
public P4SeverityCode Severity;
/// <summary>
/// The generic error code associated with this message
/// </summary>
public P4GenericCode Generic;
/// <summary>
/// The message text
/// </summary>
public string Message;
/// <summary>
/// Constructor
/// </summary>
/// <param name="Code">The value of the "code" field returned by the server</param>
/// <param name="Severity">The severity of this error</param>
/// <param name="Generic">The generic error code associated with this message</param>
/// <param name="Message">The message text</param>
public P4ReturnCode(string Code, P4SeverityCode Severity, P4GenericCode Generic, string Message)
{
this.Code = Code;
this.Severity = Severity;
this.Generic = Generic;
this.Message = Message;
}
/// <summary>
/// Formats this error for display in the debugger
/// </summary>
/// <returns>String representation of this object</returns>
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;
}
}
/// <summary>
/// Initializes build environment. If the build command needs a specific env-var mapping or
/// has an extended BuildEnvironment, it must implement this method accordingly.
/// </summary>
static internal void InitP4Environment()
{
// 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;
}
/// <summary>
/// Initializes default source control connection.
/// </summary>
static internal void InitDefaultP4Connection()
{
PerforceConnection = new P4Connection(User: P4Env.User, Client: P4Env.Client, ServerAndPort: P4Env.ServerAndPort);
}
/// <summary>
/// Check if P4 is supported.
/// </summary>
public static bool P4Enabled
{
get
{
if (!bP4Enabled.HasValue)
{
throw new AutomationException("Trying to access P4Enabled property before it was initialized.");
}
return (bool)bP4Enabled;
}
private set
{
bP4Enabled = value;
}
}
private static bool? bP4Enabled;
/// <summary>
/// Check if P4CL is required.
/// </summary>
public static bool P4CLRequired
{
get
{
if (!bP4CLRequired.HasValue)
{
throw new AutomationException("Trying to access P4CLRequired property before it was initialized.");
}
return (bool)bP4CLRequired;
}
private set
{
bP4CLRequired = value;
}
}
private static bool? bP4CLRequired;
/// <summary>
/// Checks whether commands are allowed to submit files into P4.
/// </summary>
public static bool AllowSubmit
{
get
{
if (!bAllowSubmit.HasValue)
{
throw new AutomationException("Trying to access AllowSubmit property before it was initialized.");
}
return (bool)bAllowSubmit;
}
private set
{
bAllowSubmit = value;
}
}
private static bool? bAllowSubmit;
/// <summary>
/// Sets up P4Enabled, AllowSubmit properties. Note that this does not initialize P4 environment.
/// </summary>
/// <param name="CommandsToExecute">Commands to execute</param>
/// <param name="Commands">Commands</param>
internal static void InitP4Support(List<CommandInfo> CommandsToExecute, Dictionary<string, Type> Commands)
{
// Init AllowSubmit
// If we do not specify on the commandline if submitting is allowed or not, this is
// depending on whether we run locally or on a build machine.
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);
}
/// <summary>
/// Checks if any of the commands to execute has [RequireP4] attribute.
/// </summary>
/// <param name="CommandsToExecute">List of commands to be executed.</param>
/// <param name="Commands">Commands.</param>
/// <param name="bRequireP4"></param>
/// <param name="bRequireCL"></param>
private static void CheckIfCommandsRequireP4(List<CommandInfo> CommandsToExecute, Dictionary<string, Type> Commands, out bool bRequireP4, out bool bRequireCL)
{
bRequireP4 = false;
bRequireCL = false;
foreach (var CommandInfo in CommandsToExecute)
{
Type Command;
if (Commands.TryGetValue(CommandInfo.CommandName, out Command))
{
var RequireP4Attributes = Command.GetCustomAttributes(typeof(RequireP4Attribute), true);
if (!CommandUtils.IsNullOrEmpty(RequireP4Attributes))
{
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;
}
}
}
}
}
}
/// <summary>
/// Class that stores labels info.
/// </summary>
public class P4Label
{
// The name of the label.
public string Name { get; private set; }
// The date of the label.
public DateTime Date { get; private set; }
public P4Label(string Name, DateTime Date)
{
this.Name = Name;
this.Date = Date;
}
}
/// <summary>
/// Perforce connection.
/// </summary>
public partial class P4Connection
{
/// <summary>
/// List of global options for this connection (client/user)
/// </summary>
private string GlobalOptions;
/// <summary>
/// List of global options for this connection (client/user)
/// </summary>
private string GlobalOptionsWithoutClient;
/// <summary>
/// Path where this connection's log is to go to
/// </summary>
public string LogPath { get; private set; }
/// <summary>
/// Initializes P4 connection
/// </summary>
/// <param name="User">Username (can be null, in which case the environment variable default will be used)</param>
/// <param name="Client">Workspace (can be null, in which case the environment variable default will be used)</param>
/// <param name="ServerAndPort">Server:Port (can be null, in which case the environment variable default will be used)</param>
/// <param name="P4LogPath">Log filename (can be null, in which case CmdEnv.LogFolder/p4.log will be used)</param>
/// <param name="AdditionalOpts">Additional global options to include on every p4 command line</param>
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;
}
}
/// <summary>
/// A filter that suppresses all output od stdout/stderr
/// </summary>
/// <param name="Message"></param>
/// <returns></returns>
static string NoSpewFilter(string Message)
{
return null;
}
/// <summary>
/// Shortcut to Run but with P4.exe as the program name.
/// </summary>
/// <param name="CommandLine">Command line</param>
/// <param name="Input">Stdin</param>
/// <param name="AllowSpew">true for spew</param>
/// <param name="WithClient"></param>
/// <param name="SpewIsVerbose"></param>
/// <returns>Exit code</returns>
public IProcessResult P4(string CommandLine, string Input = null, bool AllowSpew = true, bool WithClient = true, bool SpewIsVerbose = false)
{
return P4("", CommandLine, Input, AllowSpew, WithClient, SpewIsVerbose);
}
/// <summary>
/// Shortcut to Run but with P4.exe as the program name.
/// </summary>
/// <param name="ExtraGlobalOptions">Extra global options just for this command</param>
/// <param name="CommandLine">Command line</param>
/// <param name="Input">Stdin</param>
/// <param name="AllowSpew">true for spew</param>
/// <param name="WithClient"></param>
/// <param name="SpewIsVerbose"></param>
/// <returns>Exit code</returns>
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 <paramsfile> 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;
}
/// <summary>
/// Calls p4 and returns the output.
/// </summary>
/// <param name="Output">Output of the command.</param>
/// <param name="ExtraGlobalOptions"></param>
/// <param name="CommandLine">Commandline for p4.</param>
/// <param name="Input">Stdin input.</param>
/// <param name="AllowSpew">Whether the command should spew.</param>
/// <param name="WithClient"></param>
/// <returns>True if succeeded, otherwise false.</returns>
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;
}
/// <summary>
/// Calls p4 and returns the output.
/// </summary>
/// <param name="OutputLines">Output of the command.</param>
/// <param name="ExtraGlobalOptions"></param>
/// <param name="CommandLine">Commandline for p4.</param>
/// <param name="Input">Stdin input.</param>
/// <param name="AllowSpew">Whether the command should spew.</param>
/// <param name="WithClient"></param>
/// <returns>True if succeeded, otherwise false.</returns>
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<string> Lines = new List<string>();
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;
}
/// <summary>
/// Calls p4 command and writes the output to a logfile.
/// </summary>
/// <param name="ExtraGlobalOptions"></param>
/// <param name="CommandLine">Commandline to pass to p4.</param>
/// <param name="Input">Stdin input.</param>
/// <param name="AllowSpew">Whether the command is allowed to spew.</param>
/// <param name="WithClient"></param>
/// <param name="SpewIsVerbose"></param>
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);
}
}
/// <summary>
/// Calls p4 and returns the output and writes it also to a logfile.
/// </summary>
/// <param name="Output">Output of the comman.</param>
/// <param name="ExtraGlobalOptions"></param>
/// <param name="CommandLine">Commandline for p4.</param>
/// <param name="Input">Stdin input.</param>
/// <param name="AllowSpew">Whether the command should spew.</param>
/// <param name="WithClient"></param>
/// <param name="SpewIsVerbose"></param>
/// <returns>True if succeeded, otherwise false.</returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="CommandLine">Command line to execute Perforce with</param>
/// <param name="WithClient">Whether to include client information on the command line</param>
public List<Dictionary<string, string>> 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<Dictionary<string, string>> Records = new List<Dictionary<string, string>>();
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<string, string> Record = new Dictionary<string, string>();
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;
}
/// <summary>
/// Checks that the raw record data includes the given return code, or creates a ReturnCode value if it doesn't
/// </summary>
/// <param name="RawRecord">The raw record data</param>
/// <param name="ExpectedCode">The expected code value</param>
/// <param name="OtherReturnCode">Output variable for receiving the return code if it doesn't match</param>
public static bool VerifyReturnCode(Dictionary<string, string> 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;
}
}
/// <summary>
/// Invokes p4 login command.
/// </summary>
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;
}
/// <summary>
/// Invokes p4 changes command.
/// </summary>
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<string, string> UserToEmailCache = new Dictionary<string, string>();
public string UserToEmail(string User)
{
if (UserToEmailCache.ContainsKey(User))
{
return UserToEmailCache[User];
}
string Result = "";
try
{
var P4Result = P4(String.Format("user -o {0}", User), AllowSpew: false);
if (P4Result.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<string, List<ChangeRecord>> ChangesCache = new Dictionary<string, List<ChangeRecord>>();
public bool Changes(out List<ChangeRecord> ChangeRecords, string CommandLine, bool AllowSpew = true, bool UseCaching = false, bool LongComment = false, bool WithClient = false)
{
// If the user specified '-l' or '-L', the summary will appear on subsequent lines (no quotes) instead of the same line (surrounded by single quotes)
bool ContainsDashL = CommandLine.StartsWith("-L ", StringComparison.InvariantCultureIgnoreCase) ||
CommandLine.IndexOf(" -L ", StringComparison.InvariantCultureIgnoreCase) > 0;
bool bSummaryIsOnSameLine = !ContainsDashL;
if (bSummaryIsOnSameLine && LongComment)
{
CommandLine = "-L " + CommandLine;
bSummaryIsOnSameLine = false;
}
if (UseCaching && ChangesCache.ContainsKey(CommandLine))
{
ChangeRecords = ChangesCache[CommandLine];
return true;
}
ChangeRecords = new List<ChangeRecord>();
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<DescribeFile> Files = new List<DescribeFile>();
public static int Compare(DescribeRecord A, DescribeRecord B)
{
return (A.CL < B.CL) ? -1 : (A.CL > B.CL) ? 1 : 0;
}
public override string ToString()
{
return String.Format("CL {0}: {1}", CL, Summary);
}
}
/// <summary>
/// Wraps P4 describe
/// </summary>
/// <param name="Changelist">Changelist numbers to query full descriptions for</param>
/// <param name="DescribeRecord">Describe record for the given changelist.</param>
/// <param name="AllowSpew"></param>
/// <param name="bShelvedFiles"></param>
/// <returns>True if everything went okay</returns>
public bool DescribeChangelist(int Changelist, out DescribeRecord DescribeRecord, bool AllowSpew = true, bool bShelvedFiles = false)
{
List<DescribeRecord> DescribeRecords;
if(!DescribeChangelists(new List<int>{ Changelist }, out DescribeRecords, AllowSpew, bShelvedFiles))
{
DescribeRecord = null;
return false;
}
else if(DescribeRecords.Count != 1)
{
DescribeRecord = null;
return false;
}
else
{
DescribeRecord = DescribeRecords[0];
return true;
}
}
/// <summary>
/// Wraps P4 describe
/// </summary>
/// <param name="Changelists">List of changelist numbers to query full descriptions for</param>
/// <param name="DescribeRecords">List of records we found. One for each changelist number. These will be sorted from oldest to newest.</param>
/// <param name="AllowSpew"></param>
/// <param name="bShelvedFiles">Whether to display shelved files</param>
/// <returns>True if everything went okay</returns>
public bool DescribeChangelists(List<int> Changelists, out List<DescribeRecord> DescribeRecords, bool AllowSpew = true, bool bShelvedFiles = false)
{
DescribeRecords = new List<DescribeRecord>();
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;
}
/// <summary>
/// Invokes p4 sync command.
/// </summary>
/// <param name="CommandLine">CommandLine to pass on to the command.</param>
/// <param name="AllowSpew"></param>
/// <param name="SpewIsVerbose"></param>
/// <param name="Retries"></param>
/// <param name="MaxWait"></param>
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);
}
/// <summary>
/// Invokes p4 preview sync command and gets a list of preview synced files.
/// </summary>
/// <param name="FilesPreviewSynced">Files that have been preview synced with the command</param>
/// <param name="CommandLine">CommandLine to pass on to the command.</param>
/// <param name="AllowSpew"></param>
/// <param name="SpewIsVerbose"></param>
/// <returns>Whether preview sync is successful</returns>
public bool PreviewSync(out List<string> FilesPreviewSynced, string CommandLine, bool AllowSpew = true, bool SpewIsVerbose = false)
{
FilesPreviewSynced = new List<string>();
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;
}
/// <summary>
/// Invokes p4 unshelve command.
/// </summary>
/// <param name="FromCL">Changelist to unshelve.</param>
/// <param name="ToCL">Changelist where the checked out files should be added.</param>
/// <param name="CommandLine">Commandline for the command.</param>
/// <param name="SpewIsVerbose"></param>
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);
}
/// <summary>
/// Invokes p4 shelve command.
/// </summary>
/// <param name="FromCL">Changelist to unshelve.</param>
/// <param name="CommandLine">Commandline for the command.</param>
/// <param name="AllowSpew"></param>
public void Shelve(int FromCL, string CommandLine = "", bool AllowSpew = true)
{
LogP4("", "shelve " + String.Format("-r -c {0} ", FromCL) + CommandLine, AllowSpew: AllowSpew);
}
/// <summary>
/// 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
/// </summary>
/// <param name="FromCL">Changelist to unshelve.</param>
/// <param name="CommandLine">Commandline for the command.</param>
/// <param name="AllowSpew"></param>
public void ShelveNoRevert(int FromCL, string CommandLine = "", bool AllowSpew = true)
{
LogP4("", "shelve " + String.Format("-f -c {0} ", FromCL) + CommandLine, AllowSpew: AllowSpew);
}
/// <summary>
/// Deletes shelved files from a changelist
/// </summary>
/// <param name="FromCL">Changelist to unshelve.</param>
/// <param name="AllowSpew"></param>
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);
}
}
/// <summary>
/// Invoke a command on a list of files, breaking up into multiple commands so as not to blow the max commandline length
/// </summary>
/// <param name="Command">The command, like "p4 edit -c 1000"</param>
/// <param name="Files"></param>
/// <param name="AllowSpew"></param>
public void BatchedCommand(string Command, List<string> 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);
}
}
/// <summary>
/// Invokes p4 reopen command.
/// </summary>
/// <param name="CL">Changelist where the checked out files should be moved to.</param>
/// <param name="CommandLine">Commandline for the command</param>
/// <param name="AllowSpew"></param>
public void Reopen(int CL, string CommandLine, bool AllowSpew = true)
{
LogP4("", $"reopen -c {CL} {CommandLine}", AllowSpew: AllowSpew);
}
/// <summary>
/// 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
/// </summary>
/// <param name="CL">Changelist where the checked out files should be moved to.</param>
/// <param name="Files">List of files to be moved</param>
/// <param name="AllowSpew"></param>
public void Reopen(int CL, List<string> Files, bool AllowSpew = true)
{
BatchedCommand($"reopen -c {CL}", Files, AllowSpew: AllowSpew);
}
/// <summary>
/// Invokes p4 edit command.
/// </summary>
/// <param name="CL">Changelist where the checked out files should be added.</param>
/// <param name="CommandLine">Commandline for the command.</param>
/// <param name="AllowSpew"></param>
public void Edit(int CL, string CommandLine, bool AllowSpew = true)
{
LogP4("", "edit " + String.Format("-c {0} ", CL) + CommandLine, AllowSpew: AllowSpew);
}
/// <summary>
/// Invokes p4 edit command with a list of files.
/// </summary>
/// <param name="CL">Changelist where the checked out files should be added.</param>
/// <param name="Files"></param>
/// <param name="AllowSpew"></param>
public void Edit(int CL, List<string> Files, bool AllowSpew = true)
{
BatchedCommand($"edit -c {CL}", Files, AllowSpew: AllowSpew);
}
/// <summary>
/// Invokes p4 edit command, no exceptions
/// </summary>
/// <param name="CL">Changelist where the checked out files should be added.</param>
/// <param name="CommandLine">Commandline for the command.</param>
public bool Edit_NoExceptions(int CL, string CommandLine)
{
try
{
string Output;
if (!LogP4Output(out Output, "", "edit " + String.Format("-c {0} ", CL) + CommandLine, null, true))
{
return false;
}
if (Output.IndexOf("- opened for edit") < 0)
{
return false;
}
return true;
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// Invokes p4 add command.
/// </summary>
/// <param name="CL">Changelist where the files should be added to.</param>
/// <param name="CommandLine">Commandline for the command.</param>
public void Add(int CL, string CommandLine)
{
LogP4("", "add " + String.Format("-c {0} ", CL) + CommandLine);
}
/// <summary>
/// Invokes p4 add command with a list of files.
/// </summary>
/// <param name="CL">Changelist where the checked out files should be added.</param>
/// <param name="Files">The list of files to add.</param>
/// <param name="AllowSpew"></param>
public void Add(int CL, List<string> Files, bool AllowSpew = true)
{
BatchedCommand($"add -c {CL}", Files, AllowSpew: AllowSpew);
}
/// <summary>
/// Invokes p4 delete command.
/// </summary>
/// <param name="CL">Changelist where the files should be added to.</param>
/// <param name="CommandLine">Commandline for the command.</param>
public void Delete(int CL, string CommandLine)
{
LogP4("", "delete " + String.Format("-c {0} ", CL) + CommandLine);
}
/// <summary>
/// Invokes p4 delete command with a list of files.
/// </summary>
/// <param name="CL">Changelist where the checked out files should be added.</param>
/// <param name="Files">List of files to be deleted.</param>
/// <param name="AllowSpew"></param>
public void Delete(int CL, List<string> Files, bool AllowSpew = true)
{
BatchedCommand($"delete -c {CL}", Files, AllowSpew: AllowSpew);
}
/// <summary>
/// Invokes p4 reconcile command.
/// </summary>
/// <param name="CL">Changelist to check the files out.</param>
/// <param name="CommandLine">Commandline for the command.</param>
/// <param name="AllowSpew"></param>
public void Reconcile(int CL, string CommandLine, bool AllowSpew = true)
{
LogP4("", "reconcile " + String.Format("-c {0} -ead -f ", CL) + CommandLine, AllowSpew: AllowSpew);
}
/// <summary>
/// Invokes p4 reconcile command.
/// </summary>
/// <param name="CommandLine">Commandline for the command.</param>
public void ReconcilePreview(string CommandLine)
{
LogP4("", "reconcile " + String.Format("-ead -n ") + CommandLine);
}
/// <summary>
/// Invokes p4 reconcile command.
/// Ignores files that were removed.
/// </summary>
/// <param name="CL">Changelist to check the files out.</param>
/// <param name="CommandLine">Commandline for the command.</param>
/// <param name="AllowSpew"></param>
public void ReconcileNoDeletes(int CL, string CommandLine, bool AllowSpew = true)
{
LogP4("", "reconcile " + String.Format("-c {0} -ea ", CL) + CommandLine, AllowSpew: AllowSpew);
}
/// <summary>
/// Invokes p4 resolve command.
/// Resolves all files by accepting yours and ignoring theirs.
/// </summary>
/// <param name="CL">Changelist to resolve.</param>
/// <param name="CommandLine">Commandline for the command.</param>
public void Resolve(int CL, string CommandLine)
{
LogP4("", "resolve -ay " + String.Format("-c {0} ", CL) + CommandLine);
}
/// <summary>
/// Invokes revert command.
/// </summary>
/// <param name="CommandLine">Commandline for the command.</param>
/// <param name="AllowSpew"></param>
public void Revert(string CommandLine, bool AllowSpew = true)
{
LogP4("", "revert " + CommandLine, AllowSpew: AllowSpew);
}
/// <summary>
/// Invokes revert command.
/// </summary>
/// <param name="CL">Changelist to revert</param>
/// <param name="CommandLine">Commandline for the command.</param>
/// <param name="AllowSpew"></param>
public void Revert(int CL, string CommandLine = "", bool AllowSpew = true)
{
LogP4("", "revert " + String.Format("-c {0} ", CL) + CommandLine, AllowSpew: AllowSpew);
}
/// <summary>
/// Invokes p4 revert command with a list of files.
/// </summary>
/// <param name="CL">Changelist where the checked out files should be reverted.</param>
/// <param name="Files">List of files to be reverted.</param>
/// <param name="AllowSpew"></param>
public void Revert(int CL, List<string> Files, bool AllowSpew = true)
{
BatchedCommand($"revert -c {CL}", Files, AllowSpew: AllowSpew);
}
/// <summary>
/// Reverts all unchanged file from the specified changelist.
/// </summary>
/// <param name="CL">Changelist to revert the unmodified files from.</param>
public void RevertUnchanged(int CL)
{
// caution this is a really bad idea if you hope to force submit!!!
LogP4("", "revert -a " + String.Format("-c {0} ", CL));
}
/// <summary>
/// Reverts all files from the specified changelist.
/// </summary>
/// <param name="CL">Changelist to revert.</param>
/// <param name="SpewIsVerbose"></param>
public void RevertAll(int CL, bool SpewIsVerbose = false)
{
LogP4("", "revert " + String.Format("-c {0} //...", CL), SpewIsVerbose: SpewIsVerbose);
}
/// <summary>
/// Submits the specified changelist.
/// </summary>
/// <param name="CL">Changelist to submit.</param>
/// <param name="Force">If true, the submit will be forced even if resolve is needed.</param>
/// <param name="RevertIfFail">If true, if the submit fails, revert the CL.</param>
public void Submit(int CL, bool Force = false, bool RevertIfFail = false)
{
int SubmittedCL;
Submit(CL, out SubmittedCL, Force, RevertIfFail);
}
/// <summary>
/// Submits the specified changelist.
/// </summary>
/// <param name="CL">Changelist to submit.</param>
/// <param name="SubmittedCL">Will be set to the submitted changelist number.</param>
/// <param name="Force">If true, the submit will be forced even if resolve is needed.</param>
/// <param name="RevertIfFail">If true, if the submit fails, revert the CL.</param>
public void Submit(int CL, out int SubmittedCL, bool Force = false, bool RevertIfFail = false)
{
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<string> AlreadyDone = new HashSet<string>();
while (Work.Length > 0)
{
string SlashSlashStr = "//";
int SlashSlash = Work.IndexOf(SlashSlashStr);
if (SlashSlash < 0)
{
break;
}
Work = Work.Substring(SlashSlash);
int MinMatch = Work.Length + 1;
foreach (var ProblemString in KnownProblems)
{
int ThisIndex = Work.IndexOf(ProblemString);
if (ThisIndex >= 0 && ThisIndex < MinMatch)
{
MinMatch = ThisIndex;
}
}
if (MinMatch > Work.Length)
{
break;
}
string File = Work.Substring(0, MinMatch).Trim();
if (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 (?<number>\d+) and submitted");
Match SubmitMatch = SubmitRegex.Match(CmdOutput);
if (!SubmitMatch.Success)
{
SubmitRegex = new Regex(@"Change (?<number>\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);
}
/// <summary>
/// Creates a new changelist with the specified owner and description.
/// </summary>
/// <param name="Owner">Owner of the changelist.</param>
/// <param name="Description">Description of the changelist.</param>
/// <param name="User"></param>
/// <param name="Type"></param>
/// <param name="AllowSpew"></param>
/// <returns>Id of the created changelist.</returns>
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;
}
/// <summary>
/// Updates a changelist with the given fields
/// </summary>
/// <param name="CL"></param>
/// <param name="NewClient"></param>
/// <param name="NewDescription"></param>
/// <param name="SpewIsVerbose"></param>
public void UpdateChange(int CL, string NewClient, string NewDescription, bool SpewIsVerbose = false)
{
UpdateChange(CL, null, NewClient, NewDescription, SpewIsVerbose);
}
/// <summary>
/// Updates a changelist with the given fields
/// </summary>
/// <param name="CL"></param>
/// <param name="NewUser"></param>
/// <param name="NewClient"></param>
/// <param name="NewDescription"></param>
/// <param name="SpewIsVerbose"></param>
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);
}
}
/// <summary>
/// Deletes the specified changelist.
/// </summary>
/// <param name="CL">Changelist to delete.</param>
/// <param name="RevertFiles">Indicates whether files in that changelist should be reverted.</param>
/// <param name="SpewIsVerbose"></param>
/// <param name="AllowSpew"></param>
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);
}
/// <summary>
/// Tries to delete the specified empty changelist.
/// </summary>
/// <param name="CL">Changelist to delete.</param>
/// <returns>True if the changelist was deleted, false otherwise.</returns>
public bool TryDeleteEmptyChange(int CL)
{
string CmdOutput;
if (LogP4Output(out CmdOutput, "", String.Format("change -d {0}", CL)))
{
string EndStr = " deleted.";
string ChangeStr = "Change ";
int Offset = CmdOutput.LastIndexOf(ChangeStr);
int EndOffset = CmdOutput.LastIndexOf(EndStr);
if (Offset == 0 && Offset < EndOffset && !CmdOutput.Contains("can't be deleted."))
{
return true;
}
}
return false;
}
/// <summary>
/// Returns the changelist specification.
/// </summary>
/// <param name="CL">Changelist to get the specification from.</param>
/// <param name="AllowSpew"></param>
/// <returns>Specification of the changelist.</returns>
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);
}
/// <summary>
/// Checks whether the specified changelist exists.
/// </summary>
/// <param name="CL">Changelist id.</param>
/// <param name="Pending">Whether it is a pending changelist.</param>
/// <param name="AllowSpew"></param>
/// <returns>Returns whether the changelist exists.</returns>
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;
}
/// <summary>
/// Returns a list of files contained in the specified changelist.
/// </summary>
/// <param name="CL">Changelist to get the files from.</param>
/// <param name="Pending">Whether the changelist is a pending one.</param>
/// <param name="AllowSpew"></param>
/// <returns>List of the files contained in the changelist.</returns>
public List<string> ChangeFiles(int CL, out bool Pending, bool AllowSpew = true)
{
var Result = new List<string>();
if (ChangeExists(CL, out Pending, AllowSpew))
{
string CmdOutput = ChangeOutput(CL, AllowSpew);
if (CmdOutput.Length > 0)
{
string FilesStr = "Files:";
int FilesOffset = CmdOutput.LastIndexOf(FilesStr);
if (FilesOffset < 0)
{
throw new P4Exception("Change {0} returned bad output\n{1}", CL, CmdOutput);
}
else
{
CmdOutput = CmdOutput.Substring(FilesOffset + FilesStr.Length);
while (CmdOutput.Length > 0)
{
string SlashSlashStr = "//";
int SlashSlash = CmdOutput.IndexOf(SlashSlashStr);
if (SlashSlash < 0)
{
break;
}
CmdOutput = CmdOutput.Substring(SlashSlash);
string HashStr = "#";
int Hash = CmdOutput.IndexOf(HashStr);
if (Hash < 0)
{
break;
}
string File = CmdOutput.Substring(0, Hash).Trim();
CmdOutput = CmdOutput.Substring(Hash);
Result.Add(File);
}
}
}
}
else
{
throw new P4Exception("Change {0} did not exist.", CL);
}
return Result;
}
/// <summary>
/// Returns the output from p4 opened
/// </summary>
/// <returns>Specification of the changelist.</returns>
public string OpenedOutput()
{
string CmdOutput;
if (LogP4Output(out CmdOutput, "", "opened"))
{
return CmdOutput;
}
throw new P4Exception("OpenedOutput failed, output follows\n{0}", CmdOutput);
}
/// <summary>
/// Deletes the specified label.
/// </summary>
/// <param name="LabelName">Label to delete.</param>
/// <param name="AllowSpew"></param>
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);
}
}
/// <summary>
/// Creates a new label.
/// </summary>
/// <param name="Name">Name of the label.</param>
/// <param name="Options">Options for the label. Valid options are "locked", "unlocked", "autoreload" and "noautoreload".</param>
/// <param name="View">View mapping for the label.</param>
/// <param name="Owner">Owner of the label.</param>
/// <param name="Description">Description of the label.</param>
/// <param name="Date">Date of the label creation.</param>
/// <param name="Time">Time of the label creation</param>
public void CreateLabel(string Name, string Options, string View, string Owner = null, string Description = null, string Date = null, string Time = null)
{
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);
}
/// <summary>
/// Invokes p4 tag command.
/// Associates a named label with a file revision.
/// </summary>
/// <param name="LabelName">Name of the label.</param>
/// <param name="FilePath">Path to the file.</param>
/// <param name="AllowSpew">Whether the command is allowed to spew.</param>
public void Tag(string LabelName, string FilePath, bool AllowSpew = true)
{
LogP4("", "tag -l " + LabelName + " " + FilePath, null, AllowSpew);
}
/// <summary>
/// Syncs a label to the current content of the client.
/// </summary>
/// <param name="LabelName">Name of the label.</param>
/// <param name="AllowSpew">Whether the command is allowed to spew.</param>
/// <param name="FileToLabel"></param>
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);
}
}
/// <summary>
/// Syncs a label from another label.
/// </summary>
/// <param name="FromLabelName">Source label name.</param>
/// <param name="ToLabelName">Target label name.</param>
/// <param name="AllowSpew">Whether the command is allowed to spew.</param>
public void LabelToLabelSync(string FromLabelName, string ToLabelName, bool AllowSpew = true)
{
string Quiet = "";
if (!AllowSpew)
{
Quiet = "-q ";
}
LogP4("", "labelsync -a " + Quiet + "-l " + ToLabelName + " //...@" + FromLabelName);
}
/// <summary>
/// Checks whether the specified label exists and has any files.
/// </summary>
/// <param name="Name">Name of the label.</param>
/// <returns>Whether there is an label with files.</returns>
public bool LabelExistsAndHasFiles(string Name)
{
string Output;
return LogP4Output(out Output, "", "files -m 1 //...@" + Name);
}
/// <summary>
/// Returns the label description.
/// </summary>
/// <param name="Name">Name of the label.</param>
/// <param name="Description">Description of the label.</param>
/// <param name="AllowSpew">Whether to allow log spew</param>
/// <returns>Returns whether the label description could be retrieved.</returns>
public bool LabelDescription(string Name, out string Description, bool AllowSpew = true)
{
string Output;
Description = "";
if (LogP4Output(out Output, "", "label -o " + Name, AllowSpew: AllowSpew))
{
string Desc = "Description:";
int Start = Output.LastIndexOf(Desc);
if (Start > 0)
{
Start += Desc.Length;
}
int End = Output.LastIndexOf("Options:");
if (Start > 0 && End > 0 && End > Start)
{
Description = Output.Substring(Start, End - Start).Replace("\n\t", "\n");
Description = Description.Trim();
return true;
}
}
return false;
}
/// <summary>
/// Reads a label spec
/// </summary>
/// <param name="Name">Label name</param>
/// <param name="AllowSpew">Whether to allow log spew</param>
public P4Spec ReadLabelSpec(string Name, bool AllowSpew = true)
{
string LabelSpec;
if(!LogP4Output(out LabelSpec, "", "label -o " + Name, AllowSpew: AllowSpew))
{
throw new P4Exception("Couldn't describe existing label '{0}', output was:\n", Name, LabelSpec);
}
return P4Spec.FromString(LabelSpec);
}
/// <summary>
/// Updates a label with a new spec
/// </summary>
/// <param name="Spec">Label specification</param>
/// <param name="AllowSpew">Whether to allow log spew</param>
public void UpdateLabelSpec(P4Spec Spec, bool AllowSpew = true)
{
LogP4("", "label -i", Input: Spec.ToString(), AllowSpew: AllowSpew);
}
/// <summary>
/// Updates a label description.
/// </summary>
/// <param name="Name">Name of the label</param>
/// <param name="NewDescription">Description of the label.</param>
/// <param name="AllowSpew">Whether to allow log spew</param>
public void UpdateLabelDescription(string Name, string NewDescription, bool AllowSpew = true)
{
string LabelSpec;
if(!LogP4Output(out LabelSpec, "", "label -o " + Name, AllowSpew: AllowSpew))
{
throw new P4Exception("Couldn't describe existing label '{0}', output was:\n", Name, LabelSpec);
}
List<string> Lines = new List<string>(LabelSpec.Split('\n').Select(x => x.TrimEnd()));
// Find the description text, and remove it
int Idx = 0;
for(; Idx < Lines.Count; Idx++)
{
if(Lines[Idx].StartsWith("Description:"))
{
int EndIdx = Idx + 1;
while(EndIdx < Lines.Count && (Lines[EndIdx].Length == 0 || Char.IsWhiteSpace(Lines[EndIdx][0]) || Lines[EndIdx].IndexOf(':') == -1))
{
EndIdx++;
}
Lines.RemoveRange(Idx, EndIdx - Idx);
break;
}
}
// Insert the new description text
Lines.Insert(Idx, "Description: " + NewDescription.Replace("\n", "\n\t"));
LabelSpec = String.Join("\n", Lines);
// Update the label
LogP4("", "label -i", Input: LabelSpec, AllowSpew: AllowSpew);
}
/* Pattern to parse P4 changes command output. */
private static readonly Regex ChangesListOutputPattern = new Regex(@"^Change\s+(?<number>\d+)\s+.+$", RegexOptions.Compiled | RegexOptions.Multiline);
/// <summary>
/// Gets the latest CL number submitted to the depot. It equals to the @head.
/// </summary>
/// <returns>The head CL number.</returns>
public int GetLatestCLNumber()
{
string Output;
if (!LogP4Output(out Output, "", "changes -s submitted -m1") || string.IsNullOrWhiteSpace(Output))
{
throw new InvalidOperationException("The depot should have at least one submitted changelist. Brand new depot?");
}
var Match = ChangesListOutputPattern.Match(Output);
if (!Match.Success)
{
throw new InvalidOperationException("The Perforce output is not in the expected format provided by 2014.1 documentation.");
}
return Int32.Parse(Match.Groups["number"].Value);
}
/* Pattern to parse P4 labels command output. */
static readonly Regex LabelsListOutputPattern = new Regex(@"^Label\s+(?<name>[\w\/\.-]+)\s+(?<date>\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2})\s+'(?<description>.+)'\s*$", RegexOptions.Compiled | RegexOptions.Multiline);
/// <summary>
/// Gets all labels satisfying given filter.
/// </summary>
/// <param name="Filter">Filter for label names.</param>
/// <param name="bCaseSensitive">Treat filter as case-sensitive.</param>
/// <returns></returns>
public P4Label[] GetLabels(string Filter, bool bCaseSensitive = true)
{
var LabelList = new List<P4Label>();
string Output;
if (P4Output(out Output, "", "labels -t " + (bCaseSensitive ? "-e" : "-E") + Filter, null, false))
{
foreach (Match LabelMatch in LabelsListOutputPattern.Matches(Output))
{
LabelList.Add(new P4Label(LabelMatch.Groups["name"].Value,
DateTime.ParseExact(
LabelMatch.Groups["date"].Value, "yyyy/MM/dd HH:mm:ss",
System.Globalization.CultureInfo.InvariantCulture)
));
}
}
return LabelList.ToArray();
}
/// <summary>
/// Validate label for some content.
/// </summary>
/// <returns>True if label exists and has at least one file tagged. False otherwise.</returns>
public bool ValidateLabelContent(string LabelName)
{
string Output;
if (P4Output(out Output, "", "files -m 1 @" + LabelName, null, false))
{
if (Output.StartsWith("//depot"))
{
// If it starts with depot path then label has at least one file tagged in it.
return true;
}
}
else
{
throw new InvalidOperationException("For some reason P4 files failed.");
}
return false;
}
/// <summary>
/// Given a file path in the depot, returns the local disk mapping for the current view
/// </summary>
/// <param name="DepotFile">The full file path in depot naming form</param>
/// <param name="AllowSpew"></param>
/// <returns>The file's first reported path on disk or null if no mapping was found</returns>
public string DepotToLocalPath(string DepotFile, bool AllowSpew = true)
{
// 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;
}
/// <summary>
/// Given a set of file paths in the depot, returns the local disk mapping for the current view
/// </summary>
/// <param name="DepotFiles">The full file paths in depot naming form</param>
/// <param name="AllowSpew"></param>
/// <returns>The file's first reported path on disk or null if no mapping was found</returns>
public string[] DepotToLocalPaths(string[] DepotFiles, bool AllowSpew = true)
{
const int BatchSize = 20;
// Parse the output from P4
List<string> Lines = new List<string>();
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;
}
/// <summary>
/// Determines the mappings for a depot file in the workspace, without that file having to exist.
/// NOTE: This function originally allowed multiple depot paths at once. The "file(s) not in client view" messages are written to stderr
/// rather than stdout, and buffering them separately garbles the output when they're merged together.
/// </summary>
/// <param name="DepotFile">Depot path</param>
/// <param name="AllowSpew">Allows logging</param>
/// <returns>List of records describing the file's mapping. Usually just one, but may be more.</returns>
public P4WhereRecord[] Where(string DepotFile, bool AllowSpew = true)
{
// 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<P4WhereRecord> Records = new List<P4WhereRecord>();
for (int LineIdx = 0; LineIdx < Lines.Length; )
{
P4WhereRecord Record = new P4WhereRecord();
// Parse an optional "... unmap"
if (Lines[LineIdx].Trim() == "... unmap")
{
Record.bUnmap = true;
LineIdx++;
}
// Parse "... depotFile <depot path>"
const string DepotFilePrefix = "... depotFile ";
if (LineIdx >= Lines.Length || !Lines[LineIdx].StartsWith(DepotFilePrefix))
{
throw new AutomationException("Unexpected output from p4 where: {0}", String.Join("\n", Lines.Skip(LineIdx)));
}
Record.DepotFile = Lines[LineIdx++].Substring(DepotFilePrefix.Length).Trim();
// Parse "... clientFile <client path>"
const string ClientFilePrefix = "... clientFile ";
if (LineIdx >= Lines.Length || !Lines[LineIdx].StartsWith(ClientFilePrefix))
{
throw new AutomationException("Unexpected output from p4 where: {0}", String.Join("\n", Lines.Skip(LineIdx)));
}
Record.ClientFile = Lines[LineIdx++].Substring(ClientFilePrefix.Length).Trim();
// Parse "... path <path to file>"
const string PathPrefix = "... path ";
if (LineIdx >= Lines.Length || !Lines[LineIdx].StartsWith(PathPrefix))
{
throw new AutomationException("Unexpected output from p4 where: {0}", String.Join("\n", Lines.Skip(LineIdx)));
}
Record.Path = Lines[LineIdx++].Substring(PathPrefix.Length).Trim();
// Add it to the output list
Records.Add(Record);
}
return Records.ToArray();
}
/// <summary>
/// Determines whether a file exists in the depot.
/// </summary>
/// <param name="DepotFile">Depot path</param>
/// <param name="AllowSpew"></param>
/// <returns>List of records describing the file's mapping. Usually just one, but may be more.</returns>
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;
}
/// <summary>
/// Gets file stats.
/// </summary>
/// <param name="Filename">Filenam</param>
/// <returns>File stats (invalid if the file does not exist in P4)</returns>
public P4FileStat FStat(string Filename)
{
string Output;
string Command = "fstat " + CommandUtils.MakePathSafeToUseWithCommandLine(Filename);
if (!LogP4Output(out Output, "", Command))
{
throw new P4Exception("p4.exe {0} failed.", Command);
}
P4FileStat Stat = P4FileStat.Invalid;
if (Output.Contains("no such file(s)") == false)
{
Output = Output.Replace("\r", "");
var FormLines = Output.Split('\n');
foreach (var Line in FormLines)
{
var StatAttribute = Line.StartsWith("... ") ? Line.Substring(4) : Line;
var StatPair = StatAttribute.Split(' ');
if (StatPair.Length == 2 && !String.IsNullOrEmpty(StatPair[1]))
{
switch (StatPair[0])
{
case "type":
// Use type (current CL if open) if possible
ParseFileType(StatPair[1], ref Stat);
break;
case "headType":
if (Stat.Type == P4FileType.Unknown)
{
ParseFileType(StatPair[1], ref Stat);
}
break;
case "action":
Stat.Action = ParseAction(StatPair[1]);
break;
case "change":
Stat.Change = StatPair[1];
break;
}
}
}
if (Stat.IsValid == false)
{
throw new AutomationException("Unable to parse fstat result for {0} (unknown file type).", Filename);
}
}
return Stat;
}
/// <summary>
/// Set file attributes (additively)
/// </summary>
/// <param name="Filename">File to change the attributes of.</param>
/// <param name="Attributes">Attributes to set.</param>
/// <param name="Changelist"></param>
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);
}
}
/// <summary>
/// Parses P4 forms and stores them as a key/value pairs.
/// </summary>
/// <param name="Output">P4 command output (must be a form).</param>
/// <returns>Parsed output.</returns>
public Dictionary<string, string> ParseTaggedP4Output(string Output)
{
var Tags = new Dictionary<string, string>(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;
}
/// <summary>
/// Formats a tagged record as a string
/// </summary>
/// <param name="Record">The record to format</param>
/// <returns>Single string containing the record</returns>
public static string FormatTaggedOutput(Dictionary<string, string> Record)
{
StringBuilder Result = new StringBuilder();
foreach (KeyValuePair<string, string> Pair in Record)
{
if (Result.Length > 0)
{
Result.Append('\n');
}
Result.AppendFormat("{0}: {1}", Pair.Key, Pair.Value);
}
return Result.ToString();
}
/// <summary>
/// Checks if the client exists in P4.
/// </summary>
/// <param name="ClientName">Client name</param>
/// <param name="Quiet"></param>
/// <returns>True if the client exists.</returns>
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;
}
/// <summary>
/// Gets client info.
/// </summary>
/// <param name="ClientName">Name of the client.</param>
/// <param name="Quiet"></param>
/// <returns></returns>
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);
}
/// <summary>
/// Parses a string with enum values separated with spaces.
/// </summary>
/// <param name="ValueText"></param>
/// <param name="EnumType"></param>
/// <returns></returns>
private static object ParseEnumValues(string ValueText, Type EnumType)
{
ValueText = new Regex("[+ ]").Replace(ValueText, ",");
return Enum.Parse(EnumType, ValueText, true);
}
/// <summary>
/// Gets client info (does not check if the client exists)
/// </summary>
/// <param name="ClientName">Name of the client.</param>
/// <returns></returns>
public P4ClientInfo GetClientInfoInternal(string ClientName)
{
P4ClientInfo Info = new P4ClientInfo();
var P4Result = P4(String.Format("client -o {0}", ClientName), AllowSpew: false, WithClient: false);
if (P4Result.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<string, string>(
Mapping.Substring(0, ClientStartIndex - 1),
Mapping.Substring(ClientStartIndex + ClientMappingRoot.Length));
Info.View.Add(ViewPair);
}
}
}
}
else
{
throw new AutomationException("p4 client -o {0} failed!", ClientName);
}
return Info;
}
/// <summary>
/// Gets all clients owned by the user.
/// </summary>
/// <param name="UserName"></param>
/// <param name="PathUnderClientRoot"></param>
/// <param name="AllowedStream"></param>
/// <returns>List of clients owned by the user.</returns>
public P4ClientInfo[] GetClientsForUser(string UserName, string PathUnderClientRoot = null, string AllowedStream = null)
{
var ClientList = new List<P4ClientInfo>();
// 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 (?<client>.+) Root (?<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;
}
/// <summary>
/// Deletes a client.
/// </summary>
/// <param name="Name">Client name.</param>
/// <param name="Force">Forces the operation (-f)</param>
/// <param name="AllowSpew"></param>
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);
}
/// <summary>
/// Creates a new client.
/// </summary>
/// <param name="ClientSpec">Client specification.</param>
/// <param name="AllowSpew"></param>
/// <returns></returns>
public P4ClientInfo CreateClient(P4ClientInfo ClientSpec, bool AllowSpew = true)
{
string SpecInput = "Client: " + ClientSpec.Name + Environment.NewLine;
SpecInput += "Owner: " + ClientSpec.Owner + Environment.NewLine;
SpecInput += "Host: " + ClientSpec.Host + Environment.NewLine;
SpecInput += "Root: " + ClientSpec.RootPath + Environment.NewLine;
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;
}
/// <summary>
/// Lists immediate sub-directories of the specified directory.
/// </summary>
/// <param name="CommandLine"></param>
/// <returns>List of sub-directories of the specified directories.</returns>
public List<string> 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<string>();
var Lines = P4Result.Output.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
foreach (string Line in Lines)
{
if (!Line.Contains("no such file"))
{
Result.Add(Line);
}
}
return Result;
}
/// <summary>
/// Takes P4 output from files or opened and returns a dictionary of paths/actions
/// </summary>
/// <param name="P4Output"></param>
/// <returns></returns>
protected Dictionary<string,string> MapFileOutputToActions(string P4Output)
{
Dictionary<string, string> Results = new Dictionary<string, string>();
string[] Lines = P4Output.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
// <path>#<have> - <action> change 16479539 (text)
Regex OutputSplitter = new Regex(@"(?<filename>.+)#(?<have>\d+|none) \- (?<action>[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;
}
/// <summary>
/// Run 'p4 files [cmdline]'and return a list of the files in the changelist (files being deleted are excluded)
/// </summary>
/// <param name="CommandLine"></param>
/// <returns>List of files in the specified directory.</returns>
public List<string> Files(string CommandLine)
{
List<string> DeleteActions = new List<string> { "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<string> Result = new List<string>();
Dictionary<string, string> FileActions = MapFileOutputToActions(P4Result.Output);
foreach (var KV in FileActions)
{
if (!DeleteActions.Contains(KV.Value))
{
Result.Add(KV.Key);
}
}
return Result;
}
/// <summary>
/// Run 'p4 opened [cmdline]'and return a list of the files in the changelist (files being deleted are excluded)
/// </summary>
/// <param name="CommandLine"></param>
/// <returns>List of files in the specified directory.</returns>
public List<string> Opened(string CommandLine)
{
List<string> DeleteActions = new List<string> { "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<string> Result = new List<string>();
Dictionary<string, string> FileActions = MapFileOutputToActions(P4Result.Output);
foreach (var KV in FileActions)
{
if (!DeleteActions.Contains(KV.Value))
{
Result.Add(KV.Key);
}
}
return Result;
}
/// <summary>
/// Gets the contents of a particular file in the depot without syncing it
/// </summary>
/// <param name="DepotPath">Depot path to the file (with revision/range if necessary)</param>
/// <param name="AllowSpew"></param>
/// <returns>Contents of the file</returns>
public string Print(string DepotPath, bool AllowSpew = true)
{
string Output;
if(!P4Output(out Output, "", "print -q " + DepotPath, AllowSpew: AllowSpew, WithClient: false))
{
throw new AutomationException("p4 print {0} failed", DepotPath);
}
if(!Output.Trim().Contains("\n") && Output.Contains("no such file(s)"))
{
throw new AutomationException("p4 print {0} failed", DepotPath);
}
return Output;
}
/// <summary>
/// Gets the contents of a particular file in the depot and writes it to a local file without syncing it
/// </summary>
/// <param name="DepotPath">Depot path to the file (with revision/range if necessary)</param>
/// <param name="FileName">Output file to write to</param>
/// <param name="AllowSpew">If true, any output from p4.exe will be logged.</param>
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);
}
}
/// <summary>
/// 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).
/// </summary>
/// <param name="StreamName">The name of the stream, eg. //UE4/Dev-Animation</param>
/// <param name="bReverse">If true, returns changes that have not been merged from the parent stream into this one.</param>
/// <returns>List of changelist numbers that are pending integration</returns>
public List<int> 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<int> Changelists = new List<int>();
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;
}
/// <summary>
/// Execute the 'filelog' command
/// </summary>
/// <param name="Options">Options for the command</param>
/// <param name="FileSpecs">List of file specifications to query</param>
/// <returns>List of file records</returns>
public P4FileRecord[] FileLog(P4FileLogOptions Options, params string[] FileSpecs)
{
return FileLog(-1, -1, Options, FileSpecs);
}
/// <summary>
/// Execute the 'filelog' command
/// </summary>
/// <param name="MaxChanges">Number of changelists to show. Ignored if zero or negative.</param>
/// <param name="Options">Options for the command</param>
/// <param name="FileSpecs">List of file specifications to query</param>
/// <returns>List of file records</returns>
public P4FileRecord[] FileLog(int MaxChanges, P4FileLogOptions Options, params string[] FileSpecs)
{
return FileLog(-1, MaxChanges, Options, FileSpecs);
}
/// <summary>
/// Execute the 'filelog' command
/// </summary>
/// <param name="ChangeNumber">Show only files modified by this changelist. Ignored if zero or negative.</param>
/// <param name="MaxChanges">Number of changelists to show. Ignored if zero or negative.</param>
/// <param name="Options">Options for the command</param>
/// <param name="FileSpecs">List of file specifications to query</param>
/// <returns>List of file records</returns>
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;
}
/// <summary>
/// Execute the 'filelog' command
/// </summary>
/// <param name="ChangeNumber">Show only files modified by this changelist. Ignored if zero or negative.</param>
/// <param name="MaxChanges">Number of changelists to show. Ignored if zero or negative.</param>
/// <param name="Options">Options for the command</param>
/// <param name="FileSpecs">List of file specifications to query</param>
/// <param name="OutRecords"></param>
/// <returns>List of file records</returns>
public P4ReturnCode TryFileLog(int ChangeNumber, int MaxChanges, P4FileLogOptions Options, string[] FileSpecs, out P4FileRecord[] OutRecords)
{
// Build the argument list
List<string> Arguments = new List<string>();
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<Dictionary<string, string>> RawRecords = P4TaggedOutput(CommandLine);
// Parse all the output
List<P4FileRecord> Records = new List<P4FileRecord>();
foreach(Dictionary<string, string> 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<P4RevisionRecord> Revisions = new List<P4RevisionRecord>();
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<P4IntegrationRecord> Integrations = new List<P4IntegrationRecord>();
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);
/// <summary>
/// Enumerates all streams in a depot
/// </summary>
/// <param name="StreamPath">The path for streams to enumerate (eg. "//UE5/...")</param>
/// <returns>List of streams matching the given criteria</returns>
public List<P4StreamRecord> Streams(string StreamPath)
{
return Streams(StreamPath, -1, null, false);
}
/// <summary>
/// Enumerates all streams in a depot
/// </summary>
/// <param name="StreamPath">The path for streams to enumerate (eg. "//UE5/...")</param>
/// <param name="MaxResults">Maximum number of results to return</param>
/// <param name="Filter">Additional filter to be applied to the results</param>
/// <param name="bUnloaded">Whether to enumerate unloaded workspaces</param>
/// <returns>List of streams matching the given criteria</returns>
public List<P4StreamRecord> 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<Dictionary<string, string>> RawRecords = P4TaggedOutput(CommandLine.ToString(), false);
// Parse the output
List<P4StreamRecord> Records = new List<P4StreamRecord>();
foreach(Dictionary<string, string> 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<bool> 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(@"(?<depot>.+)#(?<revision>\d+) - (?<client>.+)", RegexOptions.Compiled);
/// <summary>
/// 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.
/// </summary>
/// <param name="Pattern">Local or depot path e.g. .../Source/...</param>
/// <returns>Collection of information about synced files.</returns>
public List<P4HaveRecord> HaveFiles(string Pattern)
{
List<P4HaveRecord> 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;
}
/// <summary>
/// Parse a nullable boolean
/// </summary>
/// <param name="Text">Text to parse. May be "true", "false", or "n/a".</param>
/// <returns>The parsed boolean</returns>
static Nullable<bool> 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);
}
}
/// <summary>
/// Parse a list of stream option flags
/// </summary>
/// <param name="Text">Text to parse</param>
/// <returns>Flags for the stream options</returns>
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<string, T> GetEnumLookup<T>()
{
Dictionary<string, T> Lookup = new Dictionary<string, T>();
foreach(T Value in Enum.GetValues(typeof(T)))
{
foreach(MemberInfo Member in typeof(T).GetMember(Value.ToString()))
{
string Description = Member.GetCustomAttribute<DescriptionAttribute>().Description;
Lookup.Add(Description, Value);
}
}
return Lookup;
}
static Lazy<Dictionary<string, P4Action>> DescriptionToAction = new Lazy<Dictionary<string, P4Action>>(() => GetEnumLookup<P4Action>());
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<Dictionary<string, P4IntegrateAction>> DescriptionToIntegrationAction = new Lazy<Dictionary<string, P4IntegrateAction>>(() => GetEnumLookup<P4IntegrateAction>());
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);
}
/// <summary>
/// For a given file (and revision, potentially), returns where it was integrated from. Useful in conjunction with files in a P4DescribeRecord, with action = "integrate".
/// </summary>
/// <param name="DepotPath">The file to check. May have a revision specifier at the end (eg. //depot/UE4/foo.cpp#2) </param>
/// <returns>The file that it was integrated from, without a revision specifier</returns>
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<object, string>[] GetEnumValuesAndKeywords(Type EnumType)
{
var Values = Enum.GetValues(EnumType);
KeyValuePair<object, string>[] ValuesAndKeywords = new KeyValuePair<object, string>[Values.Length];
int ValueIndex = 0;
foreach (var Value in Values)
{
ValuesAndKeywords[ValueIndex++] = new KeyValuePair<object, string>(Value, GetEnumDescription(EnumType, Value));
}
return ValuesAndKeywords;
}
private static KeyValuePair<object, string>[] GetEnumValuesAndKeywords(Type EnumType, object[] Values)
{
KeyValuePair<object, string>[] ValuesAndKeywords = new KeyValuePair<object, string>[Values.Length];
int ValueIndex = 0;
foreach (var Value in Values)
{
ValuesAndKeywords[ValueIndex++] = new KeyValuePair<object, string>(Value, GetEnumDescription(EnumType, Value));
}
return ValuesAndKeywords;
}
private static string GetEnumDescription(Type EnumType, object Value)
{
var MemberInfo = EnumType.GetMember(Value.ToString());
var Atributes = MemberInfo[0].GetCustomAttributes(typeof(DescriptionAttribute), false);
return ((DescriptionAttribute)Atributes[0]).Description;
}
private static string FileAttributesToString(P4FileAttributes Attributes)
{
var AllAttributes = GetEnumValuesAndKeywords(typeof(P4FileAttributes));
string Text = "";
foreach (var Attr in AllAttributes)
{
var AttrValue = (P4FileAttributes)Attr.Key;
if ((Attributes & AttrValue) == AttrValue)
{
Text += Attr.Value;
}
}
if (String.IsNullOrEmpty(Text) == false)
{
Text = "+" + Text;
}
return Text;
}
public bool CheckClientHasPendingChanges(string P4Client = null)
{
if (P4Client == null)
{
P4Client = P4Env.Client;
}
if (String.IsNullOrEmpty(P4Client))
{
Logger.LogWarning("No Perforce client found.");
return false;
}
List<ChangeRecord> PendingChanges;
string P4GetPendingChangesArgs = $"-s pending -c {P4Client}";
Logger.LogInformation("Checking for pending changes...");
Changes(out PendingChanges, P4GetPendingChangesArgs);
return PendingChanges.Count > 0;
}
}
}