// 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 } }