Files
UnrealEngineUWP/Engine/Source/Programs/DotNETCommon/DotNETUtilities/Perforce/PerforceConnection.cs
2019-07-16 11:39:11 -04:00

2181 lines
69 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
{
/// <summary>
/// Stores settings for communicating with a Perforce server.
/// </summary>
public class PerforceConnection
{
/// <summary>
/// Constant for the default changelist, where valid.
/// </summary>
public const int DefaultChange = -2;
#region Plumbing
/// <summary>
/// Stores cached information about a field with a P4Tag attribute
/// </summary>
class CachedTagInfo
{
/// <summary>
/// Name of the tag. Specified in the attribute or inferred from the field name.
/// </summary>
public string Name;
/// <summary>
/// Whether this tag is optional or not.
/// </summary>
public bool Optional;
/// <summary>
/// The field containing the value of this data.
/// </summary>
public FieldInfo Field;
/// <summary>
/// Index into the bitmask of required types
/// </summary>
public ulong RequiredTagBitMask;
}
/// <summary>
/// Stores cached information about a record
/// </summary>
class CachedRecordInfo
{
/// <summary>
/// Type of the record
/// </summary>
public Type Type;
/// <summary>
/// Map of tag names to their cached reflection information
/// </summary>
public Dictionary<string, CachedTagInfo> TagNameToInfo = new Dictionary<string, CachedTagInfo>();
/// <summary>
/// Bitmask of all the required tags. Formed by bitwise-or'ing the RequiredTagBitMask fields for each required CachedTagInfo.
/// </summary>
public ulong RequiredTagsBitMask;
/// <summary>
/// The type of records to create for subelements
/// </summary>
public Type SubElementType;
/// <summary>
/// The cached record info for the subelement type
/// </summary>
public CachedRecordInfo SubElementRecordInfo;
/// <summary>
/// Field containing subelements
/// </summary>
public FieldInfo SubElementField;
}
/// <summary>
/// Unix epoch; used for converting times back into C# datetime objects
/// </summary>
static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
/// <summary>
/// Cached map of enum types to a lookup mapping from p4 strings to enum values.
/// </summary>
static ConcurrentDictionary<Type, Dictionary<string, int>> EnumTypeToFlags = new ConcurrentDictionary<Type, Dictionary<string, int>>();
/// <summary>
/// Cached set of record
/// </summary>
static ConcurrentDictionary<Type, CachedRecordInfo> RecordTypeToInfo = new ConcurrentDictionary<Type, CachedRecordInfo>();
/// <summary>
/// Default type for info
/// </summary>
static CachedRecordInfo InfoRecordInfo = GetCachedRecordInfo(typeof(PerforceInfo));
/// <summary>
/// Default type for errors
/// </summary>
static CachedRecordInfo ErrorRecordInfo = GetCachedRecordInfo(typeof(PerforceError));
/// <summary>
/// Global options for each command
/// </summary>
public readonly string GlobalOptions;
/// <summary>
/// Constructor
/// </summary>
/// <param name="GlobalOptions">Global options to pass to every Perforce command</param>
public PerforceConnection(string GlobalOptions)
{
this.GlobalOptions = GlobalOptions;
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="ServerAndPort">The server address and port</param>
/// <param name="UserName">The user name</param>
/// <param name="ClientName">The client name</param>
public PerforceConnection(string ServerAndPort, string UserName, string ClientName)
{
List<string> Options = new List<string>();
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);
}
/// <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="InputData">Input data to pass to the Perforce server. May be null.</param>
/// <param name="RecordHandler">Handler for each received record.</param>
public void Command(string CommandLine, byte[] InputData, Action<List<KeyValuePair<string, object>>> RecordHandler)
{
using(PerforceChildProcess Process = new PerforceChildProcess(InputData, "{0} {1}", GlobalOptions, CommandLine))
{
List<KeyValuePair<string, object>> Record = new List<KeyValuePair<string, object>>();
while(Process.TryReadRecord(Record))
{
RecordHandler(Record);
}
}
}
/// <summary>
/// Serializes a list of key/value pairs into binary format.
/// </summary>
/// <param name="KeyValuePairs">List of key value pairs</param>
/// <returns>Serialized record data</returns>
static byte[] SerializeRecord(List<KeyValuePair<string, object>> KeyValuePairs)
{
MemoryStream Stream = new MemoryStream();
using (BinaryWriter Writer = new BinaryWriter(Stream))
{
Writer.Write((byte)'{');
foreach(KeyValuePair<string, object> 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();
}
/// <summary>
/// Execute a command and parse the response
/// </summary>
/// <param name="Arguments">Arguments for the command</param>
/// <param name="InputData">Input data to pass to Perforce</param>
/// <param name="StatRecordType">The type of records to return for "stat" responses</param>
/// <returns>List of objects returned by the server</returns>
public List<PerforceResponse> Command(string Arguments, byte[] InputData, Type StatRecordType)
{
CachedRecordInfo StatRecordInfo = (StatRecordType == null)? null : GetCachedRecordInfo(StatRecordType);
List<PerforceResponse> Responses = new List<PerforceResponse>();
Action<List<KeyValuePair<string, object>>> 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;
}
/// <summary>
/// Execute a command and parse the response
/// </summary>
/// <param name="Arguments">Arguments for the command</param>
/// <param name="InputData">Input data to pass to Perforce</param>
/// <returns>List of objects returned by the server</returns>
public PerforceResponseList<T> Command<T>(string Arguments, byte[] InputData) where T : class
{
List<PerforceResponse> Responses = Command(Arguments, InputData, typeof(T));
PerforceResponseList<T> TypedResponses = new PerforceResponseList<T>();
foreach (PerforceResponse Response in Responses)
{
TypedResponses.Add(new PerforceResponse<T>(Response));
}
return TypedResponses;
}
/// <summary>
/// Attempts to execute the given command, returning the results from the server or the first PerforceResponse object.
/// </summary>
/// <param name="Arguments">Arguments for the command.</param>
/// <param name="InputData">Input data for the command.</param>
/// <param name="StatRecordType">Type of element to return in the response</param>
/// <returns>Response from the server; either an object of type T or error.</returns>
public PerforceResponse SingleResponseCommand(string Arguments, byte[] InputData, Type StatRecordType)
{
List<PerforceResponse> 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];
}
/// <summary>
/// Attempts to execute the given command, returning the results from the server or the first PerforceResponse object.
/// </summary>
/// <typeparam name="T">Type of record to parse</typeparam>
/// <param name="Arguments">Arguments for the command.</param>
/// <param name="InputData">Input data for the command.</param>
/// <returns>Response from the server; either an object of type T or error.</returns>
public PerforceResponse<T> SingleResponseCommand<T>(string Arguments, byte[] InputData) where T : class
{
return new PerforceResponse<T>(SingleResponseCommand(Arguments, InputData, typeof(T)));
}
/// <summary>
/// Parse an individual record from the server.
/// </summary>
/// <param name="KeyValuePairs">List of tagged values returned by the server.</param>
/// <param name="Idx">Index of the first tagged value to parse.</param>
/// <param name="RequiredSuffix">The required suffix for any subobject arrays.</param>
/// <param name="RecordInfo">Reflection information for the type being serialized into.</param>
/// <returns>The parsed object.</returns>
PerforceResponse ParseResponse(List<KeyValuePair<string, object>> 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<bool>))
{
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<string>))
{
((List<string>)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);
}
/// <summary>
/// Gets a mapping of flags to enum values for the given type
/// </summary>
/// <param name="EnumType">The enum type to retrieve flags for</param>
/// <returns>Map of name to enum value</returns>
static Dictionary<string, int> GetCachedEnumFlags(Type EnumType)
{
Dictionary<string, int> NameToValue;
if (!EnumTypeToFlags.TryGetValue(EnumType, out NameToValue))
{
NameToValue = new Dictionary<string, int>();
FieldInfo[] Fields = EnumType.GetFields(BindingFlags.Public | BindingFlags.Static);
foreach (FieldInfo Field in Fields)
{
PerforceEnumAttribute Attribute = Field.GetCustomAttribute<PerforceEnumAttribute>();
if (Attribute != null)
{
NameToValue.Add(Attribute.Name, (int)Field.GetValue(null));
}
}
if (!EnumTypeToFlags.TryAdd(EnumType, NameToValue))
{
NameToValue = EnumTypeToFlags[EnumType];
}
}
return NameToValue;
}
/// <summary>
/// Parses an enum value, using PerforceEnumAttribute markup for names.
/// </summary>
/// <param name="EnumType">Type of the enum to parse.</param>
/// <param name="Value">Value of the enum.</param>
/// <returns>Text for the enum.</returns>
string GetEnumText(Type EnumType, object Value)
{
int IntegerValue = (int)Value;
Dictionary<string, int> NameToValue = GetCachedEnumFlags(EnumType);
if(EnumType.GetCustomAttribute<FlagsAttribute>() != null)
{
List<string> Names = new List<string>();
foreach (KeyValuePair<string, int> Pair in NameToValue)
{
if ((IntegerValue & Pair.Value) != 0)
{
Names.Add(Pair.Key);
}
}
return String.Join(" ", Names);
}
else
{
string Name = null;
foreach (KeyValuePair<string, int> Pair in NameToValue)
{
if (IntegerValue == Pair.Value)
{
Name = Pair.Key;
break;
}
}
return Name;
}
}
/// <summary>
/// Parses an enum value, using PerforceEnumAttribute markup for names.
/// </summary>
/// <param name="EnumType">Type of the enum to parse.</param>
/// <param name="Text">Text to parse.</param>
/// <returns>The parsed enum value. Unknown values will be ignored.</returns>
object ParseEnum(Type EnumType, string Text)
{
Dictionary<string, int> NameToValue = GetCachedEnumFlags(EnumType);
if(EnumType.GetCustomAttribute<FlagsAttribute>() != 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);
}
}
/// <summary>
/// Gets reflection data for the given record type
/// </summary>
/// <param name="RecordType">The type to retrieve record info for</param>
/// <returns>The cached reflection information for the given type</returns>
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<PerforceTagAttribute>();
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<PerforceRecordListAttribute>();
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
/// <summary>
/// Adds files to a pending changelist.
/// </summary>
/// <param name="ChangeNumber">Changelist to add files to</param>
/// <param name="FileNames">Files to be added</param>
/// <returns>Response from the server</returns>
public PerforceResponseList<AddRecord> Add(int ChangeNumber, params string[] FileNames)
{
return Add(ChangeNumber, null, AddOptions.None, FileNames);
}
/// <summary>
/// Adds files to a pending changelist.
/// </summary>
/// <param name="ChangeNumber">Changelist to add files to</param>
/// <param name="FileType">Type for new files</param>
/// <param name="Options">Options for the command</param>
/// <param name="FileNames">Files to be added</param>
/// <returns>Response from the server</returns>
public PerforceResponseList<AddRecord> 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<AddRecord>(Arguments.ToString(), null);
}
#endregion
#region p4 change
/// <summary>
/// Creates a changelist with the p4 change command.
/// </summary>
/// <param name="Record">Information for the change to create. The number field should be left set to -1.</param>
/// <returns>The changelist number, or an error.</returns>
public PerforceResponse<int> 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<int>(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>(int.Parse(Tokens[1]));
}
/// <summary>
/// Updates an existing changelist.
/// </summary>
/// <param name="Options">Options for this command</param>
/// <param name="Record">Information for the change to create. The number field should be left set to zero.</param>
/// <returns>The changelist number, or an error.</returns>
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);
}
/// <summary>
/// Deletes a changelist (p4 change -d)
/// </summary>
/// <param name="Options">Options for the command</param>
/// <param name="ChangeNumber">Changelist number to delete</param>
/// <returns>Response from the server</returns>
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);
}
/// <summary>
/// Gets a changelist
/// </summary>
/// <param name="Options">Options for the command</param>
/// <param name="ChangeNumber">Changelist number to retrieve. -1 is the default changelist for this workspace.</param>
/// <returns>Response from the server</returns>
public PerforceResponse<ChangeRecord> 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<ChangeRecord>(Arguments.ToString(), null);
}
byte[] SerializeRecord(ChangeRecord Input)
{
List<KeyValuePair<string, object>> NameToValue = new List<KeyValuePair<string, object>>();
if (Input.Number == -1)
{
NameToValue.Add(new KeyValuePair<string, object>("Change", "new"));
}
else
{
NameToValue.Add(new KeyValuePair<string, object>("Change", Input.Number.ToString()));
}
if (Input.Type != ChangeType.Unspecified)
{
NameToValue.Add(new KeyValuePair<string, object>("Type", Input.Type.ToString()));
}
if (Input.User != null)
{
NameToValue.Add(new KeyValuePair<string, object>("User", Input.User));
}
if (Input.Client != null)
{
NameToValue.Add(new KeyValuePair<string, object>("Client", Input.Client));
}
if (Input.Description != null)
{
NameToValue.Add(new KeyValuePair<string, object>("Description", Input.Description));
}
return SerializeRecord(NameToValue);
}
#endregion
#region p4 changes
/// <summary>
/// Enumerates changes on the server
/// </summary>
/// <param name="Options">Options for the command</param>
/// <param name="MaxChanges">List only the highest numbered changes</param>
/// <param name="Status">Limit the list to the changelists with the given status (pending, submitted or shelved)</param>
/// <param name="FileSpecs">Paths to query changes for</param>
/// <returns>List of responses from the server.</returns>
public PerforceResponseList<ChangesRecord> Changes(ChangesOptions Options, int MaxChanges, ChangeStatus Status, params string[] FileSpecs)
{
return Changes(Options, null, MaxChanges, Status, null, FileSpecs);
}
/// <summary>
/// Enumerates changes on the server
/// </summary>
/// <param name="Options">Options for the command</param>
/// <param name="ClientName">List only changes made from the named client workspace.</param>
/// <param name="MaxChanges">List only the highest numbered changes</param>
/// <param name="Status">Limit the list to the changelists with the given status (pending, submitted or shelved)</param>
/// <param name="UserName">List only changes made by the named user</param>
/// <param name="FileSpecs">Paths to query changes for</param>
/// <returns>List of responses from the server.</returns>
public PerforceResponseList<ChangesRecord> 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<ChangesRecord>(Arguments.ToString(), null);
}
#endregion
#region p4 client
/// <summary>
/// Creates a client
/// </summary>
/// <param name="Record">The client record</param>
/// <returns>Response from the server</returns>
public PerforceResponse CreateClient(ClientRecord Record)
{
return UpdateClient(Record);
}
/// <summary>
/// Creates a client
/// </summary>
/// <param name="Record">The client record</param>
/// <returns>Response from the server</returns>
public PerforceResponse UpdateClient(ClientRecord Record)
{
return SingleResponseCommand("client -i", SerializeRecord(Record), null);
}
/// <summary>
/// Deletes a client
/// </summary>
/// <param name="Options">Options for this command</param>
/// <param name="ClientName">Name of the client</param>
/// <returns>Response from the server</returns>
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<ClientRecord>(Arguments.ToString(), null);
}
/// <summary>
/// Changes the stream associated with a client
/// </summary>
/// <param name="ClientName">The client name</param>
/// <param name="StreamName">The new stream to be associated with the client</param>
/// <param name="Options">Options for this command</param>
/// <returns>Response from the server</returns>
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);
}
/// <summary>
/// Changes a client to mirror a template
/// </summary>
/// <param name="ClientName">The client name</param>
/// <param name="TemplateName">The new stream to be associated with the client</param>
/// <returns>Response from the server</returns>
public PerforceResponse SwitchClientToTemplate(string ClientName, string TemplateName)
{
string Arguments = String.Format("client -s -t \"{0}\"", TemplateName);
return SingleResponseCommand(Arguments, null, null);
}
/// <summary>
/// Queries the current client definition
/// </summary>
/// <param name="ClientName">Name of the client. Specify null for the current client.</param>
/// <returns>Response from the server; either a client record or error code</returns>
public PerforceResponse<ClientRecord> GetClient(string ClientName)
{
StringBuilder Arguments = new StringBuilder("client -o");
if(ClientName != null)
{
Arguments.AppendFormat(" \"{0}\"", ClientName);
}
return SingleResponseCommand<ClientRecord>(Arguments.ToString(), null);
}
/// <summary>
/// Queries the view for a stream
/// </summary>
/// <param name="StreamName">Name of the stream.</param>
/// <param name="ChangeNumber">Changelist at which to query the stream view</param>
/// <returns>Response from the server; either a client record or error code</returns>
public PerforceResponse<ClientRecord> 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<ClientRecord>(Arguments.ToString(), null);
}
/// <summary>
/// Serializes a client record to a byte array
/// </summary>
/// <param name="Input">The input record</param>
/// <returns>Serialized record data</returns>
byte[] SerializeRecord(ClientRecord Input)
{
List<KeyValuePair<string, object>> NameToValue = new List<KeyValuePair<string, object>>();
if (Input.Name != null)
{
NameToValue.Add(new KeyValuePair<string, object>("Client", Input.Name));
}
if (Input.Owner != null)
{
NameToValue.Add(new KeyValuePair<string, object>("Owner", Input.Owner));
}
if (Input.Host != null)
{
NameToValue.Add(new KeyValuePair<string, object>("Host", Input.Host));
}
if (Input.Description != null)
{
NameToValue.Add(new KeyValuePair<string, object>("Description", Input.Description));
}
if (Input.Root != null)
{
NameToValue.Add(new KeyValuePair<string, object>("Root", Input.Root));
}
if(Input.Options != ClientOptions.None)
{
NameToValue.Add(new KeyValuePair<string, object>("Options", GetEnumText(typeof(ClientOptions), Input.Options)));
}
if(Input.SubmitOptions != ClientSubmitOptions.Unspecified)
{
NameToValue.Add(new KeyValuePair<string, object>("SubmitOptions", GetEnumText(typeof(ClientSubmitOptions), Input.SubmitOptions)));
}
if(Input.LineEnd != ClientLineEndings.Unspecified)
{
NameToValue.Add(new KeyValuePair<string, object>("LineEnd", GetEnumText(typeof(ClientLineEndings), Input.LineEnd)));
}
if(Input.Type != null)
{
NameToValue.Add(new KeyValuePair<string, object>("Type", Input.Type));
}
if(Input.Stream != null)
{
NameToValue.Add(new KeyValuePair<string, object>("Stream", Input.Stream));
}
for (int Idx = 0; Idx < Input.View.Count; Idx++)
{
NameToValue.Add(new KeyValuePair<string, object>(String.Format("View{0}", Idx), Input.View[Idx]));
}
return SerializeRecord(NameToValue);
}
#endregion
#region p4 clients
/// <summary>
/// Queries the current client definition
/// </summary>
/// <param name="Options">Options for this command</param>
/// <param name="UserName">List only client workspaces owned by this user.</param>
/// <returns>Response from the server; either a client record or error code</returns>
public PerforceResponseList<ClientsRecord> Clients(ClientsOptions Options, string UserName)
{
return Clients(Options, null, -1, null, UserName);
}
/// <summary>
/// Queries the current client definition
/// </summary>
/// <param name="Options">Options for this command</param>
/// <param name="Filter">List only client workspaces matching filter. Treated as case sensitive if <ref>ClientsOptions.CaseSensitiveFilter</ref> is set.</param>
/// <param name="MaxResults">Limit the number of results to return. -1 for all.</param>
/// <param name="Stream">List client workspaces associated with the specified stream.</param>
/// <param name="UserName">List only client workspaces owned by this user.</param>
/// <returns>Response from the server; either a client record or error code</returns>
public PerforceResponseList<ClientsRecord> 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<ClientsRecord>(Arguments.ToString(), null);
}
#endregion
#region p4 delete
/// <summary>
/// Execute the 'delete' command
/// </summary>
/// <param name="Options">Options for the command</param>
/// <param name="FileSpecs">List of file specifications to query</param>
/// <returns>List of responses from the server</returns>
public PerforceResponseList<DeleteRecord> 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<DeleteRecord>(Arguments.ToString(), null);
}
#endregion
#region p4 describe
/// <summary>
/// Describes a single changelist
/// </summary>
/// <param name="ChangeNumber">The changelist number to retrieve description for</param>
/// <returns>Response from the server; either a describe record or error code</returns>
public PerforceResponse<DescribeRecord> Describe(int ChangeNumber)
{
PerforceResponseList<DescribeRecord> 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];
}
/// <summary>
/// Describes a set of changelists
/// </summary>
/// <param name="ChangeNumbers">The changelist numbers to retrieve descriptions for</param>
/// <returns>List of responses from the server</returns>
public PerforceResponseList<DescribeRecord> Describe(params int[] ChangeNumbers)
{
StringBuilder Arguments = new StringBuilder("describe -s");
foreach(int ChangeNumber in ChangeNumbers)
{
Arguments.AppendFormat(" {0}", ChangeNumber);
}
return Command<DescribeRecord>(Arguments.ToString(), null);
}
/// <summary>
/// Describes a set of changelists
/// </summary>
/// <param name="ChangeNumbers">The changelist numbers to retrieve descriptions for</param>
/// <returns>List of responses from the server</returns>
public PerforceResponseList<DescribeRecord> 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<DescribeRecord>(Arguments.ToString(), null);
}
#endregion
#region p4 edit
/// <summary>
/// Opens files for edit
/// </summary>
/// <param name="ChangeNumber">Changelist to add files to</param>
/// <param name="FileSpecs">Files to be opened for edit</param>
/// <returns>Response from the server</returns>
public PerforceResponseList<EditRecord> Edit(int ChangeNumber, params string[] FileSpecs)
{
return Edit(ChangeNumber, null, EditOptions.None, FileSpecs);
}
/// <summary>
/// Opens files for edit
/// </summary>
/// <param name="ChangeNumber">Changelist to add files to</param>
/// <param name="FileType">Type for new files</param>
/// <param name="Options">Options for the command</param>
/// <param name="FileSpecs">Files to be opened for edit</param>
/// <returns>Response from the server</returns>
public PerforceResponseList<EditRecord> 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<EditRecord>(Arguments.ToString(), null);
}
#endregion
#region p4 filelog
/// <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 responses from the server</returns>
public PerforceResponseList<FileLogRecord> FileLog(FileLogOptions 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 responses from the server</returns>
public PerforceResponseList<FileLogRecord> FileLog(int MaxChanges, FileLogOptions 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 responses from the server</returns>
public PerforceResponseList<FileLogRecord> 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<FileLogRecord>(Arguments.ToString(), null);
}
#endregion
#region p4 fstat
/// <summary>
/// Execute the 'fstat' command
/// </summary>
/// <param name="Options">Options for the command</param>
/// <param name="FileSpecs">List of file specifications to query</param>
/// <returns>List of responses from the server</returns>
public PerforceResponseList<FStatRecord> FStat(FStatOptions Options, params string[] FileSpecs)
{
return FStat(-1, Options, FileSpecs);
}
/// <summary>
/// Execute the 'fstat' command
/// </summary>
/// <param name="MaxFiles">Produce fstat output for only the first max files.</param>
/// <param name="Options">Options for the command</param>
/// <param name="FileSpecs">List of file specifications to query</param>
/// <returns>List of responses from the server</returns>
public PerforceResponseList<FStatRecord> FStat(int MaxFiles, FStatOptions Options, params string[] FileSpecs)
{
return FStat(-1, -1, null, MaxFiles, Options, FileSpecs);
}
/// <summary>
/// Execute the 'fstat' command
/// </summary>
/// <param name="AfterChangeNumber">Return only files affected after the given changelist number.</param>
/// <param name="OnlyChangeNumber">Return only files affected by the given changelist number.</param>
/// <param name="Filter">List only those files that match the criteria specified.</param>
/// <param name="MaxFiles">Produce fstat output for only the first max files.</param>
/// <param name="Options">Options for the command</param>
/// <param name="FileSpecs">List of file specifications to query</param>
/// <returns>List of responses from the server</returns>
public PerforceResponseList<FStatRecord> 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<FStatRecord>(Arguments.ToString(), null);
}
#endregion
#region p4 info
/// <summary>
/// Execute the 'info' command
/// </summary>
/// <param name="Options">Options for the command</param>
/// <returns>Response from the server; an InfoRecord or error code</returns>
public PerforceResponse<InfoRecord> Info(InfoOptions Options)
{
// Build the argument list
StringBuilder Arguments = new StringBuilder("info");
if((Options & InfoOptions.ShortOutput) != 0)
{
Arguments.Append(" -s");
}
return SingleResponseCommand<InfoRecord>(Arguments.ToString(), null);
}
#endregion
#region p4 opened
/// <summary>
/// Execute the 'opened' command
/// </summary>
/// <param name="Options">Options for the command</param>
/// <param name="ChangeNumber">List the files in pending changelist change. To list files in the default changelist, use DefaultChange.</param>
/// <param name="ClientName">List only files that are open in the given client</param>
/// <param name="UserName">List only files that are opened by the given user</param>
/// <param name="MaxResults">Maximum number of results to return</param>
/// <param name="FileSpecs">Specification for the files to list</param>
/// <returns>Response from the server</returns>
public PerforceResponseList<FStatRecord> 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<FStatRecord>(Arguments.ToString(), null);
}
#endregion
#region p4 print
/// <summary>
/// Execute the 'print' command
/// </summary>
/// <param name="OutputFile">Output file to redirect output to</param>
/// <param name="FileSpec">Specification for the files to print</param>
/// <returns>Response from the server</returns>
public PerforceResponse<PrintRecord> 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<PrintRecord>(Arguments.ToString(), null);
}
class PrintHandler : IDisposable
{
Dictionary<string, string> DepotFileToLocalFile;
FileStream OutputStream;
public PrintHandler(Dictionary<string, string> 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<KeyValuePair<string, object>> 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");
}
}
}
/// <summary>
/// Execute the 'print' command
/// </summary>
/// <param name="OutputFile">Output file to redirect output to</param>
/// <param name="FileSpec">Specification for the files to print</param>
/// <returns>Response from the server</returns>
public void Print(Dictionary<string, string> 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
/// <summary>
/// Open files for add, delete, and/or edit in order to reconcile a workspace with changes made outside of Perforce.
/// </summary>
/// <param name="ChangeNumber">Changelist to open files to</param>
/// <param name="Options">Options for the command</param>
/// <param name="FileSpecs">Files to be reverted</param>
/// <returns>Response from the server</returns>
public PerforceResponseList<ReconcileRecord> 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<ReconcileRecord>(Arguments.ToString(), null);
}
#endregion
#region p4 resolve
/// <summary>
/// Resolve conflicts between file revisions.
/// </summary>
/// <param name="ChangeNumber">Changelist to open files to</param>
/// <param name="Options">Options for the command</param>
/// <param name="FileSpecs">Files to be reverted</param>
/// <returns>Response from the server</returns>
public PerforceResponseList<ResolveRecord> 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<ResolveRecord>(Arguments.ToString(), null);
}
#endregion
#region p4 revert
/// <summary>
/// Reverts files that have been added to a pending changelist.
/// </summary>
/// <param name="ChangeNumber">Changelist to add files to</param>
/// <param name="ClientName">Revert another user’s open files. </param>
/// <param name="Options">Options for the command</param>
/// <param name="FileSpecs">Files to be reverted</param>
/// <returns>Response from the server</returns>
public PerforceResponseList<RevertRecord> 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<RevertRecord>(Arguments.ToString(), null);
}
#endregion
#region p4 shelve
/// <summary>
/// Shelves a set of files
/// </summary>
/// <param name="ChangeNumber">The change number to receive the shelved files</param>
/// <param name="Options">Options for the command</param>
/// <param name="FileSpecs">Files to sync</param>
/// <returns>Response from the server</returns>
public PerforceResponseList<ShelveRecord> 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<ShelveRecord>(Arguments.ToString(), null);
}
/// <summary>
/// Deletes files from a shelved changelist
/// </summary>
/// <param name="ChangeNumber">Changelist containing shelved files to be deleted</param>
/// <param name="FileSpecs">Files to delete</param>
/// <returns>Response from the server</returns>
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
/// <summary>
/// Enumerates all streams in a depot
/// </summary>
/// <param name="StreamPath">The path for streams to enumerate (eg. "//UE4/...")</param>
/// <returns>List of streams matching the given criteria</returns>
public PerforceResponseList<StreamRecord> 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. "//UE4/...")</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 PerforceResponseList<StreamRecord> 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<StreamRecord>(Arguments.ToString(), null);
}
#endregion
#region p4 submit
/// <summary>
/// Submits a pending changelist
/// </summary>
/// <param name="ChangeNumber">The changelist to submit</param>
/// <param name="Options">Options for the command</param>
/// <returns>Response from the server</returns>
public PerforceResponse<SubmitRecord> 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<SubmitRecord>(Arguments.ToString(), null)[0];
}
#endregion
#region p4 sync
/// <summary>
/// Syncs files from the server
/// </summary>
/// <param name="FileSpecs">Files to sync</param>
/// <returns>Response from the server</returns>
public PerforceResponseList<SyncRecord> Sync(params string[] FileSpecs)
{
return Sync(SyncOptions.None, -1, FileSpecs);
}
/// <summary>
/// Syncs files from the server
/// </summary>
/// <param name="Options">Options for the command</param>
/// <param name="MaxFiles">Syncs only the first number of files specified.</param>
/// <param name="FileSpecs">Files to sync</param>
/// <returns>Response from the server</returns>
public PerforceResponseList<SyncRecord> Sync(SyncOptions Options, int MaxFiles, params string[] FileSpecs)
{
return Sync(Options, -1, -1, -1, -1, -1, -1, FileSpecs);
}
/// <summary>
/// Syncs files from the server
/// </summary>
/// <param name="Options">Options for the command</param>
/// <param name="MaxFiles">Syncs only the first number of files specified</param>
/// <param name="NumThreads">Sync in parallel using the given number of threads</param>
/// <param name="Batch">The number of files in a batch</param>
/// <param name="BatchSize">The number of bytes in a batch</param>
/// <param name="Min">Minimum number of files in a parallel sync</param>
/// <param name="MinSize">Minimum number of bytes in a parallel sync</param>
/// <param name="FileSpecs">Files to sync</param>
/// <returns>Response from the server</returns>
public PerforceResponseList<SyncRecord> 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<SyncRecord>(Arguments.ToString(), null);
}
/// <summary>
/// Syncs files from the server
/// </summary>
/// <param name="Options">Options for the command</param>
/// <param name="MaxFiles">Syncs only the first number of files specified.</param>
/// <param name="FileSpecs">Files to sync</param>
/// <returns>Response from the server</returns>
public PerforceResponseList<SyncSummaryRecord> SyncQuiet(SyncOptions Options, int MaxFiles, params string[] FileSpecs)
{
return SyncQuiet(Options, -1, -1, -1, -1, -1, -1, FileSpecs);
}
/// <summary>
/// Syncs files from the server without returning detailed file info
/// </summary>
/// <param name="Options">Options for the command</param>
/// <param name="MaxFiles">Syncs only the first number of files specified</param>
/// <param name="NumThreads">Sync in parallel using the given number of threads</param>
/// <param name="Batch">The number of files in a batch</param>
/// <param name="BatchSize">The number of bytes in a batch</param>
/// <param name="Min">Minimum number of files in a parallel sync</param>
/// <param name="MinSize">Minimum number of bytes in a parallel sync</param>
/// <param name="FileSpecs">Files to sync</param>
/// <returns>Response from the server</returns>
public PerforceResponseList<SyncSummaryRecord> 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<SyncSummaryRecord>(Arguments.ToString(), null);
}
/// <summary>
/// Gets arguments for a sync command
/// </summary>
/// <param name="Options">Options for the command</param>
/// <param name="MaxFiles">Syncs only the first number of files specified</param>
/// <param name="NumThreads">Sync in parallel using the given number of threads</param>
/// <param name="Batch">The number of files in a batch</param>
/// <param name="BatchSize">The number of bytes in a batch</param>
/// <param name="Min">Minimum number of files in a parallel sync</param>
/// <param name="MinSize">Minimum number of bytes in a parallel sync</param>
/// <param name="FileSpecs">Files to sync</param>
/// <param name="bQuiet">Whether to use quiet output</param>
/// <returns>Arguments for the command</returns>
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
/// <summary>
/// Restore shelved files from a pending change into a workspace
/// </summary>
/// <param name="ChangeNumber">The changelist containing shelved files</param>
/// <param name="IntoChangeNumber">The changelist to receive the unshelved files</param>
/// <param name="UsingBranchSpec">The branchspec to use when unshelving files</param>
/// <param name="UsingStream">Specifies the use of a stream-derived branch view to map the shelved files between the specified stream and its parent stream.</param>
/// <param name="ForceParentStream">Unshelve to the specified parent stream. Overrides the parent defined in the source stream specification.</param>
/// <param name="Options">Options for the command</param>
/// <param name="FileSpecs">Files to unshelve</param>
/// <returns>Response from the server</returns>
public PerforceResponseList<UnshelveRecord> 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<UnshelveRecord>(Arguments.ToString(), null);
}
#endregion
#region p4 user
/// <summary>
/// Enumerates all streams in a depot
/// </summary>
/// <param name="UserName">Name of the user to fetch information for</param>
/// <returns>Response from the server</returns>
public PerforceResponse<UserRecord> User(string UserName)
{
StringBuilder Arguments = new StringBuilder("user");
Arguments.AppendFormat(" -o \"{0}\"", UserName);
return SingleResponseCommand<UserRecord>(Arguments.ToString(), null);
}
#endregion
#region p4 where
/// <summary>
/// Retrieves the location of a file of set of files in the workspace
/// </summary>
/// <param name="FileSpecs">Patterns for the files to query</param>
/// <returns>List of responses from the server</returns>
public PerforceResponseList<WhereRecord> Where(params string[] FileSpecs)
{
StringBuilder Arguments = new StringBuilder("where");
foreach(string FileSpec in FileSpecs)
{
Arguments.AppendFormat(" \"{0}\"", FileSpec);
}
return Command<WhereRecord>(Arguments.ToString(), null);
}
#endregion
}
}