// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace Tools.DotNETCommon.Perforce
{
///
/// Stores settings for communicating with a Perforce server.
///
public class PerforceConnection
{
///
/// Constant for the default changelist, where valid.
///
public const int DefaultChange = -2;
#region Plumbing
///
/// Stores cached information about a field with a P4Tag attribute
///
class CachedTagInfo
{
///
/// Name of the tag. Specified in the attribute or inferred from the field name.
///
public string Name;
///
/// Whether this tag is optional or not.
///
public bool Optional;
///
/// The field containing the value of this data.
///
public FieldInfo Field;
///
/// Index into the bitmask of required types
///
public ulong RequiredTagBitMask;
}
///
/// Stores cached information about a record
///
class CachedRecordInfo
{
///
/// Type of the record
///
public Type Type;
///
/// Map of tag names to their cached reflection information
///
public Dictionary TagNameToInfo = new Dictionary();
///
/// Bitmask of all the required tags. Formed by bitwise-or'ing the RequiredTagBitMask fields for each required CachedTagInfo.
///
public ulong RequiredTagsBitMask;
///
/// The type of records to create for subelements
///
public Type SubElementType;
///
/// The cached record info for the subelement type
///
public CachedRecordInfo SubElementRecordInfo;
///
/// Field containing subelements
///
public FieldInfo SubElementField;
}
///
/// Unix epoch; used for converting times back into C# datetime objects
///
static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
///
/// Cached map of enum types to a lookup mapping from p4 strings to enum values.
///
static ConcurrentDictionary> EnumTypeToFlags = new ConcurrentDictionary>();
///
/// Cached set of record
///
static ConcurrentDictionary RecordTypeToInfo = new ConcurrentDictionary();
///
/// Default type for info
///
static CachedRecordInfo InfoRecordInfo = GetCachedRecordInfo(typeof(PerforceInfo));
///
/// Default type for errors
///
static CachedRecordInfo ErrorRecordInfo = GetCachedRecordInfo(typeof(PerforceError));
///
/// Global options for each command
///
public readonly string GlobalOptions;
///
/// Constructor
///
/// Global options to pass to every Perforce command
public PerforceConnection(string GlobalOptions)
{
this.GlobalOptions = GlobalOptions;
}
///
/// Constructor
///
/// The server address and port
/// The user name
/// The client name
public PerforceConnection(string ServerAndPort, string UserName, string ClientName)
{
List Options = new List();
if(ServerAndPort != null)
{
Options.Add(String.Format("-p {0}", ServerAndPort));
}
if (UserName != null)
{
Options.Add(String.Format("-u {0}", UserName));
}
if (ClientName != null)
{
Options.Add(String.Format("-c {0}", ClientName));
}
this.GlobalOptions = String.Join(" ", Options);
}
///
/// Execute a Perforce command and parse the output as marshalled Python objects. This is more robustly defined than the text-based tagged output
/// format, because it avoids ambiguity when returned fields can have newlines.
///
/// Command line to execute Perforce with
/// Input data to pass to the Perforce server. May be null.
/// Handler for each received record.
public void Command(string CommandLine, byte[] InputData, Action>> RecordHandler)
{
using(PerforceChildProcess Process = new PerforceChildProcess(InputData, "{0} {1}", GlobalOptions, CommandLine))
{
List> Record = new List>();
while(Process.TryReadRecord(Record))
{
RecordHandler(Record);
}
}
}
///
/// Serializes a list of key/value pairs into binary format.
///
/// List of key value pairs
/// Serialized record data
static byte[] SerializeRecord(List> KeyValuePairs)
{
MemoryStream Stream = new MemoryStream();
using (BinaryWriter Writer = new BinaryWriter(Stream))
{
Writer.Write((byte)'{');
foreach(KeyValuePair KeyValuePair in KeyValuePairs)
{
Writer.Write('s');
byte[] KeyBytes = Encoding.UTF8.GetBytes(KeyValuePair.Key);
Writer.Write((int)KeyBytes.Length);
Writer.Write(KeyBytes);
if (KeyValuePair.Value is string)
{
Writer.Write('s');
byte[] ValueBytes = Encoding.UTF8.GetBytes((string)KeyValuePair.Value);
Writer.Write((int)ValueBytes.Length);
Writer.Write(ValueBytes);
}
else
{
throw new PerforceException("Unsupported formatting type for {0}", KeyValuePair.Key);
}
}
Writer.Write((byte)'0');
}
return Stream.ToArray();
}
///
/// Execute a command and parse the response
///
/// Arguments for the command
/// Input data to pass to Perforce
/// The type of records to return for "stat" responses
/// List of objects returned by the server
public List Command(string Arguments, byte[] InputData, Type StatRecordType)
{
CachedRecordInfo StatRecordInfo = (StatRecordType == null)? null : GetCachedRecordInfo(StatRecordType);
List Responses = new List();
Action>> Handler = (KeyValuePairs) =>
{
if(KeyValuePairs.Count == 0)
{
throw new PerforceException("Unexpected empty record returned by Perforce.");
}
if(KeyValuePairs[0].Key != "code")
{
throw new PerforceException("Expected first returned field to be 'code'");
}
string Code = KeyValuePairs[0].Value as string;
int Idx = 1;
if (Code == "stat" && StatRecordType != null)
{
Responses.Add(ParseResponse(KeyValuePairs, ref Idx, "", StatRecordInfo));
}
else if (Code == "info")
{
Responses.Add(ParseResponse(KeyValuePairs, ref Idx, "", InfoRecordInfo));
}
else if(Code == "error")
{
Responses.Add(ParseResponse(KeyValuePairs, ref Idx, "", ErrorRecordInfo));
}
else
{
throw new PerforceException("Unknown return code for record: {0}", KeyValuePairs[0].Value);
}
};
Command(Arguments, InputData, Handler);
return Responses;
}
///
/// Execute a command and parse the response
///
/// Arguments for the command
/// Input data to pass to Perforce
/// List of objects returned by the server
public PerforceResponseList Command(string Arguments, byte[] InputData) where T : class
{
List Responses = Command(Arguments, InputData, typeof(T));
PerforceResponseList TypedResponses = new PerforceResponseList();
foreach (PerforceResponse Response in Responses)
{
TypedResponses.Add(new PerforceResponse(Response));
}
return TypedResponses;
}
///
/// Attempts to execute the given command, returning the results from the server or the first PerforceResponse object.
///
/// Arguments for the command.
/// Input data for the command.
/// Type of element to return in the response
/// Response from the server; either an object of type T or error.
public PerforceResponse SingleResponseCommand(string Arguments, byte[] InputData, Type StatRecordType)
{
List Responses = Command(Arguments, InputData, StatRecordType);
if(Responses.Count != 1)
{
throw new PerforceException("Expected one result from 'p4 {0}', got {1}", Arguments, Responses.Count);
}
return Responses[0];
}
///
/// Attempts to execute the given command, returning the results from the server or the first PerforceResponse object.
///
/// Type of record to parse
/// Arguments for the command.
/// Input data for the command.
/// Response from the server; either an object of type T or error.
public PerforceResponse SingleResponseCommand(string Arguments, byte[] InputData) where T : class
{
return new PerforceResponse(SingleResponseCommand(Arguments, InputData, typeof(T)));
}
///
/// Parse an individual record from the server.
///
/// List of tagged values returned by the server.
/// Index of the first tagged value to parse.
/// The required suffix for any subobject arrays.
/// Reflection information for the type being serialized into.
/// The parsed object.
PerforceResponse ParseResponse(List> KeyValuePairs, ref int Idx, string RequiredSuffix, CachedRecordInfo RecordInfo)
{
// Create a bitmask for all the required tags
ulong RequiredTagsBitMask = 0;
// Get the record info, and parse it into the object
object NewRecord = Activator.CreateInstance(RecordInfo.Type);
while(Idx < KeyValuePairs.Count)
{
// Split out the tag and value
string Tag = KeyValuePairs[Idx].Key;
string Value = KeyValuePairs[Idx].Value.ToString();
// Parse the suffix from the current key
int SuffixIdx = Tag.Length;
while(SuffixIdx > 0 && (Tag[SuffixIdx - 1] == ',' || (Tag[SuffixIdx - 1] >= '0' && Tag[SuffixIdx - 1] <= '9')))
{
SuffixIdx--;
}
// Split out the suffix
string Suffix = Tag.Substring(SuffixIdx);
Tag = Tag.Substring(0, SuffixIdx);
// Check whether it's a subobject or part of the current object.
if (Suffix == RequiredSuffix)
{
// Part of the current object
CachedTagInfo TagInfo;
if (RecordInfo.TagNameToInfo.TryGetValue(Tag, out TagInfo))
{
FieldInfo FieldInfo = TagInfo.Field;
if (FieldInfo.FieldType == typeof(DateTime))
{
DateTime Time;
if(!DateTime.TryParse(Value, out Time))
{
Time = UnixEpoch + TimeSpan.FromSeconds(long.Parse(Value));
}
FieldInfo.SetValue(NewRecord, Time);
}
else if (FieldInfo.FieldType == typeof(bool))
{
FieldInfo.SetValue(NewRecord, Value.Length == 0 || Value == "true");
}
else if(FieldInfo.FieldType == typeof(Nullable))
{
FieldInfo.SetValue(NewRecord, Value == "true");
}
else if (FieldInfo.FieldType == typeof(int))
{
if(Value == "new" || Value == "none")
{
FieldInfo.SetValue(NewRecord, -1);
}
else if(Value.StartsWith("#"))
{
FieldInfo.SetValue(NewRecord, (Value == "#none") ? 0 : int.Parse(Value.Substring(1)));
}
else if(Value == "default")
{
FieldInfo.SetValue(NewRecord, DefaultChange);
}
else
{
FieldInfo.SetValue(NewRecord, int.Parse(Value));
}
}
else if (FieldInfo.FieldType == typeof(long))
{
FieldInfo.SetValue(NewRecord, long.Parse(Value));
}
else if (FieldInfo.FieldType == typeof(string))
{
FieldInfo.SetValue(NewRecord, Value);
}
else if(FieldInfo.FieldType.IsEnum)
{
FieldInfo.SetValue(NewRecord, ParseEnum(FieldInfo.FieldType, Value));
}
else
{
throw new PerforceException("Unsupported type of {0}.{1} for tag '{0}'", RecordInfo.Type.Name, FieldInfo.FieldType.Name, Tag);
}
RequiredTagsBitMask |= TagInfo.RequiredTagBitMask;
}
Idx++;
}
else if (Suffix.StartsWith(RequiredSuffix) && (RequiredSuffix.Length == 0 || Suffix[RequiredSuffix.Length] == ','))
{
// Part of a subobject. If this record doesn't have any listed subobject type, skip the field and continue.
if (RecordInfo.SubElementField == null)
{
CachedTagInfo TagInfo;
if (RecordInfo.TagNameToInfo.TryGetValue(Tag, out TagInfo))
{
FieldInfo FieldInfo = TagInfo.Field;
if (FieldInfo.FieldType == typeof(List))
{
((List)FieldInfo.GetValue(NewRecord)).Add(Value);
}
else
{
throw new PerforceException("Unsupported type of {0}.{1} for tag '{0}'", RecordInfo.Type.Name, FieldInfo.FieldType.Name, Tag);
}
RequiredTagsBitMask |= TagInfo.RequiredTagBitMask;
}
Idx++;
}
else
{
// Get the expected suffix for the next item based on the number of elements already in the list
System.Collections.IList List = (System.Collections.IList)RecordInfo.SubElementField.GetValue(NewRecord);
string RequiredChildSuffix = (RequiredSuffix.Length == 0) ? String.Format("{0}", List.Count) : String.Format("{0},{1}", RequiredSuffix, List.Count);
if (Suffix != RequiredChildSuffix)
{
throw new PerforceException("Subobject element received out of order; expected {0}{1}, got {0}{2}", Tag, RequiredChildSuffix, Suffix);
}
// Parse the subobject and add it to the list
PerforceResponse Response = ParseResponse(KeyValuePairs, ref Idx, RequiredChildSuffix, RecordInfo.SubElementRecordInfo);
List.Add(Response.Data);
}
}
else
{
break;
}
}
// Make sure we've got all the required tags we need
if (RequiredTagsBitMask != RecordInfo.RequiredTagsBitMask)
{
string MissingTagNames = String.Join(", ", RecordInfo.TagNameToInfo.Where(x => (RequiredTagsBitMask | x.Value.RequiredTagBitMask) != RequiredTagsBitMask).Select(x => x.Key));
throw new PerforceException("Missing '{0}' tag when parsing '{1}'", MissingTagNames, RecordInfo.Type.Name);
}
return new PerforceResponse(NewRecord);
}
///
/// Gets a mapping of flags to enum values for the given type
///
/// The enum type to retrieve flags for
/// Map of name to enum value
static Dictionary GetCachedEnumFlags(Type EnumType)
{
Dictionary NameToValue;
if (!EnumTypeToFlags.TryGetValue(EnumType, out NameToValue))
{
NameToValue = new Dictionary();
FieldInfo[] Fields = EnumType.GetFields(BindingFlags.Public | BindingFlags.Static);
foreach (FieldInfo Field in Fields)
{
PerforceEnumAttribute Attribute = Field.GetCustomAttribute();
if (Attribute != null)
{
NameToValue.Add(Attribute.Name, (int)Field.GetValue(null));
}
}
if (!EnumTypeToFlags.TryAdd(EnumType, NameToValue))
{
NameToValue = EnumTypeToFlags[EnumType];
}
}
return NameToValue;
}
///
/// Parses an enum value, using PerforceEnumAttribute markup for names.
///
/// Type of the enum to parse.
/// Value of the enum.
/// Text for the enum.
string GetEnumText(Type EnumType, object Value)
{
int IntegerValue = (int)Value;
Dictionary NameToValue = GetCachedEnumFlags(EnumType);
if(EnumType.GetCustomAttribute() != null)
{
List Names = new List();
foreach (KeyValuePair Pair in NameToValue)
{
if ((IntegerValue & Pair.Value) != 0)
{
Names.Add(Pair.Key);
}
}
return String.Join(" ", Names);
}
else
{
string Name = null;
foreach (KeyValuePair Pair in NameToValue)
{
if (IntegerValue == Pair.Value)
{
Name = Pair.Key;
break;
}
}
return Name;
}
}
///
/// Parses an enum value, using PerforceEnumAttribute markup for names.
///
/// Type of the enum to parse.
/// Text to parse.
/// The parsed enum value. Unknown values will be ignored.
object ParseEnum(Type EnumType, string Text)
{
Dictionary NameToValue = GetCachedEnumFlags(EnumType);
if(EnumType.GetCustomAttribute() != null)
{
int Result = 0;
foreach (string Item in Text.Split(' '))
{
int ItemValue;
if (NameToValue.TryGetValue(Item, out ItemValue))
{
Result |= ItemValue;
}
}
return Enum.ToObject(EnumType, Result);
}
else
{
int Result;
NameToValue.TryGetValue(Text, out Result);
return Enum.ToObject(EnumType, Result);
}
}
///
/// Gets reflection data for the given record type
///
/// The type to retrieve record info for
/// The cached reflection information for the given type
static CachedRecordInfo GetCachedRecordInfo(Type RecordType)
{
CachedRecordInfo Record;
if (!RecordTypeToInfo.TryGetValue(RecordType, out Record))
{
Record = new CachedRecordInfo();
Record.Type = RecordType;
// Get all the fields for this type
FieldInfo[] Fields = RecordType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
// Build the map of all tags for this record
foreach (FieldInfo Field in Fields)
{
PerforceTagAttribute TagAttribute = Field.GetCustomAttribute();
if (TagAttribute != null)
{
CachedTagInfo Tag = new CachedTagInfo();
Tag.Name = TagAttribute.Name ?? Field.Name;
Tag.Optional = TagAttribute.Optional;
Tag.Field = Field;
if(!Tag.Optional)
{
Tag.RequiredTagBitMask = Record.RequiredTagsBitMask + 1;
if(Tag.RequiredTagBitMask == 0)
{
throw new PerforceException("Too many required tags in {0}; max is {1}", RecordType.Name, sizeof(ulong) * 8);
}
Record.RequiredTagsBitMask |= Tag.RequiredTagBitMask;
}
Record.TagNameToInfo.Add(Tag.Name, Tag);
}
PerforceRecordListAttribute SubElementAttribute = Field.GetCustomAttribute();
if(SubElementAttribute != null)
{
Record.SubElementField = Field;
Record.SubElementType = Field.FieldType.GenericTypeArguments[0];
Record.SubElementRecordInfo = GetCachedRecordInfo(Record.SubElementType);
}
}
// Try to save the record info, or get the version that's already in the cache
if(!RecordTypeToInfo.TryAdd(RecordType, Record))
{
Record = RecordTypeToInfo[RecordType];
}
}
return Record;
}
#endregion
#region p4 add
///
/// Adds files to a pending changelist.
///
/// Changelist to add files to
/// Files to be added
/// Response from the server
public PerforceResponseList Add(int ChangeNumber, params string[] FileNames)
{
return Add(ChangeNumber, null, AddOptions.None, FileNames);
}
///
/// Adds files to a pending changelist.
///
/// Changelist to add files to
/// Type for new files
/// Options for the command
/// Files to be added
/// Response from the server
public PerforceResponseList Add(int ChangeNumber, string FileType, AddOptions Options, params string[] FileNames)
{
StringBuilder Arguments = new StringBuilder("add");
if(ChangeNumber != -1)
{
Arguments.AppendFormat(" -c {0}", ChangeNumber);
}
if((Options & AddOptions.DowngradeToAdd) != 0)
{
Arguments.Append(" -d");
}
if((Options & AddOptions.IncludeWildcards) != 0)
{
Arguments.Append(" -f");
}
if((Options & AddOptions.NoIgnore) != 0)
{
Arguments.Append(" -I");
}
if((Options & AddOptions.PreviewOnly) != 0)
{
Arguments.Append(" -n");
}
if(FileType != null)
{
Arguments.AppendFormat(" -t \"{0}\"", FileType);
}
foreach(string FileName in FileNames)
{
Arguments.AppendFormat(" \"{0}\"", FileName);
}
return Command(Arguments.ToString(), null);
}
#endregion
#region p4 change
///
/// Creates a changelist with the p4 change command.
///
/// Information for the change to create. The number field should be left set to -1.
/// The changelist number, or an error.
public PerforceResponse CreateChange(ChangeRecord Record)
{
if(Record.Number != -1)
{
throw new PerforceException("'Number' field should be set to -1 to create a new changelist.");
}
PerforceResponse Response = SingleResponseCommand("change -i", SerializeRecord(Record), null);
if (Response.Failed)
{
return new PerforceResponse(Response.Error);
}
string[] Tokens = Response.Info.Data.Split(' ');
if (Tokens.Length != 3)
{
throw new PerforceException("Unexpected info response from change command: {0}", Response);
}
return new PerforceResponse(int.Parse(Tokens[1]));
}
///
/// Updates an existing changelist.
///
/// Options for this command
/// Information for the change to create. The number field should be left set to zero.
/// The changelist number, or an error.
public PerforceResponse UpdateChange(UpdateChangeOptions Options, ChangeRecord Record)
{
if(Record.Number == -1)
{
throw new PerforceException("'Number' field must be set to update a changelist.");
}
StringBuilder Arguments = new StringBuilder("change -i");
if((Options & UpdateChangeOptions.Force) != 0)
{
Arguments.Append(" -f");
}
if((Options & UpdateChangeOptions.Submitted) != 0)
{
Arguments.Append(" -u");
}
return SingleResponseCommand(Arguments.ToString(), SerializeRecord(Record), null);
}
///
/// Deletes a changelist (p4 change -d)
///
/// Options for the command
/// Changelist number to delete
/// Response from the server
public PerforceResponse DeleteChange(DeleteChangeOptions Options, int ChangeNumber)
{
StringBuilder Arguments = new StringBuilder("change -d");
if((Options & DeleteChangeOptions.Submitted) != 0)
{
Arguments.Append(" -f");
}
if((Options & DeleteChangeOptions.BeforeRenumber) != 0)
{
Arguments.Append(" -O");
}
Arguments.AppendFormat(" {0}", ChangeNumber);
return SingleResponseCommand(Arguments.ToString(), null, null);
}
///
/// Gets a changelist
///
/// Options for the command
/// Changelist number to retrieve. -1 is the default changelist for this workspace.
/// Response from the server
public PerforceResponse GetChange(GetChangeOptions Options, int ChangeNumber)
{
StringBuilder Arguments = new StringBuilder("change -o");
if((Options & GetChangeOptions.BeforeRenumber) != 0)
{
Arguments.Append(" -O");
}
Arguments.AppendFormat(" {0}", ChangeNumber);
return SingleResponseCommand(Arguments.ToString(), null);
}
byte[] SerializeRecord(ChangeRecord Input)
{
List> NameToValue = new List>();
if (Input.Number == -1)
{
NameToValue.Add(new KeyValuePair("Change", "new"));
}
else
{
NameToValue.Add(new KeyValuePair("Change", Input.Number.ToString()));
}
if (Input.Type != ChangeType.Unspecified)
{
NameToValue.Add(new KeyValuePair("Type", Input.Type.ToString()));
}
if (Input.User != null)
{
NameToValue.Add(new KeyValuePair("User", Input.User));
}
if (Input.Client != null)
{
NameToValue.Add(new KeyValuePair("Client", Input.Client));
}
if (Input.Description != null)
{
NameToValue.Add(new KeyValuePair("Description", Input.Description));
}
return SerializeRecord(NameToValue);
}
#endregion
#region p4 changes
///
/// Enumerates changes on the server
///
/// Options for the command
/// List only the highest numbered changes
/// Limit the list to the changelists with the given status (pending, submitted or shelved)
/// Paths to query changes for
/// List of responses from the server.
public PerforceResponseList Changes(ChangesOptions Options, int MaxChanges, ChangeStatus Status, params string[] FileSpecs)
{
return Changes(Options, null, MaxChanges, Status, null, FileSpecs);
}
///
/// Enumerates changes on the server
///
/// Options for the command
/// List only changes made from the named client workspace.
/// List only the highest numbered changes
/// Limit the list to the changelists with the given status (pending, submitted or shelved)
/// List only changes made by the named user
/// Paths to query changes for
/// List of responses from the server.
public PerforceResponseList Changes(ChangesOptions Options, string ClientName, int MaxChanges, ChangeStatus Status, string UserName, params string[] FileSpecs)
{
StringBuilder Arguments = new StringBuilder("changes");
if ((Options & ChangesOptions.IncludeIntegrations) != 0)
{
Arguments.Append(" -i");
}
if ((Options & ChangesOptions.IncludeTimes) != 0)
{
Arguments.Append(" -t");
}
if ((Options & ChangesOptions.LongOutput) != 0)
{
Arguments.Append(" -l");
}
if ((Options & ChangesOptions.TruncatedLongOutput) != 0)
{
Arguments.Append(" -L");
}
if ((Options & ChangesOptions.IncludeRestricted) != 0)
{
Arguments.Append(" -f");
}
if(ClientName != null)
{
Arguments.AppendFormat(" -c \"{0}\"", ClientName);
}
if(MaxChanges != -1)
{
Arguments.AppendFormat(" -m {0}", MaxChanges);
}
if(Status != ChangeStatus.All)
{
Arguments.AppendFormat(" -s {0}", GetEnumText(typeof(ChangeStatus), Status));
}
if(UserName != null)
{
Arguments.AppendFormat(" -u {0}", UserName);
}
foreach(string FileSpec in FileSpecs)
{
Arguments.AppendFormat(" \"{0}\"", FileSpec);
}
return Command(Arguments.ToString(), null);
}
#endregion
#region p4 client
///
/// Creates a client
///
/// The client record
/// Response from the server
public PerforceResponse CreateClient(ClientRecord Record)
{
return UpdateClient(Record);
}
///
/// Creates a client
///
/// The client record
/// Response from the server
public PerforceResponse UpdateClient(ClientRecord Record)
{
return SingleResponseCommand("client -i", SerializeRecord(Record), null);
}
///
/// Deletes a client
///
/// Options for this command
/// Name of the client
/// Response from the server
public PerforceResponse DeleteClient(DeleteClientOptions Options, string ClientName)
{
StringBuilder Arguments = new StringBuilder("client -d");
if((Options & DeleteClientOptions.Force) != 0)
{
Arguments.Append(" -f");
}
if((Options & DeleteClientOptions.DeleteShelved) != 0)
{
Arguments.Append(" -Fs");
}
Arguments.AppendFormat(" -d \"{0}\"", ClientName);
return SingleResponseCommand(Arguments.ToString(), null);
}
///
/// Changes the stream associated with a client
///
/// The client name
/// The new stream to be associated with the client
/// Options for this command
/// Response from the server
public PerforceResponse SwitchClientToStream(string ClientName, string StreamName, SwitchClientOptions Options)
{
StringBuilder Arguments = new StringBuilder("client -s");
if((Options & SwitchClientOptions.IgnoreOpenFiles) != 0)
{
Arguments.Append(" -f");
}
Arguments.AppendFormat(" -S \"{0}\"", StreamName);
return SingleResponseCommand(Arguments.ToString(), null, null);
}
///
/// Changes a client to mirror a template
///
/// The client name
/// The new stream to be associated with the client
/// Response from the server
public PerforceResponse SwitchClientToTemplate(string ClientName, string TemplateName)
{
string Arguments = String.Format("client -s -t \"{0}\"", TemplateName);
return SingleResponseCommand(Arguments, null, null);
}
///
/// Queries the current client definition
///
/// Name of the client. Specify null for the current client.
/// Response from the server; either a client record or error code
public PerforceResponse GetClient(string ClientName)
{
StringBuilder Arguments = new StringBuilder("client -o");
if(ClientName != null)
{
Arguments.AppendFormat(" \"{0}\"", ClientName);
}
return SingleResponseCommand(Arguments.ToString(), null);
}
///
/// Queries the view for a stream
///
/// Name of the stream.
/// Changelist at which to query the stream view
/// Response from the server; either a client record or error code
public PerforceResponse GetStreamView(string StreamName, int ChangeNumber)
{
StringBuilder Arguments = new StringBuilder("client -o");
Arguments.AppendFormat(" -S \"{0}\"", StreamName);
if(ChangeNumber != -1)
{
Arguments.AppendFormat(" -c {0}", ChangeNumber);
}
return SingleResponseCommand(Arguments.ToString(), null);
}
///
/// Serializes a client record to a byte array
///
/// The input record
/// Serialized record data
byte[] SerializeRecord(ClientRecord Input)
{
List> NameToValue = new List>();
if (Input.Name != null)
{
NameToValue.Add(new KeyValuePair("Client", Input.Name));
}
if (Input.Owner != null)
{
NameToValue.Add(new KeyValuePair("Owner", Input.Owner));
}
if (Input.Host != null)
{
NameToValue.Add(new KeyValuePair("Host", Input.Host));
}
if (Input.Description != null)
{
NameToValue.Add(new KeyValuePair("Description", Input.Description));
}
if (Input.Root != null)
{
NameToValue.Add(new KeyValuePair("Root", Input.Root));
}
if(Input.Options != ClientOptions.None)
{
NameToValue.Add(new KeyValuePair("Options", GetEnumText(typeof(ClientOptions), Input.Options)));
}
if(Input.SubmitOptions != ClientSubmitOptions.Unspecified)
{
NameToValue.Add(new KeyValuePair("SubmitOptions", GetEnumText(typeof(ClientSubmitOptions), Input.SubmitOptions)));
}
if(Input.LineEnd != ClientLineEndings.Unspecified)
{
NameToValue.Add(new KeyValuePair("LineEnd", GetEnumText(typeof(ClientLineEndings), Input.LineEnd)));
}
if(Input.Type != null)
{
NameToValue.Add(new KeyValuePair("Type", Input.Type));
}
if(Input.Stream != null)
{
NameToValue.Add(new KeyValuePair("Stream", Input.Stream));
}
for (int Idx = 0; Idx < Input.View.Count; Idx++)
{
NameToValue.Add(new KeyValuePair(String.Format("View{0}", Idx), Input.View[Idx]));
}
return SerializeRecord(NameToValue);
}
#endregion
#region p4 clients
///
/// Queries the current client definition
///
/// Options for this command
/// List only client workspaces owned by this user.
/// Response from the server; either a client record or error code
public PerforceResponseList Clients(ClientsOptions Options, string UserName)
{
return Clients(Options, null, -1, null, UserName);
}
///
/// Queries the current client definition
///
/// Options for this command
/// List only client workspaces matching filter. Treated as case sensitive if [ClientsOptions.CaseSensitiveFilter] is set.
/// Limit the number of results to return. -1 for all.
/// List client workspaces associated with the specified stream.
/// List only client workspaces owned by this user.
/// Response from the server; either a client record or error code
public PerforceResponseList Clients(ClientsOptions Options, string Filter, int MaxResults, string Stream, string UserName)
{
StringBuilder Arguments = new StringBuilder("clients");
if((Options & ClientsOptions.All) != 0)
{
Arguments.Append(" -a");
}
if (Filter != null)
{
if ((Options & ClientsOptions.CaseSensitiveFilter) != 0)
{
Arguments.AppendFormat(" -e \"{0}\"", Filter);
}
else
{
Arguments.AppendFormat(" -E \"{0}\"", Filter);
}
}
if(MaxResults != -1)
{
Arguments.AppendFormat(" -m {0}", MaxResults);
}
if(Stream != null)
{
Arguments.AppendFormat(" -S \"{0}\"", Stream);
}
if((Options & ClientsOptions.WithTimes) != 0)
{
Arguments.Append(" -t");
}
if(UserName != null)
{
Arguments.AppendFormat(" -u \"{0}\"", UserName);
}
if((Options & ClientsOptions.Unloaded) != 0)
{
Arguments.Append(" -U");
}
return Command(Arguments.ToString(), null);
}
#endregion
#region p4 delete
///
/// Execute the 'delete' command
///
/// Options for the command
/// List of file specifications to query
/// List of responses from the server
public PerforceResponseList Delete(int ChangeNumber, DeleteOptions Options, params string[] FileSpecs)
{
StringBuilder Arguments = new StringBuilder("delete");
if(ChangeNumber != -1)
{
Arguments.AppendFormat(" -c {0}", ChangeNumber);
}
if((Options & DeleteOptions.PreviewOnly) != 0)
{
Arguments.Append(" -n");
}
if((Options & DeleteOptions.KeepWorkspaceFiles) != 0)
{
Arguments.Append(" -k");
}
if((Options & DeleteOptions.WithoutSyncing) != 0)
{
Arguments.Append(" -v");
}
foreach(string FileSpec in FileSpecs)
{
Arguments.AppendFormat(" \"{0}\"", FileSpec);
}
return Command(Arguments.ToString(), null);
}
#endregion
#region p4 describe
///
/// Describes a single changelist
///
/// The changelist number to retrieve description for
/// Response from the server; either a describe record or error code
public PerforceResponse Describe(int ChangeNumber)
{
PerforceResponseList Records = Describe(new int[] { ChangeNumber });
if (Records.Count != 1)
{
throw new PerforceException("Expected only one record returned from p4 describe command, got {0}", Records.Count);
}
return Records[0];
}
///
/// Describes a set of changelists
///
/// The changelist numbers to retrieve descriptions for
/// List of responses from the server
public PerforceResponseList Describe(params int[] ChangeNumbers)
{
StringBuilder Arguments = new StringBuilder("describe -s");
foreach(int ChangeNumber in ChangeNumbers)
{
Arguments.AppendFormat(" {0}", ChangeNumber);
}
return Command(Arguments.ToString(), null);
}
///
/// Describes a set of changelists
///
/// The changelist numbers to retrieve descriptions for
/// List of responses from the server
public PerforceResponseList Describe(DescribeOptions Options, int MaxResults, params int[] ChangeNumbers)
{
StringBuilder Arguments = new StringBuilder("describe -s");
if((Options & DescribeOptions.ShowDescriptionForRestrictedChanges) != 0)
{
Arguments.Append(" -f");
}
if((Options & DescribeOptions.Identity) != 0)
{
Arguments.Append(" -I");
}
if(MaxResults != -1)
{
Arguments.AppendFormat(" -m{0}", MaxResults);
}
if((Options & DescribeOptions.OriginalChangeNumber) != 0)
{
Arguments.Append(" -O");
}
if((Options & DescribeOptions.Shelved) != 0)
{
Arguments.Append(" -S");
}
foreach(int ChangeNumber in ChangeNumbers)
{
Arguments.AppendFormat(" {0}", ChangeNumber);
}
return Command(Arguments.ToString(), null);
}
#endregion
#region p4 edit
///
/// Opens files for edit
///
/// Changelist to add files to
/// Files to be opened for edit
/// Response from the server
public PerforceResponseList Edit(int ChangeNumber, params string[] FileSpecs)
{
return Edit(ChangeNumber, null, EditOptions.None, FileSpecs);
}
///
/// Opens files for edit
///
/// Changelist to add files to
/// Type for new files
/// Options for the command
/// Files to be opened for edit
/// Response from the server
public PerforceResponseList Edit(int ChangeNumber, string FileType, EditOptions Options, params string[] FileSpecs)
{
StringBuilder Arguments = new StringBuilder("edit");
if(ChangeNumber != -1)
{
Arguments.AppendFormat(" -c {0}", ChangeNumber);
}
if((Options & EditOptions.KeepWorkspaceFiles) != 0)
{
Arguments.Append(" -k");
}
if((Options & EditOptions.PreviewOnly) != 0)
{
Arguments.Append(" -n");
}
if(FileType != null)
{
Arguments.AppendFormat(" -t \"{0}\"", FileType);
}
foreach(string FileSpec in FileSpecs)
{
Arguments.AppendFormat(" \"{0}\"", FileSpec);
}
return Command(Arguments.ToString(), null);
}
#endregion
#region p4 filelog
///
/// Execute the 'filelog' command
///
/// Options for the command
/// List of file specifications to query
/// List of responses from the server
public PerforceResponseList FileLog(FileLogOptions Options, params string[] FileSpecs)
{
return FileLog(-1, -1, Options, FileSpecs);
}
///
/// Execute the 'filelog' command
///
/// Number of changelists to show. Ignored if zero or negative.
/// Options for the command
/// List of file specifications to query
/// List of responses from the server
public PerforceResponseList FileLog(int MaxChanges, FileLogOptions Options, params string[] FileSpecs)
{
return FileLog(-1, MaxChanges, Options, FileSpecs);
}
///
/// Execute the 'filelog' command
///
/// Show only files modified by this changelist. Ignored if zero or negative.
/// Number of changelists to show. Ignored if zero or negative.
/// Options for the command
/// List of file specifications to query
/// List of responses from the server
public PerforceResponseList FileLog(int ChangeNumber, int MaxChanges, FileLogOptions Options, params string[] FileSpecs)
{
// Build the argument list
StringBuilder Arguments = new StringBuilder("filelog");
if(ChangeNumber > 0)
{
Arguments.AppendFormat(" -c {0}", ChangeNumber);
}
if((Options & FileLogOptions.ContentHistory) != 0)
{
Arguments.Append(" -h");
}
if((Options & FileLogOptions.FollowAcrossBranches) != 0)
{
Arguments.Append(" -i");
}
if((Options & FileLogOptions.FullDescriptions) != 0)
{
Arguments.Append(" -l");
}
if((Options & FileLogOptions.LongDescriptions) != 0)
{
Arguments.Append(" -L");
}
if(MaxChanges > 0)
{
Arguments.AppendFormat(" -m {0}", MaxChanges);
}
if((Options & FileLogOptions.DoNotFollowPromotedTaskStreams) != 0)
{
Arguments.Append(" -p");
}
if((Options & FileLogOptions.IgnoreNonContributoryIntegrations) != 0)
{
Arguments.Append(" -s");
}
// Always include times to simplify parsing
Arguments.Append(" -t");
// Add the file arguments
foreach(string FileSpec in FileSpecs)
{
Arguments.AppendFormat(" \"{0}\"", FileSpec);
}
// Execute the command
return Command(Arguments.ToString(), null);
}
#endregion
#region p4 fstat
///
/// Execute the 'fstat' command
///
/// Options for the command
/// List of file specifications to query
/// List of responses from the server
public PerforceResponseList FStat(FStatOptions Options, params string[] FileSpecs)
{
return FStat(-1, Options, FileSpecs);
}
///
/// Execute the 'fstat' command
///
/// Produce fstat output for only the first max files.
/// Options for the command
/// List of file specifications to query
/// List of responses from the server
public PerforceResponseList FStat(int MaxFiles, FStatOptions Options, params string[] FileSpecs)
{
return FStat(-1, -1, null, MaxFiles, Options, FileSpecs);
}
///
/// Execute the 'fstat' command
///
/// Return only files affected after the given changelist number.
/// Return only files affected by the given changelist number.
/// List only those files that match the criteria specified.
/// Produce fstat output for only the first max files.
/// Options for the command
/// List of file specifications to query
/// List of responses from the server
public PerforceResponseList FStat(int AfterChangeNumber, int OnlyChangeNumber, string Filter, int MaxFiles, FStatOptions Options, params string[] FileSpecs)
{
// Build the argument list
StringBuilder Arguments = new StringBuilder("fstat");
if (AfterChangeNumber != -1)
{
Arguments.AppendFormat(" -c {0}", AfterChangeNumber);
}
if(OnlyChangeNumber != -1)
{
Arguments.AppendFormat(" -e {0}", OnlyChangeNumber);
}
if(Filter != null)
{
Arguments.AppendFormat(" -F \"{0}\"", Filter);
}
if((Options & FStatOptions.ReportDepotSyntax) != 0)
{
Arguments.Append(" -L");
}
if((Options & FStatOptions.AllRevisions) != 0)
{
Arguments.Append(" -Of");
}
if((Options & FStatOptions.IncludeFileSizes) != 0)
{
Arguments.Append(" -Ol");
}
if((Options & FStatOptions.ClientFileInPerforceSyntax) != 0)
{
Arguments.Append(" -Op");
}
if((Options & FStatOptions.ShowPendingIntegrations) != 0)
{
Arguments.Append(" -Or");
}
if((Options & FStatOptions.ShortenOutput) != 0)
{
Arguments.Append(" -Os");
}
if((Options & FStatOptions.ReverseOrder) != 0)
{
Arguments.Append(" -r");
}
if((Options & FStatOptions.OnlyMapped) != 0)
{
Arguments.Append(" -Rc");
}
if((Options & FStatOptions.OnlyHave) != 0)
{
Arguments.Append(" -Rh");
}
if((Options & FStatOptions.OnlyOpenedBeforeHead) != 0)
{
Arguments.Append(" -Rn");
}
if((Options & FStatOptions.OnlyOpenInWorkspace) != 0)
{
Arguments.Append(" -Ro");
}
if((Options & FStatOptions.OnlyOpenAndResolved) != 0)
{
Arguments.Append(" -Rr");
}
if((Options & FStatOptions.OnlyShelved) != 0)
{
Arguments.Append(" -Rs");
}
if((Options & FStatOptions.OnlyUnresolved) != 0)
{
Arguments.Append(" -Ru");
}
if((Options & FStatOptions.SortByDate) != 0)
{
Arguments.Append(" -Sd");
}
if((Options & FStatOptions.SortByHaveRevision) != 0)
{
Arguments.Append(" -Sh");
}
if((Options & FStatOptions.SortByHeadRevision) != 0)
{
Arguments.Append(" -Sr");
}
if((Options & FStatOptions.SortByFileSize) != 0)
{
Arguments.Append(" -Ss");
}
if((Options & FStatOptions.SortByFileType) != 0)
{
Arguments.Append(" -St");
}
if((Options & FStatOptions.IncludeFilesInUnloadDepot) != 0)
{
Arguments.Append(" -U");
}
// Add the file arguments
foreach(string FileSpec in FileSpecs)
{
Arguments.AppendFormat(" \"{0}\"", FileSpec);
}
// Execute the command
return Command(Arguments.ToString(), null);
}
#endregion
#region p4 info
///
/// Execute the 'info' command
///
/// Options for the command
/// Response from the server; an InfoRecord or error code
public PerforceResponse Info(InfoOptions Options)
{
// Build the argument list
StringBuilder Arguments = new StringBuilder("info");
if((Options & InfoOptions.ShortOutput) != 0)
{
Arguments.Append(" -s");
}
return SingleResponseCommand(Arguments.ToString(), null);
}
#endregion
#region p4 opened
///
/// Execute the 'opened' command
///
/// Options for the command
/// List the files in pending changelist change. To list files in the default changelist, use DefaultChange.
/// List only files that are open in the given client
/// List only files that are opened by the given user
/// Maximum number of results to return
/// Specification for the files to list
/// Response from the server
public PerforceResponseList Opened(OpenedOptions Options, int ChangeNumber, string ClientName, string UserName, int MaxResults, params string[] FileSpecs)
{
// Build the argument list
StringBuilder Arguments = new StringBuilder("opened");
if((Options & OpenedOptions.AllWorkspaces) != 0)
{
Arguments.AppendFormat(" -a");
}
if(ChangeNumber != -1)
{
Arguments.AppendFormat(" -c {0}", ChangeNumber);
}
if(ClientName != null)
{
Arguments.AppendFormat(" -C \"{0}\"", ClientName);
}
if(UserName != null)
{
Arguments.AppendFormat(" -u \"{0}\"", UserName);
}
if(MaxResults == DefaultChange)
{
Arguments.AppendFormat(" -m default");
}
else if(MaxResults != -1)
{
Arguments.AppendFormat(" -m {0}", MaxResults);
}
if((Options & OpenedOptions.ShortOutput) != 0)
{
Arguments.AppendFormat(" -s");
}
foreach(string FileSpec in FileSpecs)
{
Arguments.AppendFormat(" \"{0}\"", FileSpec);
}
return Command(Arguments.ToString(), null);
}
#endregion
#region p4 print
///
/// Execute the 'print' command
///
/// Output file to redirect output to
/// Specification for the files to print
/// Response from the server
public PerforceResponse Print(string OutputFile, string FileSpec)
{
// Build the argument list
StringBuilder Arguments = new StringBuilder("print");
Arguments.AppendFormat(" -o \"{0}\"", OutputFile);
Arguments.AppendFormat(" \"{0}\"", FileSpec);
return SingleResponseCommand(Arguments.ToString(), null);
}
class PrintHandler : IDisposable
{
Dictionary DepotFileToLocalFile;
FileStream OutputStream;
public PrintHandler(Dictionary DepotFileToLocalFile)
{
this.DepotFileToLocalFile = DepotFileToLocalFile;
}
public void Dispose()
{
CloseStream();
}
private void OpenStream(string FileName)
{
CloseStream();
Directory.CreateDirectory(Path.GetDirectoryName(FileName));
OutputStream = File.Open(FileName, FileMode.Create, FileAccess.Write, FileShare.None);
}
private void CloseStream()
{
if(OutputStream != null)
{
OutputStream.Dispose();
OutputStream = null;
}
}
public void HandleRecord(List> Fields)
{
if(Fields[0].Key != "code")
{
throw new Exception("Missing code field");
}
string Value = (string)Fields[0].Value;
if(Value == "stat")
{
string DepotFile = Fields.First(x => x.Key == "depotFile").Value.ToString();
string LocalFile;
if(!DepotFileToLocalFile.TryGetValue(DepotFile, out LocalFile))
{
throw new PerforceException("Depot file '{0}' not found in input dictionary", DepotFile);
}
OpenStream(LocalFile);
}
else if(Value == "binary" || Value == "text")
{
byte[] Data = (byte[])Fields.First(x => x.Key == "data").Value;
OutputStream.Write(Data, 0, Data.Length);
}
else
{
throw new Exception("Unexpected record type");
}
}
}
///
/// Execute the 'print' command
///
/// Output file to redirect output to
/// Specification for the files to print
/// Response from the server
public void Print(Dictionary DepotFileToLocalFile)
{
string ListFileName = Path.GetTempFileName();
try
{
// Write the list of depot files
File.WriteAllLines(ListFileName, DepotFileToLocalFile.Keys);
// Execute Perforce, consuming the binary output into a memory stream
using(PrintHandler Handler = new PrintHandler(DepotFileToLocalFile))
{
Command(String.Format("-x \"{0}\" print", ListFileName), null, Handler.HandleRecord);
}
}
finally
{
File.Delete(ListFileName);
}
}
#endregion
#region p4 reconcile
///
/// Open files for add, delete, and/or edit in order to reconcile a workspace with changes made outside of Perforce.
///
/// Changelist to open files to
/// Options for the command
/// Files to be reverted
/// Response from the server
public PerforceResponseList Reconcile(int ChangeNumber, ReconcileOptions Options, params string[] FileSpecs)
{
StringBuilder Arguments = new StringBuilder("reconcile");
if(ChangeNumber != -1)
{
Arguments.AppendFormat(" -c {0}", ChangeNumber);
}
if((Options & ReconcileOptions.Edit) != 0)
{
Arguments.Append(" -e");
}
if((Options & ReconcileOptions.Add) != 0)
{
Arguments.Append(" -a");
}
if((Options & ReconcileOptions.Delete) != 0)
{
Arguments.Append(" -d");
}
if((Options & ReconcileOptions.PreviewOnly) != 0)
{
Arguments.Append(" -n");
}
if((Options & ReconcileOptions.AllowWildcards) != 0)
{
Arguments.Append(" -f");
}
if((Options & ReconcileOptions.NoIgnore) != 0)
{
Arguments.Append(" -I");
}
if((Options & ReconcileOptions.LocalFileSyntax) != 0)
{
Arguments.Append(" -l");
}
foreach(string FileSpec in FileSpecs)
{
Arguments.AppendFormat(" \"{0}\"", FileSpec);
}
return Command(Arguments.ToString(), null);
}
#endregion
#region p4 resolve
///
/// Resolve conflicts between file revisions.
///
/// Changelist to open files to
/// Options for the command
/// Files to be reverted
/// Response from the server
public PerforceResponseList Resolve(int ChangeNumber, ResolveOptions Options, params string[] FileSpecs)
{
StringBuilder Arguments = new StringBuilder("resolve");
if ((Options & ResolveOptions.Automatic) != 0)
{
Arguments.Append(" -am");
}
if ((Options & ResolveOptions.AcceptYours) != 0)
{
Arguments.Append(" -ay");
}
if ((Options & ResolveOptions.AcceptTheirs) != 0)
{
Arguments.Append(" -at");
}
if ((Options & ResolveOptions.SafeAccept) != 0)
{
Arguments.Append(" -as");
}
if ((Options & ResolveOptions.ForceAccept) != 0)
{
Arguments.Append(" -af");
}
if((Options & ResolveOptions.IgnoreWhitespaceOnly) != 0)
{
Arguments.Append(" -db");
}
if((Options & ResolveOptions.IgnoreWhitespace) != 0)
{
Arguments.Append(" -dw");
}
if((Options & ResolveOptions.IgnoreLineEndings) != 0)
{
Arguments.Append(" -dl");
}
if ((Options & ResolveOptions.ResolveAgain) != 0)
{
Arguments.Append(" -f");
}
if ((Options & ResolveOptions.PreviewOnly) != 0)
{
Arguments.Append(" -n");
}
foreach(string FileSpec in FileSpecs)
{
Arguments.AppendFormat(" \"{0}\"", FileSpec);
}
return Command(Arguments.ToString(), null);
}
#endregion
#region p4 revert
///
/// Reverts files that have been added to a pending changelist.
///
/// Changelist to add files to
/// Revert another user’s open files.
/// Options for the command
/// Files to be reverted
/// Response from the server
public PerforceResponseList Revert(int ChangeNumber, string ClientName, RevertOptions Options, params string[] FileSpecs)
{
StringBuilder Arguments = new StringBuilder("revert");
if((Options & RevertOptions.Unchanged) != 0)
{
Arguments.Append(" -a");
}
if((Options & RevertOptions.PreviewOnly) != 0)
{
Arguments.Append(" -n");
}
if((Options & RevertOptions.KeepWorkspaceFiles) != 0)
{
Arguments.Append(" -k");
}
if((Options & RevertOptions.DeleteAddedFiles) != 0)
{
Arguments.Append(" -w");
}
if(ChangeNumber != -1)
{
Arguments.AppendFormat(" -c {0}", ChangeNumber);
}
if(ClientName != null)
{
Arguments.AppendFormat(" -C \"{0}\"", ClientName);
}
foreach(string FileSpec in FileSpecs)
{
Arguments.AppendFormat(" \"{0}\"", FileSpec);
}
return Command(Arguments.ToString(), null);
}
#endregion
#region p4 shelve
///
/// Shelves a set of files
///
/// The change number to receive the shelved files
/// Options for the command
/// Files to sync
/// Response from the server
public PerforceResponseList Shelve(int ChangeNumber, ShelveOptions Options, params string[] FileSpecs)
{
StringBuilder Arguments = new StringBuilder("shelve");
Arguments.AppendFormat(" -c {0}", ChangeNumber);
if((Options & ShelveOptions.OnlyChanged) != 0)
{
Arguments.Append(" -a leaveunchanged");
}
if((Options & ShelveOptions.Overwrite) != 0)
{
Arguments.Append(" -f");
}
foreach(string FileSpec in FileSpecs)
{
Arguments.AppendFormat(" \"{0}\"", FileSpec);
}
return Command(Arguments.ToString(), null);
}
///
/// Deletes files from a shelved changelist
///
/// Changelist containing shelved files to be deleted
/// Files to delete
/// Response from the server
public PerforceResponse DeleteShelvedFiles(int ChangeNumber, params string[] FileSpecs)
{
StringBuilder Arguments = new StringBuilder("shelve -d");
if(ChangeNumber != -1)
{
Arguments.AppendFormat(" -c {0}", ChangeNumber);
}
foreach (string FileSpec in FileSpecs)
{
Arguments.AppendFormat(" \"{0}\"", FileSpec);
}
return SingleResponseCommand(Arguments.ToString(), null, null);
}
#endregion
#region p4 streams
///
/// Enumerates all streams in a depot
///
/// The path for streams to enumerate (eg. "//UE4/...")
/// List of streams matching the given criteria
public PerforceResponseList Streams(string StreamPath)
{
return Streams(StreamPath, -1, null, false);
}
///
/// Enumerates all streams in a depot
///
/// The path for streams to enumerate (eg. "//UE4/...")
/// Maximum number of results to return
/// Additional filter to be applied to the results
/// Whether to enumerate unloaded workspaces
/// List of streams matching the given criteria
public PerforceResponseList Streams(string StreamPath, int MaxResults, string Filter, bool bUnloaded)
{
// Build the command line
StringBuilder Arguments = new StringBuilder("streams");
if (bUnloaded)
{
Arguments.Append(" -U");
}
if (Filter != null)
{
Arguments.AppendFormat("-F \"{0}\"", Filter);
}
if (MaxResults > 0)
{
Arguments.AppendFormat("-m {0}", MaxResults);
}
Arguments.AppendFormat(" \"{0}\"", StreamPath);
// Execute the command
return Command(Arguments.ToString(), null);
}
#endregion
#region p4 submit
///
/// Submits a pending changelist
///
/// The changelist to submit
/// Options for the command
/// Response from the server
public PerforceResponse Submit(int ChangeNumber, SubmitOptions Options)
{
StringBuilder Arguments = new StringBuilder("submit");
if((Options & SubmitOptions.ReopenAsEdit) != 0)
{
Arguments.Append(" -r");
}
Arguments.AppendFormat(" -c {0}", ChangeNumber);
return Command(Arguments.ToString(), null)[0];
}
#endregion
#region p4 sync
///
/// Syncs files from the server
///
/// Files to sync
/// Response from the server
public PerforceResponseList Sync(params string[] FileSpecs)
{
return Sync(SyncOptions.None, -1, FileSpecs);
}
///
/// Syncs files from the server
///
/// Options for the command
/// Syncs only the first number of files specified.
/// Files to sync
/// Response from the server
public PerforceResponseList Sync(SyncOptions Options, int MaxFiles, params string[] FileSpecs)
{
return Sync(Options, -1, -1, -1, -1, -1, -1, FileSpecs);
}
///
/// Syncs files from the server
///
/// Options for the command
/// Syncs only the first number of files specified
/// Sync in parallel using the given number of threads
/// The number of files in a batch
/// The number of bytes in a batch
/// Minimum number of files in a parallel sync
/// Minimum number of bytes in a parallel sync
/// Files to sync
/// Response from the server
public PerforceResponseList Sync(SyncOptions Options, int MaxFiles, int NumThreads, int Batch, int BatchSize, int Min, int MinSize, params string[] FileSpecs)
{
string Arguments = GetSyncArguments(Options, MaxFiles, NumThreads, Batch, BatchSize, Min, MinSize, FileSpecs, false);
return Command(Arguments.ToString(), null);
}
///
/// Syncs files from the server
///
/// Options for the command
/// Syncs only the first number of files specified.
/// Files to sync
/// Response from the server
public PerforceResponseList SyncQuiet(SyncOptions Options, int MaxFiles, params string[] FileSpecs)
{
return SyncQuiet(Options, -1, -1, -1, -1, -1, -1, FileSpecs);
}
///
/// Syncs files from the server without returning detailed file info
///
/// Options for the command
/// Syncs only the first number of files specified
/// Sync in parallel using the given number of threads
/// The number of files in a batch
/// The number of bytes in a batch
/// Minimum number of files in a parallel sync
/// Minimum number of bytes in a parallel sync
/// Files to sync
/// Response from the server
public PerforceResponseList SyncQuiet(SyncOptions Options, int MaxFiles, int NumThreads, int Batch, int BatchSize, int Min, int MinSize, params string[] FileSpecs)
{
string Arguments = GetSyncArguments(Options, MaxFiles, NumThreads, Batch, BatchSize, Min, MinSize, FileSpecs, true);
return Command(Arguments.ToString(), null);
}
///
/// Gets arguments for a sync command
///
/// Options for the command
/// Syncs only the first number of files specified
/// Sync in parallel using the given number of threads
/// The number of files in a batch
/// The number of bytes in a batch
/// Minimum number of files in a parallel sync
/// Minimum number of bytes in a parallel sync
/// Files to sync
/// Whether to use quiet output
/// Arguments for the command
static string GetSyncArguments(SyncOptions Options, int MaxFiles, int NumThreads, int Batch, int BatchSize, int Min, int MinSize, string[] FileSpecs, bool bQuiet)
{
StringBuilder Arguments = new StringBuilder("sync");
if((Options & SyncOptions.Force) != 0)
{
Arguments.Append(" -f");
}
if((Options & SyncOptions.KeepWorkspaceFiles) != 0)
{
Arguments.Append(" -k");
}
if((Options & SyncOptions.FullDepotSyntax) != 0)
{
Arguments.Append(" -L");
}
if((Options & SyncOptions.PreviewOnly) != 0)
{
Arguments.Append(" -n");
}
if((Options & SyncOptions.NetworkPreviewOnly) != 0)
{
Arguments.Append(" -N");
}
if((Options & SyncOptions.DoNotUpdateHaveList) != 0)
{
Arguments.Append(" -p");
}
if(bQuiet)
{
Arguments.Append(" -q");
}
if((Options & SyncOptions.ReopenMovedFiles) != 0)
{
Arguments.Append(" -r");
}
if((Options & SyncOptions.Safe) != 0)
{
Arguments.Append(" -s");
}
if(MaxFiles != -1)
{
Arguments.AppendFormat(" -m {0}", MaxFiles);
}
if(NumThreads != -1)
{
Arguments.AppendFormat(" --parallel-threads={0}", NumThreads);
if(Batch != -1)
{
Arguments.AppendFormat(",batch={0}", Batch);
}
if(BatchSize != -1)
{
Arguments.AppendFormat(",batchsize={0}", BatchSize);
}
if(Min != -1)
{
Arguments.AppendFormat(",min={0}", Min);
}
if(MinSize != -1)
{
Arguments.AppendFormat(",minsize={0}", MinSize);
}
}
foreach (string FileSpec in FileSpecs)
{
Arguments.AppendFormat(" \"{0}\"", FileSpec);
}
return Arguments.ToString();
}
#endregion
#region p4 unshelve
///
/// Restore shelved files from a pending change into a workspace
///
/// The changelist containing shelved files
/// The changelist to receive the unshelved files
/// The branchspec to use when unshelving files
/// Specifies the use of a stream-derived branch view to map the shelved files between the specified stream and its parent stream.
/// Unshelve to the specified parent stream. Overrides the parent defined in the source stream specification.
/// Options for the command
/// Files to unshelve
/// Response from the server
public PerforceResponseList Unshelve(int ChangeNumber, int IntoChangeNumber, string UsingBranchSpec, string UsingStream, string ForceParentStream, UnshelveOptions Options, params string[] FileSpecs)
{
StringBuilder Arguments = new StringBuilder("unshelve");
Arguments.AppendFormat(" -s {0}", ChangeNumber);
if((Options & UnshelveOptions.ForceOverwrite) != 0)
{
Arguments.Append(" -f");
}
if((Options & UnshelveOptions.PreviewOnly) != 0)
{
Arguments.Append(" -n");
}
if(IntoChangeNumber != -1)
{
Arguments.AppendFormat(" -c {0}", IntoChangeNumber);
}
if(UsingBranchSpec != null)
{
Arguments.AppendFormat(" -b \"{0}\"", UsingBranchSpec);
}
if(UsingStream != null)
{
Arguments.AppendFormat(" -S \"{0}\"", UsingStream);
}
if(ForceParentStream != null)
{
Arguments.AppendFormat(" -P \"{0}\"", ForceParentStream);
}
foreach(string FileSpec in FileSpecs)
{
Arguments.AppendFormat(" \"{0}\"", FileSpec);
}
return Command(Arguments.ToString(), null);
}
#endregion
#region p4 user
///
/// Enumerates all streams in a depot
///
/// Name of the user to fetch information for
/// Response from the server
public PerforceResponse User(string UserName)
{
StringBuilder Arguments = new StringBuilder("user");
Arguments.AppendFormat(" -o \"{0}\"", UserName);
return SingleResponseCommand(Arguments.ToString(), null);
}
#endregion
#region p4 where
///
/// Retrieves the location of a file of set of files in the workspace
///
/// Patterns for the files to query
/// List of responses from the server
public PerforceResponseList Where(params string[] FileSpecs)
{
StringBuilder Arguments = new StringBuilder("where");
foreach(string FileSpec in FileSpecs)
{
Arguments.AppendFormat(" \"{0}\"", FileSpec);
}
return Command(Arguments.ToString(), null);
}
#endregion
}
}