// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace EpicGames.Perforce { /// /// Stores settings for communicating with a Perforce server. /// public sealed class PerforceConnection : IPerforceConnection { /// /// Create a new Perforce connection /// /// /// public static Task CreateAsync(ILogger logger) { return CreateAsync(PerforceSettings.Default, logger); } /// /// Create a new Perforce connection /// /// The server address and port /// The user name /// Interface for logging /// public static Task CreateAsync(string? serverAndPort, string? userName, ILogger logger) { return CreateAsync(CombineSettings(serverAndPort, userName), logger); } /// /// Create a new Perforce connection /// /// The server address and port /// The user name /// The client name /// Interface for logging /// public static Task CreateAsync(string? serverAndPort, string? userName, string? clientName, ILogger logger) { return CreateAsync(CombineSettings(serverAndPort, userName, clientName), logger); } /// /// Create a new Perforce connection /// /// The server address and port /// The user name /// The client name /// /// /// Interface for logging /// public static Task CreateAsync(string? serverAndPort, string? userName, string? clientName, string? appName, string? appVersion, ILogger logger) { return CreateAsync(CombineSettings(serverAndPort, userName, clientName, appName, appVersion), logger); } /// /// Create a new Perforce connection /// /// Settings for the connection /// /// public static async Task CreateAsync(IPerforceSettings settings, ILogger logger) { if (settings.PreferNativeClient && NativePerforceConnection.IsSupported()) { return await NativePerforceConnection.CreateAsync(settings, logger); } else { return new PerforceConnection(settings, logger); } } static PerforceSettings CombineSettings(string? serverAndPort, string? userName, string? clientName = null, string? appName = null, string? appVersion = null) { PerforceSettings settings = new PerforceSettings(PerforceSettings.Default); if (serverAndPort != null) { settings.ServerAndPort = serverAndPort; } if (userName != null) { settings.UserName = userName; } if (clientName != null) { settings.ClientName = clientName; } if (appName != null) { settings.AppName = appName; } if (appVersion != null) { settings.AppVersion = appVersion; } return settings; } #region Legacy implementation /// public IPerforceSettings Settings { get { PerforceSettings settings = new PerforceSettings(PerforceEnvironment.Default); if (ServerAndPort != null) { settings.ServerAndPort = ServerAndPort; } if (UserName != null) { settings.UserName = UserName; } if (ClientName != null) { settings.ClientName = ClientName; } if (AppName != null) { settings.AppName = AppName; } if (AppVersion != null) { settings.AppVersion = AppVersion; } return settings; } } /// /// The current server and port /// public string? ServerAndPort { get; set; } /// /// The current user name /// public string? UserName { get; set; } /// /// The current host name /// public string? HostName { get; set; } /// /// The current client name /// public string? ClientName { get; set; } /// /// Name of this application, reported to server through -zprog arguments /// public string? AppName { get; set; } /// /// Version of this application, reported to server through -zversion arguments /// public string? AppVersion { get; set; } /// /// Additional options to append to the command line /// public List GlobalOptions { get; } = new List(); /// /// The logging interface /// public ILogger Logger { get; set; } /// /// Constructor /// /// The server address and port /// The user name /// Interface for logging public PerforceConnection(string? serverAndPort, string? userName, ILogger logger) : this(serverAndPort, userName, null, logger) { } /// /// Constructor /// /// The server address and port /// The user name /// The client name /// Interface for logging public PerforceConnection(string? serverAndPort, string? userName, string? clientName, ILogger logger) : this(serverAndPort, userName, clientName, null, null, logger) { AssemblyName entryAssemblyName = Assembly.GetEntryAssembly()!.GetName(); if (entryAssemblyName.Name != null) { AppName = entryAssemblyName.Name; AppVersion = entryAssemblyName.Version?.ToString() ?? String.Empty; } } /// /// Constructor /// /// The server address and port /// The user name /// The client name /// /// /// Interface for logging public PerforceConnection(string? serverAndPort, string? userName, string? clientName, string? appName, string? appVersion, ILogger logger) { ServerAndPort = serverAndPort; UserName = userName; ClientName = clientName; AppName = appName; AppVersion = appVersion; Logger = logger; } /// /// Constructor /// /// /// public PerforceConnection(IPerforceSettings settings, ILogger logger) : this(settings.ServerAndPort, settings.UserName, settings.ClientName, settings.AppName, settings.AppVersion, logger) { HostName = settings.HostName; } /// /// Constructor /// /// Connection to copy settings from public PerforceConnection(PerforceConnection other) : this(other.ServerAndPort, other.UserName, other.ClientName, other.AppName, other.AppVersion, other.Logger) { GlobalOptions.AddRange(other.GlobalOptions); } /// public void Dispose() { } List GetGlobalArguments() { List arguments = new List(); if (ServerAndPort != null) { arguments.Add($"-p{ServerAndPort}"); } if (UserName != null) { arguments.Add($"-u{UserName}"); } if (HostName != null) { arguments.Add($"-H{HostName}"); } if (ClientName != null) { arguments.Add($"-c{ClientName}"); } if (AppName != null) { arguments.Add($"-zprog={AppName}"); } if (AppVersion != null) { arguments.Add($"-zversion={AppVersion}"); } arguments.AddRange(GlobalOptions); return arguments; } /// public IPerforceOutput Command(string command, IReadOnlyList arguments, IReadOnlyList? fileArguments, byte[]? inputData, string? promptResponse, bool interceptIo) { if (promptResponse != null) { inputData = Encoding.UTF8.GetBytes(promptResponse); } if (interceptIo) { throw new NotSupportedException($"{nameof(interceptIo)} option is not supported through legacy Perforce client"); } return new PerforceChildProcess(command, arguments, fileArguments, inputData, GetGlobalArguments(), Logger); } /// /// Sets an environment variable /// /// Name of the variable to set /// Value for the variable /// Token used to cancel the operation /// Response from the server public async Task SetAsync(string name, string value, CancellationToken cancellationToken = default) { Tuple result = await TrySetAsync(name, value, cancellationToken); if (!result.Item1) { throw new PerforceException(result.Item2); } } /// /// Sets an environment variable /// /// Name of the variable to set /// Value for the variable /// Token used to cancel the operation /// Response from the server public async Task> TrySetAsync(string name, string value, CancellationToken cancellationToken = default) { List arguments = new List(); arguments.Add($"{name}={value}"); using (PerforceChildProcess childProcess = new PerforceChildProcess("set", arguments, null, null, GetGlobalArguments(), Logger)) { return await childProcess.TryReadToEndAsync(cancellationToken); } } /// public PerforceRecord CreateRecord(List> fields) => PerforceRecord.FromFields(fields, true); #endregion } /// /// Extension methods for /// public static class PerforceConnectionExtensions { /// /// Create a new connection with a different client /// /// /// /// public static Task WithClientAsync(this IPerforceConnection perforce, string? clientName) { PerforceSettings settings = new PerforceSettings(perforce.Settings) { ClientName = clientName }; return PerforceConnection.CreateAsync(settings, perforce.Logger); } /// /// Create a new connection with a different client /// /// /// public static Task WithoutClientAsync(this IPerforceConnection perforce) { return WithClientAsync(perforce, null); } #region Command wrappers /// /// Execute a command and parse the response /// /// The Perforce connection /// Command to execute /// Arguments for the command /// Input data to pass to Perforce /// The type of records to return for "stat" responses /// Token used to cancel the operation /// List of objects returned by the server public static Task> CommandAsync(this IPerforceConnection perforce, string command, IReadOnlyList arguments, byte[]? inputData, Type? statRecordType, CancellationToken cancellationToken = default) { return CommandAsync(perforce, command, arguments, null, inputData, statRecordType, cancellationToken); } /// /// Execute a command and parse the response /// /// The Perforce connection /// Command to execute /// Arguments for the command /// File arguments for the command /// Input data to pass to Perforce /// The type of records to return for "stat" responses /// Token used to cancel the operation /// List of objects returned by the server public static async Task> CommandAsync(this IPerforceConnection perforce, string command, IReadOnlyList arguments, IReadOnlyList? fileArguments, byte[]? inputData, Type? statRecordType, CancellationToken cancellationToken = default) { await using (IPerforceOutput response = perforce.Command(command, arguments, fileArguments, inputData, null, false)) { return await response.ReadResponsesAsync(statRecordType, cancellationToken); } } /// /// Execute a command and parse the response /// /// The Perforce connection /// Command to execute /// Arguments for the command /// File arguments for the command /// Input data to pass to Perforce /// The type of records to return for "stat" responses /// Whether to intercept Io operations and return them in the response output /// Token used to cancel the operation /// List of objects returned by the server public static async IAsyncEnumerable StreamCommandAsync(this IPerforceConnection perforce, string command, IReadOnlyList arguments, IReadOnlyList? fileArguments, byte[]? inputData, Type? statRecordType, bool interceptIo, [EnumeratorCancellation] CancellationToken cancellationToken) { #pragma warning disable CA1849 // Call async methods when in an async method await using (IPerforceOutput output = perforce.Command(command, arguments, fileArguments, inputData, null, interceptIo)) { await foreach (PerforceResponse response in output.ReadStreamingResponsesAsync(statRecordType, cancellationToken)) { yield return response; } } #pragma warning restore CA1849 // Call async methods when in an async method } /// /// Execute a command and parse the response /// /// The Perforce connection /// Command to execute /// Arguments for the command /// File arguments for the command /// Input data to pass to Perforce /// Token used to cancel the operation /// List of objects returned by the server public static async IAsyncEnumerable> StreamCommandAsync(this IPerforceConnection perforce, string command, IReadOnlyList arguments, IReadOnlyList? fileArguments = null, byte[]? inputData = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) where T : class { #pragma warning disable CA1849 // Call async methods when in an async method await using (IPerforceOutput output = perforce.Command(command, arguments, fileArguments, inputData, null, false)) { Type statRecordType = typeof(T); await foreach (PerforceResponse response in output.ReadStreamingResponsesAsync(statRecordType, cancellationToken)) { yield return new PerforceResponse(response); } } #pragma warning restore CA1849 // Call async methods when in an async method } /// /// Execute a command and parse the response /// /// The Perforce connection /// Command to execute /// Arguments for the command /// Input data to pass to Perforce /// Delegate used to handle each record /// Token used to cancel the operation /// List of objects returned by the server public static async Task RecordCommandAsync(this IPerforceConnection perforce, string command, IReadOnlyList arguments, byte[]? inputData, Action handleRecord, CancellationToken cancellationToken = default) { #pragma warning disable CA1849 // Call async methods when in an async method await using (IPerforceOutput response = perforce.Command(command, arguments, null, inputData, null, false)) { await response.ReadRecordsAsync(handleRecord, cancellationToken); } #pragma warning restore CA1849 // Call async methods when in an async method } /// /// Execute a command and parse the response /// /// Connection to the Perforce server /// The command to execute /// Arguments for the command /// Arguments which can be passed into a -x argument /// Input data to pass to Perforce /// Token used to cancel the operation /// List of objects returned by the server public static async Task> CommandAsync(this IPerforceConnection connection, string command, IReadOnlyList arguments, IReadOnlyList? fileArguments, byte[]? inputData, CancellationToken cancellationToken = default) where T : class { List responses = await connection.CommandAsync(command, arguments, fileArguments, inputData, typeof(T), cancellationToken); PerforceResponseList typedResponses = new PerforceResponseList(); foreach (PerforceResponse response in responses) { typedResponses.Add(new PerforceResponse(response)); } return typedResponses; } /// /// Execute a command and parse the response /// /// Connection to the Perforce server /// The command to execute /// Arguments for the command /// Input data to pass to Perforce /// Token used to cancel the operation /// List of objects returned by the server public static Task> CommandAsync(this IPerforceConnection connection, string command, List arguments, byte[]? inputData, CancellationToken cancellationToken = default) where T : class { return CommandAsync(connection, command, arguments, null, inputData, cancellationToken); } /// /// Execute a command and parse the response /// /// Connection to the Perforce server /// The command to execute /// Arguments for the command /// Arguments to pass to the command in batches /// Token used to cancel the operation /// List of objects returned by the server public static async Task> BatchedCommandAsync(IPerforceConnection connection, string command, IReadOnlyList commonArguments, IReadOnlyList batchedArguments, CancellationToken cancellationToken = default) where T : class { PerforceResponseList responses = new PerforceResponseList(); for (int fileSpecIdx = 0; fileSpecIdx < batchedArguments.Count;) { List arguments = new List(); arguments.AddRange(commonArguments); const int PerArgumentExtra = 5; int length = (command.Length + PerArgumentExtra) + arguments.Sum(x => x.Length + PerArgumentExtra); for (; fileSpecIdx < batchedArguments.Count && length < 4096; fileSpecIdx++) { arguments.Add(batchedArguments[fileSpecIdx]); length += batchedArguments[fileSpecIdx].Length + PerArgumentExtra; } responses.AddRange(await CommandAsync(connection, command, arguments, null, cancellationToken)); } return responses; } /// /// Attempts to execute the given command, returning the results from the server or the first PerforceResponse object. /// /// Connection to the Perforce server /// The command to execute /// Arguments for the command. /// Input data for the command. /// Type of element to return in the response /// Token used to cancel the operation /// Response from the server; either an object of type T or error. static async Task SingleResponseCommandAsync(IPerforceConnection connection, string command, IReadOnlyList arguments, byte[]? inputData, Type? statRecordType, CancellationToken cancellationToken = default) { List responses = await connection.CommandAsync(command, arguments, inputData, statRecordType, cancellationToken); if (responses.Count != 1) { for (int idx = 0; idx < responses.Count; idx++) { connection.Logger.LogInformation("Unexpected response {Idx}: {Text}", idx, responses[idx].ToString()); } throw new PerforceException("Expected one result from 'p4 {0}', got {1}", command, 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 /// Connection to the Perforce server /// The command to execute /// Arguments for the command. /// Input data for the command. /// Token used to cancel the operation /// Response from the server; either an object of type T or error. public static async Task> SingleResponseCommandAsync(IPerforceConnection connection, string command, List arguments, byte[]? inputData, CancellationToken cancellationToken = default) where T : class { return new PerforceResponse(await SingleResponseCommandAsync(connection, command, arguments, inputData, typeof(T), cancellationToken)); } #endregion #region p4 add /// /// Adds files to a pending changelist. /// /// Connection to the Perforce server /// Changelist to add files to /// Files to be added /// Token used to cancel the operation /// Response from the server public static async Task> AddAsync(this IPerforceConnection connection, int changeNumber, FileSpecList fileSpecList, CancellationToken cancellationToken = default) { return (await TryAddAsync(connection, changeNumber, null, AddOptions.None, fileSpecList, cancellationToken)).Data; } /// /// Adds files to a pending changelist. /// /// Connection to the Perforce server /// Changelist to add files to /// Type for new files /// Options for the command /// Files to be added /// Token used to cancel the operation /// Response from the server public static async Task> AddAsync(this IPerforceConnection connection, int changeNumber, string? fileType, AddOptions options, FileSpecList fileSpecList, CancellationToken cancellationToken = default) { return (await TryAddAsync(connection, changeNumber, fileType, options, fileSpecList, cancellationToken)).Data; } /// /// Adds files to a pending changelist. /// /// Connection to the Perforce server /// Changelist to add files to /// Files to be added /// Token used to cancel the operation /// Response from the server public static Task> TryAddAsync(this IPerforceConnection connection, int changeNumber, FileSpecList fileSpecList, CancellationToken cancellationToken = default) { return TryAddAsync(connection, changeNumber, null, AddOptions.None, fileSpecList, cancellationToken); } /// /// Adds files to a pending changelist. /// /// Connection to the Perforce server /// Changelist to add files to /// Type for new files /// Options for the command /// Files to be added /// Token used to cancel the operation /// Response from the server public static Task> TryAddAsync(this IPerforceConnection connection, int changeNumber, string? fileType, AddOptions options, FileSpecList fileNames, CancellationToken cancellationToken = default) { List arguments = new List(); if (changeNumber != -1) { arguments.Add($"-c{changeNumber}"); } if ((options & AddOptions.DowngradeToAdd) != 0) { arguments.Add("-d"); } if ((options & AddOptions.IncludeWildcards) != 0) { arguments.Add("-f"); } if ((options & AddOptions.NoIgnore) != 0) { arguments.Add("-I"); } if ((options & AddOptions.PreviewOnly) != 0) { arguments.Add("-n"); } if (fileType != null) { arguments.Add($"-t{fileType}"); } return BatchedCommandAsync(connection, "add", arguments, fileNames.List, cancellationToken); } #endregion #region p4 change /// /// Creates a changelist with the p4 change command. /// /// Connection to the Perforce server /// Information for the change to create. The number field should be left set to -1. /// Token used to cancel the operation /// The changelist number, or an error. public static async Task CreateChangeAsync(this IPerforceConnection connection, ChangeRecord record, CancellationToken cancellationToken = default) { return (await TryCreateChangeAsync(connection, record, cancellationToken)).Data; } /// /// Creates a changelist with the p4 change command. /// /// Connection to the Perforce server /// Information for the change to create. The number field should be left set to -1. /// Token used to cancel the operation /// The changelist number, or an error. public static async Task> TryCreateChangeAsync(this IPerforceConnection connection, ChangeRecord record, CancellationToken cancellationToken = default) { if (record.Number != -1) { throw new PerforceException("'Number' field should be set to -1 to create a new changelist."); } PerforceResponse response = await SingleResponseCommandAsync(connection, "change", new List { "-i" }, SerializeRecord(connection, record), null, cancellationToken); PerforceError? error = response.Error; if (error != null) { return new PerforceResponse(error); } PerforceInfo? info = response.Info; if (info == null) { throw new PerforceException("Unexpected info response from change command: {0}", response); } Match match = Regex.Match(info.Data, @"^Change (\d+) created"); if (!match.Success) { throw new PerforceException("Unexpected info response from change command: {0}", response); } record.Number = Int32.Parse(match.Groups[1].Value); return new PerforceResponse(record); } /// /// Updates an existing changelist. /// /// Connection to the Perforce server /// Options for this command /// Information for the change to create. The number field should be left set to zero. /// Token used to cancel the operation /// The changelist number, or an error. public static async Task UpdateChangeAsync(this IPerforceConnection connection, UpdateChangeOptions options, ChangeRecord record, CancellationToken cancellationToken = default) { (await TryUpdateChangeAsync(connection, options, record, cancellationToken)).EnsureSuccess(); } /// /// Updates an existing changelist. /// /// Connection to the Perforce server /// Options for this command /// Information for the change to create. The number field should be left set to zero. /// Token used to cancel the operation /// The changelist number, or an error. public static Task TryUpdateChangeAsync(this IPerforceConnection connection, UpdateChangeOptions options, ChangeRecord record, CancellationToken cancellationToken = default) { if (record.Number == -1) { throw new PerforceException("'Number' field must be set to update a changelist."); } List arguments = new List(); arguments.Add("-i"); if ((options & UpdateChangeOptions.Force) != 0) { arguments.Add("-f"); } if ((options & UpdateChangeOptions.Submitted) != 0) { arguments.Add("-u"); } return SingleResponseCommandAsync(connection, "change", arguments, connection.SerializeRecord(record), null, cancellationToken); } /// /// Deletes a changelist (p4 change -d) /// /// Connection to the Perforce server /// Options for the command /// Changelist number to delete /// Token used to cancel the operation /// Response from the server public static async Task DeleteChangeAsync(this IPerforceConnection connection, DeleteChangeOptions options, int changeNumber, CancellationToken cancellationToken = default) { (await TryDeleteChangeAsync(connection, options, changeNumber, cancellationToken)).EnsureSuccess(); } /// /// Deletes a changelist (p4 change -d) /// /// Connection to the Perforce server /// Options for the command /// Changelist number to delete /// Token used to cancel the operation /// Response from the server public static Task TryDeleteChangeAsync(this IPerforceConnection connection, DeleteChangeOptions options, int changeNumber, CancellationToken cancellationToken = default) { List arguments = new List { "-d" }; if ((options & DeleteChangeOptions.Submitted) != 0) { arguments.Add("-f"); } if ((options & DeleteChangeOptions.BeforeRenumber) != 0) { arguments.Add("-O"); } arguments.Add($"{changeNumber}"); return SingleResponseCommandAsync(connection, "change", arguments, null, null, cancellationToken); } /// /// Gets a changelist /// /// Connection to the Perforce server /// Options for the command /// Changelist number to retrieve. -1 is the default changelist for this workspace. /// Token used to cancel the operation /// Response from the server public static async Task GetChangeAsync(this IPerforceConnection connection, GetChangeOptions options, int changeNumber, CancellationToken cancellationToken = default) { return (await TryGetChange(connection, options, changeNumber, cancellationToken)).Data; } /// /// Gets a changelist /// /// Connection to the Perforce server /// Options for the command /// Changelist number to retrieve. -1 is the default changelist for this workspace. /// Token used to cancel the operation /// Response from the server public static Task> TryGetChange(this IPerforceConnection connection, GetChangeOptions options, int changeNumber, CancellationToken cancellationToken = default) { List arguments = new List { "-o" }; if ((options & GetChangeOptions.BeforeRenumber) != 0) { arguments.Add("-O"); } if (changeNumber != -1) { arguments.Add($"{changeNumber}"); } return SingleResponseCommandAsync(connection, "change", arguments, null, cancellationToken); } /// /// Serializes a change record to a byte array /// /// Connection to the Perforce server /// The record to serialize /// Serialized record static byte[] SerializeRecord(this IPerforceConnection connection, 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)); } if (input.Status != ChangeStatus.All) { nameToValue.Add(new KeyValuePair("Status", PerforceReflection.GetEnumText(typeof(ChangeStatus), input.Status))); } if (input.Files.Count > 0) { nameToValue.Add(new KeyValuePair("Files", input.Files)); } return connection.CreateRecord(nameToValue).Serialize(); } #endregion #region p4 changes /// /// Enumerates changes on the server /// /// Connection to the Perforce 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 /// Token used to cancel the operation /// List of responses from the server. public static async Task> GetChangesAsync(this IPerforceConnection connection, ChangesOptions options, int maxChanges, ChangeStatus status, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return (await TryGetChangesAsync(connection, options, maxChanges, status, fileSpecs, cancellationToken)).Data; } /// /// Enumerates changes on the server /// /// Connection to the Perforce 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 /// Token used to cancel the operation /// List of responses from the server. public static async Task> GetChangesAsync(this IPerforceConnection connection, ChangesOptions options, string? clientName, int maxChanges, ChangeStatus status, string? userName, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return (await TryGetChangesAsync(connection, options, clientName, maxChanges, status, userName, fileSpecs, cancellationToken)).Data; } /// /// Enumerates changes on the server /// /// Connection to the Perforce server /// Options for the command /// List only changes made from the named client workspace. /// The minimum changelist number /// 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 /// Token used to cancel the operation /// List of responses from the server. public static async Task> GetChangesAsync(this IPerforceConnection connection, ChangesOptions options, string? clientName, int minChangeNumber, int maxChanges, ChangeStatus status, string? userName, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return (await TryGetChangesAsync(connection, options, clientName, minChangeNumber, maxChanges, status, userName, fileSpecs, cancellationToken)).Data; } /// /// Enumerates changes on the server /// /// Connection to the Perforce 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 /// Token used to cancel the operation /// List of responses from the server. public static Task> TryGetChangesAsync(this IPerforceConnection connection, ChangesOptions options, int maxChanges, ChangeStatus status, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TryGetChangesAsync(connection, options, null, maxChanges, status, null, fileSpecs, cancellationToken); } /// /// Enumerates changes on the server /// /// Connection to the Perforce 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 /// Token used to cancel the operation /// List of responses from the server. public static Task> TryGetChangesAsync(this IPerforceConnection connection, ChangesOptions options, string? clientName, int maxChanges, ChangeStatus status, string? userName, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TryGetChangesAsync(connection, options, clientName, -1, maxChanges, status, userName, fileSpecs, cancellationToken); } /// /// Enumerates changes on the server /// /// Connection to the Perforce server /// Options for the command /// List only changes made from the named client workspace. /// The minimum changelist number /// 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 /// Token used to cancel the operation /// List of responses from the server. public static Task> TryGetChangesAsync(this IPerforceConnection connection, ChangesOptions options, string? clientName, int minChangeNumber, int maxChanges, ChangeStatus status, string? userName, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { List arguments = new List(); if ((options & ChangesOptions.IncludeIntegrations) != 0) { arguments.Add("-i"); } if ((options & ChangesOptions.IncludeTimes) != 0) { arguments.Add("-t"); } if ((options & ChangesOptions.LongOutput) != 0) { arguments.Add("-l"); } if ((options & ChangesOptions.Reverse) != 0) { arguments.Add("-r"); } if ((options & ChangesOptions.TruncatedLongOutput) != 0) { arguments.Add("-L"); } if ((options & ChangesOptions.IncludeRestricted) != 0) { arguments.Add("-f"); } if (clientName != null) { arguments.Add($"-c{clientName}"); } if (minChangeNumber != -1) { arguments.Add($"-e{minChangeNumber}"); } if (maxChanges != -1) { arguments.Add($"-m{maxChanges}"); } if (status != ChangeStatus.All) { arguments.Add($"-s{PerforceReflection.GetEnumText(typeof(ChangeStatus), status)}"); } if (userName != null) { arguments.Add($"-u{userName}"); } if (fileSpecs != FileSpecList.Any) // Queries are slower with a path specification { arguments.AddRange(fileSpecs.List); } return CommandAsync(connection, "changes", arguments, null, cancellationToken); } #endregion #region p4 clean /// /// Cleans the workspace /// /// Connection to the Perforce server /// Options for the command /// Files to sync /// Token used to cancel the operation /// Response from the server public static async Task> CleanAsync(this IPerforceConnection connection, CleanOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return (await TryCleanAsync(connection, options, fileSpecs, cancellationToken)).Data; } /// /// Cleans the workspace /// /// Connection to the Perforce server /// Options for the command /// Files to sync /// Token used to cancel the operation /// Response from the server public static async Task> TryCleanAsync(this IPerforceConnection connection, CleanOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { List arguments = new List(); if ((options & CleanOptions.Edited) != 0) { arguments.Add("-e"); } if ((options & CleanOptions.Added) != 0) { arguments.Add("-a"); } if ((options & CleanOptions.Deleted) != 0) { arguments.Add("-d"); } if ((options & CleanOptions.Preview) != 0) { arguments.Add("-n"); } if ((options & CleanOptions.NoIgnoreChecking) != 0) { arguments.Add("-I"); } if ((options & CleanOptions.LocalSyntax) != 0) { arguments.Add("-l"); } if ((options & CleanOptions.ModifiedTimes) != 0) { arguments.Add("-m"); } arguments.AddRange(fileSpecs.List); PerforceResponseList records = await CommandAsync(connection, "clean", arguments, null, cancellationToken); records.RemoveAll(x => x.Error != null && x.Error.Generic == PerforceGenericCode.Empty); return records; } #endregion #region p4 client /// /// Creates a client /// /// Connection to the Perforce server /// The client record /// Token used to cancel the operation /// Response from the server public static async Task CreateClientAsync(this IPerforceConnection connection, ClientRecord record, CancellationToken cancellationToken = default) { (await TryCreateClientAsync(connection, record, cancellationToken)).EnsureSuccess(); } /// /// Creates a client /// /// Connection to the Perforce server /// The client record /// Token used to cancel the operation /// Response from the server public static Task TryCreateClientAsync(this IPerforceConnection connection, ClientRecord record, CancellationToken cancellationToken = default) { return TryUpdateClientAsync(connection, record, cancellationToken); } /// /// Creates a client /// /// Connection to the Perforce server /// The client record /// Token used to cancel the operation /// Response from the server public static async Task UpdateClientAsync(this IPerforceConnection connection, ClientRecord record, CancellationToken cancellationToken = default) { (await TryUpdateClientAsync(connection, record, cancellationToken)).EnsureSuccess(); } /// /// Update a client /// /// Connection to the Perforce server /// The client record /// Token used to cancel the operation /// Response from the server public static Task TryUpdateClientAsync(this IPerforceConnection connection, ClientRecord record, CancellationToken cancellationToken = default) { return SingleResponseCommandAsync(connection, "client", new List { "-i" }, connection.SerializeRecord(record), null, cancellationToken); } /// /// Deletes a client /// /// Connection to the Perforce server /// Options for this command /// Name of the client /// Token used to cancel the operation /// Response from the server public static async Task DeleteClientAsync(this IPerforceConnection connection, DeleteClientOptions options, string clientName, CancellationToken cancellationToken = default) { (await TryDeleteClientAsync(connection, options, clientName, cancellationToken)).EnsureSuccess(); } /// /// Deletes a client /// /// Connection to the Perforce server /// Options for this command /// Name of the client /// Token used to cancel the operation /// Response from the server public static Task TryDeleteClientAsync(this IPerforceConnection connection, DeleteClientOptions options, string clientName, CancellationToken cancellationToken = default) { List arguments = new List { "-d" }; if ((options & DeleteClientOptions.Force) != 0) { arguments.Add("-f"); } if ((options & DeleteClientOptions.DeleteShelved) != 0) { arguments.Add("-Fs"); } arguments.Add(clientName); return SingleResponseCommandAsync(connection, "client", arguments, null, null, cancellationToken); } /// /// Changes the stream associated with a client /// /// Connection to the Perforce server /// The new stream to be associated with the client /// Options for this command /// Token used to cancel the operation /// Response from the server public static async Task SwitchClientToStreamAsync(this IPerforceConnection connection, string streamName, SwitchClientOptions options, CancellationToken cancellationToken = default) { (await TrySwitchClientToStreamAsync(connection, streamName, options, cancellationToken)).EnsureSuccess(); } /// /// Changes the stream associated with a client /// /// Connection to the Perforce server /// The new stream to be associated with the client /// Options for this command /// Token used to cancel the operation /// Response from the server public static Task TrySwitchClientToStreamAsync(this IPerforceConnection connection, string streamName, SwitchClientOptions options, CancellationToken cancellationToken = default) { List arguments = new List { "-s" }; if ((options & SwitchClientOptions.IgnoreOpenFiles) != 0) { arguments.Add("-f"); } arguments.Add($"-S{streamName}"); return SingleResponseCommandAsync(connection, "client", arguments, null, null, cancellationToken); } /// /// Changes a client to mirror a template /// /// Connection to the Perforce server /// The new stream to be associated with the client /// Token used to cancel the operation /// Response from the server public static async Task SwitchClientToTemplateAsync(this IPerforceConnection connection, string templateName, CancellationToken cancellationToken = default) { (await TrySwitchClientToTemplateAsync(connection, templateName, cancellationToken)).EnsureSuccess(); } /// /// Changes a client to mirror a template /// /// Connection to the Perforce server /// The new stream to be associated with the client /// Token used to cancel the operation /// Response from the server public static Task TrySwitchClientToTemplateAsync(this IPerforceConnection connection, string templateName, CancellationToken cancellationToken = default) { List arguments = new List(); arguments.Add("-s"); arguments.Add($"-t{templateName}"); return SingleResponseCommandAsync(connection, "client", arguments, null, null, cancellationToken); } /// /// Queries the current client definition /// /// Connection to the Perforce server /// Name of the client. Specify null for the current client. /// Token used to cancel the operation /// Response from the server; either a client record or error code public static async Task GetClientAsync(this IPerforceConnection connection, string? clientName, CancellationToken cancellationToken = default) { return (await TryGetClientAsync(connection, clientName, cancellationToken)).Data; } /// /// Queries the current client definition /// /// Connection to the Perforce server /// Name of the client. Specify null for the current client. /// Token used to cancel the operation /// Response from the server; either a client record or error code public static Task> TryGetClientAsync(this IPerforceConnection connection, string? clientName, CancellationToken cancellationToken = default) { List arguments = new List { "-o" }; if (clientName != null) { arguments.Add(clientName); } return SingleResponseCommandAsync(connection, "client", arguments, null, cancellationToken); } /// /// Queries the view for a stream /// /// Connection to the Perforce server /// Name of the stream. /// Changelist at which to query the stream view /// Token used to cancel the operation /// Response from the server; either a client record or error code public static async Task GetStreamViewAsync(this IPerforceConnection connection, string streamName, int changeNumber, CancellationToken cancellationToken = default) { return (await TryGetStreamViewAsync(connection, streamName, changeNumber, cancellationToken)).Data; } /// /// Queries the view for a stream /// /// Connection to the Perforce server /// Name of the stream. /// Changelist at which to query the stream view /// Token used to cancel the operation /// Response from the server; either a client record or error code public static Task> TryGetStreamViewAsync(this IPerforceConnection connection, string streamName, int changeNumber, CancellationToken cancellationToken = default) { List arguments = new List { "-o" }; arguments.Add($"-S{streamName}"); if (changeNumber != -1) { arguments.Add($"-c{changeNumber}"); } return SingleResponseCommandAsync(connection, "client", arguments, null, cancellationToken); } /// /// Serializes a client record to a byte array /// /// Connection to the Perforce server /// The input record /// Serialized record data static byte[] SerializeRecord(this IPerforceConnection connection, 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", PerforceReflection.GetEnumText(typeof(ClientOptions), input.Options))); } if (input.SubmitOptions != ClientSubmitOptions.Unspecified) { nameToValue.Add(new KeyValuePair("SubmitOptions", PerforceReflection.GetEnumText(typeof(ClientSubmitOptions), input.SubmitOptions))); } if (input.LineEnd != ClientLineEndings.Unspecified) { nameToValue.Add(new KeyValuePair("LineEnd", PerforceReflection.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)); } if (input.View.Count > 0) { nameToValue.Add(new KeyValuePair("View", input.View)); } return connection.CreateRecord(nameToValue).Serialize(); } #endregion #region p4 clients /// /// Queries the current client definition /// /// Connection to the Perforce server /// Options for this command /// List only client workspaces owned by this user. /// Token used to cancel the operation /// Response from the server; either a client record or error code public static async Task> GetClientsAsync(this IPerforceConnection connection, ClientsOptions options, string? userName, CancellationToken cancellationToken = default) { return (await TryGetClientsAsync(connection, options, userName, cancellationToken)).Data; } /// /// Queries the current client definition /// /// Connection to the Perforce server /// Options for this command /// List only client workspaces owned by this user. /// Token used to cancel the operation /// Response from the server; either a client record or error code public static Task> TryGetClientsAsync(this IPerforceConnection connection, ClientsOptions options, string? userName, CancellationToken cancellationToken = default) { return TryGetClientsAsync(connection, options, null, -1, null, userName, cancellationToken); } /// /// Queries the current client definition /// /// Connection to the Perforce server /// 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. /// Token used to cancel the operation /// Response from the server; either a client record or error code public static async Task> GetClientsAsync(this IPerforceConnection connection, ClientsOptions options, string? filter, int maxResults, string? stream, string? userName, CancellationToken cancellationToken = default) { return (await TryGetClientsAsync(connection, options, filter, maxResults, stream, userName, cancellationToken)).Data; } /// /// Queries the current client definition /// /// Connection to the Perforce server /// 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. /// Token used to cancel the operation /// Response from the server; either a client record or error code public static Task> TryGetClientsAsync(this IPerforceConnection connection, ClientsOptions options, string? filter, int maxResults, string? stream, string? userName, CancellationToken cancellationToken = default) { List arguments = new List(); if ((options & ClientsOptions.All) != 0) { arguments.Add("-a"); } if (filter != null) { if ((options & ClientsOptions.CaseSensitiveFilter) != 0) { arguments.Add("-e"); arguments.Add(filter); } else { arguments.Add("-E"); arguments.Add(filter); } } if (maxResults != -1) { arguments.Add($"-m{maxResults}"); } if (stream != null) { arguments.Add($"-S{stream}"); } if ((options & ClientsOptions.WithTimes) != 0) { arguments.Add("-t"); } if (userName != null) { arguments.Add("-u"); arguments.Add(userName); } if ((options & ClientsOptions.Unloaded) != 0) { arguments.Add("-U"); } return CommandAsync(connection, "clients", arguments, null, cancellationToken); } #endregion #region p4 delete /// /// Execute the 'delete' command /// /// Connection to the Perforce server /// The change to add deleted files to /// Options for the command /// List of file specifications to query /// Token used to cancel the operation /// List of responses from the server public static async Task> DeleteAsync(this IPerforceConnection connection, int changeNumber, DeleteOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return (await TryDeleteAsync(connection, changeNumber, options, fileSpecs, cancellationToken)).Data; } /// /// Execute the 'delete' command /// /// Connection to the Perforce server /// The change to add deleted files to /// Options for the command /// List of file specifications to query /// Token used to cancel the operation /// List of responses from the server public static async Task> TryDeleteAsync(this IPerforceConnection connection, int changeNumber, DeleteOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { List arguments = new List(); if (changeNumber != -1) { arguments.Add($"-c{changeNumber}"); } if ((options & DeleteOptions.PreviewOnly) != 0) { arguments.Add("-n"); } if ((options & DeleteOptions.KeepWorkspaceFiles) != 0) { arguments.Add("-k"); } if ((options & DeleteOptions.WithoutSyncing) != 0) { arguments.Add("-v"); } PerforceResponseList records = await BatchedCommandAsync(connection, "delete", arguments, fileSpecs.List, cancellationToken); records.RemoveAll(x => x.Error != null && x.Error.Generic == PerforceGenericCode.Empty); return records; } #endregion #region p4 depot /// /// Queries the current depot definition /// /// Connection to the Perforce server /// Name of the client. Specify null for the current client. /// Token used to cancel the operation /// Response from the server; either a client record or error code public static Task GetDepotAsync(this IPerforceConnection connection, string depotName, CancellationToken cancellationToken = default) { return TryGetDepotAsync(connection, depotName, cancellationToken).UnwrapAsync(); } /// /// Queries the current depot definition /// /// Connection to the Perforce server /// Name of the client. Specify null for the current client. /// Token used to cancel the operation /// Response from the server; either a client record or error code public static Task> TryGetDepotAsync(this IPerforceConnection connection, string depotName, CancellationToken cancellationToken = default) { List arguments = new List { "-o", depotName }; return SingleResponseCommandAsync(connection, "depot", arguments, null, cancellationToken); } #endregion #region p4 describe /// /// Describes a single changelist /// /// Connection to the Perforce server /// The changelist number to retrieve description for /// Token used to cancel the operation /// Response from the server; either a describe record or error code public static async Task DescribeAsync(this IPerforceConnection connection, int changeNumber, CancellationToken cancellationToken = default) { return (await TryDescribeAsync(connection, changeNumber, cancellationToken)).Data; } /// /// Describes a single changelist /// /// Connection to the Perforce server /// The changelist number to retrieve description for /// Token used to cancel the operation /// Response from the server; either a describe record or error code public static async Task> TryDescribeAsync(this IPerforceConnection connection, int changeNumber, CancellationToken cancellationToken = default) { PerforceResponseList records = await TryDescribeAsync(connection, new int[] { changeNumber }, cancellationToken); 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 single changelist /// /// Connection to the Perforce server /// Options for the command /// Maximum number of files to return /// The changelist number to retrieve description for /// Token used to cancel the operation /// Response from the server; either a describe record or error code public static async Task DescribeAsync(this IPerforceConnection connection, DescribeOptions options, int maxNumFiles, int changeNumber, CancellationToken cancellationToken = default) { PerforceResponse response = await TryDescribeAsync(connection, options, maxNumFiles, changeNumber, cancellationToken); return response.Data; } /// /// Describes a single changelist /// /// Connection to the Perforce server /// Options for the command /// Maximum number of files to return /// The changelist number to retrieve description for /// Token used to cancel the operation /// Response from the server; either a describe record or error code public static async Task> TryDescribeAsync(this IPerforceConnection connection, DescribeOptions options, int maxNumFiles, int changeNumber, CancellationToken cancellationToken = default) { PerforceResponseList records = await TryDescribeAsync(connection, options, maxNumFiles, new int[] { changeNumber }, cancellationToken); 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 /// /// Connection to the Perforce server /// The changelist numbers to retrieve descriptions for /// Token used to cancel the operation /// List of responses from the server public static async Task> DescribeAsync(this IPerforceConnection connection, int[] changeNumbers, CancellationToken cancellationToken = default) { return (await TryDescribeAsync(connection, changeNumbers, cancellationToken)).Data; } /// /// Describes a set of changelists /// /// Connection to the Perforce server /// The changelist numbers to retrieve descriptions for /// Token used to cancel the operation /// List of responses from the server public static Task> TryDescribeAsync(this IPerforceConnection connection, int[] changeNumbers, CancellationToken cancellationToken = default) { List arguments = new List { "-s" }; foreach (int changeNumber in changeNumbers) { arguments.Add($"{changeNumber}"); } return CommandAsync(connection, "describe", arguments, null, cancellationToken); } /// /// Describes a set of changelists /// /// Connection to the Perforce server /// Options for the command /// Maximum number of files to return /// The changelist numbers to retrieve descriptions for /// Token used to cancel the operation /// List of responses from the server public static async Task> DescribeAsync(this IPerforceConnection connection, DescribeOptions options, int maxNumFiles, int[] changeNumbers, CancellationToken cancellationToken = default) { return (await TryDescribeAsync(connection, options, maxNumFiles, changeNumbers, cancellationToken)).Data; } /// /// Describes a set of changelists /// /// Connection to the Perforce server /// Options for the command /// Maximum number of files to return /// The changelist numbers to retrieve descriptions for /// Token used to cancel the operation /// List of responses from the server public static Task> TryDescribeAsync(this IPerforceConnection connection, DescribeOptions options, int maxNumFiles, int[] changeNumbers, CancellationToken cancellationToken = default) { List arguments = new List { "-s" }; if ((options & DescribeOptions.ShowDescriptionForRestrictedChanges) != 0) { arguments.Add("-f"); } if ((options & DescribeOptions.Identity) != 0) { arguments.Add("-I"); } if (maxNumFiles != -1) { arguments.Add($"-m{maxNumFiles}"); } if ((options & DescribeOptions.OriginalChangeNumber) != 0) { arguments.Add("-O"); } if ((options & DescribeOptions.Shelved) != 0) { arguments.Add("-S"); } foreach (int changeNumber in changeNumbers) { arguments.Add($"{changeNumber}"); } return CommandAsync(connection, "describe", arguments, null, cancellationToken); } #endregion #region p4 dirs /// /// List directories under a depot path /// /// Connection to the Perforce server /// Options for the command /// List directories mapped for the specified stream /// Files to be opened for edit /// Token used to cancel the operation /// Response from the server public static async Task> GetDirsAsync(this IPerforceConnection connection, DirsOptions options, string? stream, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return (await TryGetDirsAsync(connection, options, stream, fileSpecs, cancellationToken)).Data; } /// /// List directories under a depot path /// /// Connection to the Perforce server /// Options for the command /// List directories mapped for the specified stream /// Files to be opened for edit /// Token used to cancel the operation /// Response from the server public static Task> TryGetDirsAsync(this IPerforceConnection connection, DirsOptions options, string? stream, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { List arguments = new List(); if ((options & DirsOptions.OnlyMapped) != 0) { arguments.Add("-C"); } if ((options & DirsOptions.IncludeDeleted) != 0) { arguments.Add("-D"); } if ((options & DirsOptions.OnlyHave) != 0) { arguments.Add("-H"); } if (stream != null) { arguments.Add("-S"); arguments.Add(stream); } if ((options & DirsOptions.IgnoreCase) != 0) { arguments.Add("-i"); } return CommandAsync(connection, "dirs", arguments, fileSpecs.List, null, cancellationToken); } #endregion #region p4 edit /// /// Opens files for edit /// /// Connection to the Perforce server /// Changelist to add files to /// Files to be opened for edit /// Token used to cancel the operation /// Response from the server public static async Task> EditAsync(this IPerforceConnection connection, int changeNumber, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return (await TryEditAsync(connection, changeNumber, fileSpecs, cancellationToken)).Data; } /// /// Opens files for edit /// /// Connection to the Perforce server /// Changelist to add files to /// Files to be opened for edit /// Token used to cancel the operation /// Response from the server public static Task> TryEditAsync(this IPerforceConnection connection, int changeNumber, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TryEditAsync(connection, changeNumber, null, EditOptions.None, fileSpecs, cancellationToken); } /// /// Opens files for edit /// /// Connection to the Perforce server /// Changelist to add files to /// Type for new files /// Options for the command /// Files to be opened for edit /// Token used to cancel the operation /// Response from the server public static async Task> EditAsync(this IPerforceConnection connection, int changeNumber, string? fileType, EditOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return (await TryEditAsync(connection, changeNumber, fileType, options, fileSpecs, cancellationToken)).Data; } /// /// Opens files for edit /// /// Connection to the Perforce server /// Changelist to add files to /// Type for new files /// Options for the command /// Files to be opened for edit /// Token used to cancel the operation /// Response from the server public static Task> TryEditAsync(this IPerforceConnection connection, int changeNumber, string? fileType, EditOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { List arguments = new List(); if (changeNumber != -1) { arguments.Add($"-c{changeNumber}"); } if ((options & EditOptions.KeepWorkspaceFiles) != 0) { arguments.Add("-k"); } if ((options & EditOptions.PreviewOnly) != 0) { arguments.Add("-n"); } if (fileType != null) { arguments.Add($"-t{fileType}"); } return BatchedCommandAsync(connection, "edit", arguments, fileSpecs.List, cancellationToken); } #endregion #region p4 filelog /// /// Execute the 'filelog' command /// /// Connection to the Perforce server /// Options for the command /// List of file specifications to query /// Token used to cancel the operation /// List of responses from the server public static async Task> FileLogAsync(this IPerforceConnection connection, FileLogOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return (await TryFileLogAsync(connection, options, fileSpecs, cancellationToken)).Data; } /// /// Execute the 'filelog' command /// /// Connection to the Perforce server /// Options for the command /// List of file specifications to query /// Token used to cancel the operation /// List of responses from the server public static Task> TryFileLogAsync(this IPerforceConnection connection, FileLogOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TryFileLogAsync(connection, -1, -1, options, fileSpecs, cancellationToken); } /// /// Execute the 'filelog' command /// /// Connection to the Perforce server /// Number of changelists to show. Ignored if zero or negative. /// Options for the command /// List of file specifications to query /// Token used to cancel the operation /// List of responses from the server public static async Task> FileLogAsync(this IPerforceConnection connection, int maxChanges, FileLogOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return (await TryFileLogAsync(connection, maxChanges, options, fileSpecs, cancellationToken)).Data; } /// /// Execute the 'filelog' command /// /// Connection to the Perforce server /// Number of changelists to show. Ignored if zero or negative. /// Options for the command /// List of file specifications to query /// Token used to cancel the operation /// List of responses from the server public static Task> TryFileLogAsync(this IPerforceConnection connection, int maxChanges, FileLogOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TryFileLogAsync(connection, -1, maxChanges, options, fileSpecs, cancellationToken); } /// /// Execute the 'filelog' command /// /// Connection to the Perforce server /// 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 /// Token used to cancel the operation /// List of responses from the server public static async Task> FileLogAsync(this IPerforceConnection connection, int changeNumber, int maxChanges, FileLogOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return (await TryFileLogAsync(connection, changeNumber, maxChanges, options, fileSpecs, cancellationToken)).Data; } /// /// Execute the 'filelog' command /// /// Connection to the Perforce server /// 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 /// Token used to cancel the operation /// List of responses from the server public static async Task> TryFileLogAsync(this IPerforceConnection connection, int changeNumber, int maxChanges, FileLogOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { // Build the argument list List arguments = new List(); if (changeNumber > 0) { arguments.Add($"-c{changeNumber}"); } if ((options & FileLogOptions.ContentHistory) != 0) { arguments.Add("-h"); } if ((options & FileLogOptions.FollowAcrossBranches) != 0) { arguments.Add("-i"); } if ((options & FileLogOptions.FullDescriptions) != 0) { arguments.Add("-l"); } if ((options & FileLogOptions.LongDescriptions) != 0) { arguments.Add("-L"); } if (maxChanges > 0) { arguments.Add($"-m{maxChanges}"); } if ((options & FileLogOptions.DoNotFollowPromotedTaskStreams) != 0) { arguments.Add("-p"); } if ((options & FileLogOptions.IgnoreNonContributoryIntegrations) != 0) { arguments.Add("-s"); } // Always include times to simplify parsing arguments.Add("-t"); // Append all the arguments PerforceResponseList records = await BatchedCommandAsync(connection, "filelog", arguments, fileSpecs.List, cancellationToken); records.RemoveAll(x => x.Error != null && x.Error.Generic == PerforceGenericCode.Empty); return records; } #endregion #region p4 files /// /// Execute the 'files' command /// /// Connection to the Perforce server /// Options for the command /// List of file specifications to query /// Token used to cancel the operation /// List of response objects public static async Task> FilesAsync(this IPerforceConnection connection, FilesOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return (await TryFilesAsync(connection, options, -1, fileSpecs, cancellationToken)).Data; } /// /// Execute the 'files' command /// /// Connection to the Perforce server /// Options for the command /// List of file specifications to query /// Token used to cancel the operation /// List of response objects public static Task> TryFilesAsync(this IPerforceConnection connection, FilesOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TryFilesAsync(connection, options, -1, fileSpecs, cancellationToken); } /// /// Execute the 'files' command /// /// Connection to the Perforce server /// Options for the command /// Maximum number of results to return. Ignored if less than or equal to zero. /// List of file specifications to query /// Token used to cancel the operation /// List of response objects public static async Task> FilesAsync(this IPerforceConnection connection, FilesOptions options, int maxFiles, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return (await TryFilesAsync(connection, options, maxFiles, fileSpecs, cancellationToken)).Data; } /// /// Execute the 'files' command /// /// Connection to the Perforce server /// Options for the command /// Maximum number of results to return. Ignored if less than or equal to zero. /// List of file specifications to query /// Token used to cancel the operation /// List of response objects public static async Task> TryFilesAsync(this IPerforceConnection connection, FilesOptions options, int maxFiles, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { List arguments = new List(); if ((options & FilesOptions.AllRevisions) != 0) { arguments.Add("-a"); } if ((options & FilesOptions.LimitToArchiveDepots) != 0) { arguments.Add("-A"); } if ((options & FilesOptions.ExcludeDeleted) != 0) { arguments.Add("-e"); } if ((options & FilesOptions.IgnoreCase) != 0) { arguments.Add("-i"); } if (maxFiles > 0) { arguments.Add($"-m{maxFiles}"); } PerforceResponseList records = await BatchedCommandAsync(connection, "files", arguments, fileSpecs.List, cancellationToken); records.RemoveAll(x => x.Error != null && x.Error.Generic == PerforceGenericCode.Empty); return records; } #endregion #region p4 fstat /// /// Execute the 'fstat' command /// /// Connection to the Perforce server /// List of file specifications to query /// Token used to cancel the operation /// List of responses from the server public static IAsyncEnumerable FStatAsync(this IPerforceConnection connection, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TryFStatAsync(connection, FStatOptions.None, fileSpecs, cancellationToken).Select(x => x.Data); } /// /// Execute the 'fstat' command /// /// Connection to the Perforce server /// Options for the command /// List of file specifications to query /// Token used to cancel the operation /// List of responses from the server public static IAsyncEnumerable FStatAsync(this IPerforceConnection connection, FStatOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TryFStatAsync(connection, options, fileSpecs, cancellationToken).Select(x => x.Data); } /// /// Execute the 'fstat' command /// /// Connection to the Perforce server /// Options for the command /// List of file specifications to query /// Token used to cancel the operation /// List of responses from the server public static IAsyncEnumerable> TryFStatAsync(this IPerforceConnection connection, FStatOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TryFStatAsync(connection, -1, options, fileSpecs, cancellationToken); } /// /// Execute the 'fstat' command /// /// Connection to the Perforce server /// Produce fstat output for only the first max files. /// Options for the command /// List of file specifications to query /// Token used to cancel the operation /// List of responses from the server public static IAsyncEnumerable FStatAsync(this IPerforceConnection connection, int maxFiles, FStatOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TryFStatAsync(connection, maxFiles, options, fileSpecs, cancellationToken).Select(x => x.Data); } /// /// Execute the 'fstat' command /// /// Connection to the Perforce server /// Produce fstat output for only the first max files. /// Options for the command /// List of file specifications to query /// Token used to cancel the operation /// List of responses from the server public static IAsyncEnumerable> TryFStatAsync(this IPerforceConnection connection, int maxFiles, FStatOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TryFStatAsync(connection, -1, -1, null, null, maxFiles, options, fileSpecs, cancellationToken); } /// /// Execute the 'fstat' command /// /// Connection to the Perforce server /// 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. /// Fields to return in the output /// Produce fstat output for only the first max files. /// Options for the command /// List of file specifications to query /// Token used to cancel the operation /// List of responses from the server public static IAsyncEnumerable FStatAsync(this IPerforceConnection connection, int afterChangeNumber, int onlyChangeNumber, string? filter, string? fields, int maxFiles, FStatOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TryFStatAsync(connection, afterChangeNumber, onlyChangeNumber, filter, fields, maxFiles, options, fileSpecs, cancellationToken).Select(x => x.Data); } /// /// Execute the 'fstat' command /// /// Connection to the Perforce server /// 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. /// Fields to return in the output /// Produce fstat output for only the first max files. /// Options for the command /// List of file specifications to query /// Token used to cancel the operation /// List of responses from the server public static IAsyncEnumerable> TryFStatAsync(this IPerforceConnection connection, int afterChangeNumber, int onlyChangeNumber, string? filter, string? fields, int maxFiles, FStatOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { // Build the argument list List arguments = new List(); if (afterChangeNumber != -1) { arguments.Add($"-c{afterChangeNumber}"); } if (onlyChangeNumber != -1) { arguments.Add($"-e{onlyChangeNumber}"); } if (filter != null) { arguments.Add("-F"); arguments.Add(filter); } if (fields != null) { arguments.Add("-T"); arguments.Add(fields); } if ((options & FStatOptions.ReportDepotSyntax) != 0) { arguments.Add("-L"); } if ((options & FStatOptions.AllRevisions) != 0) { arguments.Add("-Of"); } if ((options & FStatOptions.IncludeFileSizes) != 0) { arguments.Add("-Ol"); } if ((options & FStatOptions.ClientFileInPerforceSyntax) != 0) { arguments.Add("-Op"); } if ((options & FStatOptions.ShowPendingIntegrations) != 0) { arguments.Add("-Or"); } if ((options & FStatOptions.ShortenOutput) != 0) { arguments.Add("-Os"); } if ((options & FStatOptions.ReverseOrder) != 0) { arguments.Add("-r"); } if ((options & FStatOptions.OnlyMapped) != 0) { arguments.Add("-Rc"); } if ((options & FStatOptions.OnlyHave) != 0) { arguments.Add("-Rh"); } if ((options & FStatOptions.OnlyOpenedBeforeHead) != 0) { arguments.Add("-Rn"); } if ((options & FStatOptions.OnlyOpenInWorkspace) != 0) { arguments.Add("-Ro"); } if ((options & FStatOptions.OnlyOpenAndResolved) != 0) { arguments.Add("-Rr"); } if ((options & FStatOptions.OnlyShelved) != 0) { arguments.Add("-Rs"); } if ((options & FStatOptions.OnlyUnresolved) != 0) { arguments.Add("-Ru"); } if ((options & FStatOptions.SortByDate) != 0) { arguments.Add("-Sd"); } if ((options & FStatOptions.SortByHaveRevision) != 0) { arguments.Add("-Sh"); } if ((options & FStatOptions.SortByHeadRevision) != 0) { arguments.Add("-Sr"); } if ((options & FStatOptions.SortByFileSize) != 0) { arguments.Add("-Ss"); } if ((options & FStatOptions.SortByFileType) != 0) { arguments.Add("-St"); } if ((options & FStatOptions.IncludeFilesInUnloadDepot) != 0) { arguments.Add("-U"); } if (maxFiles > 0) { arguments.Add($"-m{maxFiles}"); } // Execute the command IAsyncEnumerable> records = StreamCommandAsync(connection, "fstat", arguments, fileSpecs.List, null, cancellationToken); records = records.Where(x => x.Error == null || x.Error.Generic != PerforceGenericCode.Empty); if (onlyChangeNumber != -1) { records = records.Where(x => !x.Succeeded || x.Data.Description == null); } return records; } #endregion #region p4 have /// /// Determine files currently synced to the client /// /// Connection to the Perforce server /// Files to query /// Token used to cancel the operation /// List of file records public static IAsyncEnumerable HaveAsync(this IPerforceConnection connection, FileSpecList fileSpec, CancellationToken cancellationToken = default) { return TryHaveAsync(connection, fileSpec, cancellationToken).Select(x => x.Data); } /// /// Determine files currently synced to the client /// /// Connection to the Perforce server /// Files to query /// Token used to cancel the operation /// List of file records public static IAsyncEnumerable> TryHaveAsync(this IPerforceConnection connection, FileSpecList fileSpec, CancellationToken cancellationToken = default) { IAsyncEnumerable> records = StreamCommandAsync(connection, "have", new List(), fileSpec.List, null, cancellationToken); records = records.Where(x => x.Error == null || x.Error.Generic != PerforceGenericCode.Empty); return records; } #endregion #region p4 info /// /// Execute the 'info' command /// /// Connection to the Perforce server /// Options for the command /// Token used to cancel the operation /// Response from the server; an InfoRecord or error code public static async Task GetInfoAsync(this IPerforceConnection connection, InfoOptions options, CancellationToken cancellationToken = default) { return (await TryGetInfoAsync(connection, options, cancellationToken)).Data; } /// /// Execute the 'info' command /// /// Connection to the Perforce server /// Options for the command /// Token used to cancel the operation /// Response from the server; an InfoRecord or error code public static Task> TryGetInfoAsync(this IPerforceConnection connection, InfoOptions options, CancellationToken cancellationToken = default) { // Build the argument list List arguments = new List(); if ((options & InfoOptions.ShortOutput) != 0) { arguments.Add("-s"); } return SingleResponseCommandAsync(connection, "info", arguments, null, cancellationToken); } #endregion #region p4 login /// /// Log in to the server /// /// /// /// /// public static async Task LoginAsync(this IPerforceConnection connection, string password, CancellationToken cancellationToken = default) { return (await TryLoginAsync(connection, password, cancellationToken)).Data; } /// /// Attempts to log in to the server /// /// Connection to use /// /// /// public static Task> TryLoginAsync(this IPerforceConnection connection, string? password, CancellationToken cancellationToken = default) { return TryLoginAsync(connection, LoginOptions.None, null, password, null, cancellationToken); } /// /// Attempts to log in to the server /// /// Connection to use /// Options for the command /// User to login as /// Password for the user /// Host for the ticket /// /// public static async Task> TryLoginAsync(this IPerforceConnection connection, LoginOptions options, string? user, string? password, string? host, CancellationToken cancellationToken = default) { List arguments = new List(); if ((options & LoginOptions.AllHosts) != 0) { arguments.Add("-a"); } if ((options & LoginOptions.PrintTicket) != 0) { arguments.Add("-p"); } if ((options & LoginOptions.Status) != 0) { arguments.Add("-s"); } if (host != null) { arguments.Add($"-h{host}"); } if (user != null) { arguments.Add(user); } List parsedResponses; #pragma warning disable CA1849 // Call async methods when in an async method await using (IPerforceOutput response = connection.Command("login", arguments, null, null, password, false)) { for (; ; ) { if (!await response.ReadAsync(cancellationToken)) { break; } } DiscardPasswordPrompt(response); parsedResponses = await response.ReadResponsesAsync(typeof(LoginRecord), cancellationToken); // end lifetime of `response` here // this prevents a deadlock in case `connection` is a `NativePerforceConnection` and `response` is a `NativePerforceConnection.Response` // not DisposeAsync()ing here will cause a deadlock when calling `TryGetLoginStateAsync()` below } #pragma warning restore CA1849 // Call async methods when in an async method PerforceResponse? error = parsedResponses.FirstOrDefault(x => !x.Succeeded); if (error != null) { return new PerforceResponse(error); } string? ticket = null; if ((options & LoginOptions.PrintTicket) != 0) { if (parsedResponses.Count != 2) { throw new PerforceException("Unable to parse login response; expected two records, one with ticket id"); } PerforceInfo? info = parsedResponses[0].Info; if (info == null) { throw new PerforceException("Unable to parse login response; expected two records, one with ticket id"); } ticket = info.Data; parsedResponses.RemoveAt(0); } LoginRecord? loginRecord = parsedResponses.First().Data as LoginRecord; if (loginRecord == null) { // Older versions of P4.EXE do not return a login record for succesful login, instread just returning a string. Call p4 login -s to get the login state instead. PerforceResponse legacyResponse = await TryGetLoginStateAsync(connection, cancellationToken); if (!legacyResponse.Succeeded) { return legacyResponse; } loginRecord = legacyResponse.Data; } loginRecord.Ticket = ticket; return new PerforceResponse(loginRecord); } static void DiscardPasswordPrompt(IPerforceOutput output) { ReadOnlySpan data = output.Data.Span; for (int idx = 0; idx < data.Length; idx++) { if (data[idx] == '{') { output.Discard(idx); break; } } } /// /// Gets the state of the current user's login /// /// Connection to the Perforce server /// Token used to cancel the operation /// public static async Task GetLoginStateAsync(this IPerforceConnection connection, CancellationToken cancellationToken = default) { return (await TryGetLoginStateAsync(connection, cancellationToken)).Data; } /// /// Gets the state of the current user's login /// /// Connection to the Perforce server /// Token used to cancel the operation /// public static async Task> TryGetLoginStateAsync(this IPerforceConnection connection, CancellationToken cancellationToken = default) { return await SingleResponseCommandAsync(connection, "login", new List { "-s" }, null, cancellationToken); } #endregion #region p4 merge /// /// Execute the 'merge' command /// /// Connection to the Perforce server /// Options for the merge /// /// Maximum number of files to merge /// The source filespec and revision range /// The target filespec /// Cancellation token /// List of records public static async Task> MergeAsync(this IPerforceConnection connection, MergeOptions options, int change, int maxFiles, string sourceFileSpec, string targetFileSpec, CancellationToken cancellationToken = default) { List arguments = new List(); if ((options & MergeOptions.Preview) != 0) { arguments.Add($"-n"); } if (change != -1) { arguments.Add($"-c{change}"); } if (maxFiles != -1) { arguments.Add($"-m{maxFiles}"); } if ((options & MergeOptions.Force) != 0) { arguments.Add("-F"); } if ((options & MergeOptions.ReverseMapping) != 0) { arguments.Add("-r"); } if ((options & MergeOptions.AsStreamSpec) != 0) { arguments.Add("-As"); } if ((options & MergeOptions.AsFiles) != 0) { arguments.Add("-Af"); } if ((options & MergeOptions.Stream) != 0) { arguments.Add("-S"); } arguments.Add(sourceFileSpec); if ((options & MergeOptions.Source) != 0) { arguments.Add("-s"); } arguments.Add(targetFileSpec); PerforceResponseList records = await CommandAsync(connection, "merge", arguments, null, cancellationToken); records.RemoveAll(x => x.Error != null && x.Error.Generic == PerforceGenericCode.Empty); return records; } #endregion #region p4 move /// /// Opens files for move /// /// Connection to the Perforce server /// Changelist to add files to /// Type for new files /// Options for the command /// The source file(s) /// The target file(s) /// Token used to cancel the operation /// Response from the server public static async Task> MoveAsync(this IPerforceConnection connection, int changeNumber, string? fileType, MoveOptions options, string sourceFileSpec, string targetFileSpec, CancellationToken cancellationToken = default) { return (await TryMoveAsync(connection, changeNumber, fileType, options, sourceFileSpec, targetFileSpec, cancellationToken)).Data; } /// /// Opens files for move /// /// Connection to the Perforce server /// Changelist to add files to /// Type for new files /// Options for the command /// The source file(s) /// The target file(s) /// Token used to cancel the operation /// Response from the server public static async Task> TryMoveAsync(this IPerforceConnection connection, int changeNumber, string? fileType, MoveOptions options, string sourceFileSpec, string targetFileSpec, CancellationToken cancellationToken = default) { List arguments = new List(); if (changeNumber != -1) { arguments.Add($"-c{changeNumber}"); } if ((options & MoveOptions.KeepWorkspaceFiles) != 0) { arguments.Add("-k"); } if ((options & MoveOptions.RenameOnly) != 0) { arguments.Add("-r"); } if ((options & MoveOptions.PreviewOnly) != 0) { arguments.Add("-n"); } if (fileType != null) { arguments.Add($"-t{fileType}"); } arguments.Add(sourceFileSpec); arguments.Add(targetFileSpec); PerforceResponseList records = await CommandAsync(connection, "move", arguments, null, cancellationToken); records.RemoveAll(x => x.Error != null && x.Error.Generic == PerforceGenericCode.Empty); return records; } #endregion #region p4 opened /// /// Execute the 'opened' command /// /// Connection to the Perforce server /// Options for the command /// Specification for the files to list /// Token used to cancel the operation /// Response from the server public static IAsyncEnumerable OpenedAsync(this IPerforceConnection connection, OpenedOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TryOpenedAsync(connection, options, fileSpecs, cancellationToken).Select(x => x.Data); } /// /// Execute the 'opened' command /// /// Connection to the Perforce server /// Options for the command /// Specification for the files to list /// Token used to cancel the operation /// Response from the server public static IAsyncEnumerable> TryOpenedAsync(this IPerforceConnection connection, OpenedOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TryOpenedAsync(connection, options, -1, null, null, -1, fileSpecs, cancellationToken); } /// /// Execute the 'opened' command /// /// Connection to the Perforce server /// 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 /// Token used to cancel the operation /// Response from the server public static IAsyncEnumerable OpenedAsync(this IPerforceConnection connection, OpenedOptions options, int changeNumber, string? clientName, string? userName, int maxResults, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TryOpenedAsync(connection, options, changeNumber, clientName, userName, maxResults, fileSpecs, cancellationToken).Select(x => x.Data); } /// /// Execute the 'opened' command /// /// Connection to the Perforce server /// 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 /// Token used to cancel the operation /// Response from the server public static IAsyncEnumerable> TryOpenedAsync(this IPerforceConnection connection, OpenedOptions options, int changeNumber, string? clientName, string? userName, int maxResults, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { // Build the argument list List arguments = new List(); if ((options & OpenedOptions.AllWorkspaces) != 0) { arguments.Add($"-a"); } if (changeNumber == PerforceReflection.DefaultChange) { arguments.Add($"-cdefault"); } else if (changeNumber != -1) { arguments.Add($"-c{changeNumber}"); } if (clientName != null) { arguments.Add($"-C{clientName}"); } if (userName != null) { arguments.Add($"-u{userName}"); } if (maxResults != -1) { arguments.Add($"-m{maxResults}"); } if ((options & OpenedOptions.ShortOutput) != 0) { arguments.Add($"-s"); } IAsyncEnumerable> records = StreamCommandAsync(connection, "opened", arguments, fileSpecs.List, null, cancellationToken); return records.Where(x => x.Error == null || x.Error.Generic != PerforceGenericCode.Empty); } #endregion #region p4 print /// /// Execute the 'print' command /// /// Connection to the Perforce server /// Output file to redirect output to /// Specification for the files to print /// Token used to cancel the operation /// Response from the server public static async Task PrintAsync(this IPerforceConnection connection, string outputFile, string fileSpec, CancellationToken cancellationToken = default) { return (await TryPrintSingleInternalAsync(connection, outputFile, fileSpec, cancellationToken)).Data; } /// /// Execute the 'print' command /// /// Connection to the Perforce server /// Output file to redirect output to /// Specification for the files to print /// Token used to cancel the operation /// Response from the server public static Task> TryPrintAsync(this IPerforceConnection connection, string outputFile, string fileSpec, CancellationToken cancellationToken = default) { return TryPrintInternalAsync(connection, outputFile, fileSpec, cancellationToken); } /// /// Execute the 'print' command /// /// Connection to the Perforce server /// Specification for the files to print /// Token used to cancel the operation /// Response from the server public static async Task>> TryPrintDataAsync(this IPerforceConnection connection, string fileSpec, CancellationToken cancellationToken = default) { string tempFile = Path.GetTempFileName(); try { PerforceResponse> record = await TryPrintSingleInternalAsync>(connection, tempFile, fileSpec, cancellationToken); if (record.Succeeded) { record.Data.Contents = await File.ReadAllBytesAsync(tempFile, cancellationToken); } return record; } finally { try { File.SetAttributes(tempFile, FileAttributes.Normal); File.Delete(tempFile); } catch { } } } /// /// Execute the 'print' command /// /// Connection to the Perforce server /// Specification for the files to print /// Token used to cancel the operation /// Response from the server public static async Task>> TryPrintLinesAsync(this IPerforceConnection connection, string fileSpec, CancellationToken cancellationToken = default) { string tempFile = Path.GetTempFileName(); try { PerforceResponse> record = await TryPrintSingleInternalAsync>(connection, tempFile, fileSpec, cancellationToken); if (record.Succeeded) { record.Data.Contents = await File.ReadAllLinesAsync(tempFile, cancellationToken); } return record; } finally { try { File.SetAttributes(tempFile, FileAttributes.Normal); File.Delete(tempFile); } catch { } } } /// /// Execute the 'print' command /// /// Connection to the Perforce server /// Output file to redirect output to /// Specification for the files to print /// Token used to cancel the operation /// Response from the server static async Task> TryPrintInternalAsync(this IPerforceConnection connection, string outputFile, string fileSpec, CancellationToken cancellationToken = default) { // Build the argument list List arguments = new List(); arguments.Add("-o"); arguments.Add(outputFile); arguments.Add(fileSpec); PerforceResponseList records = await CommandAsync(connection, "print", arguments, null, cancellationToken); records.RemoveAll(x => x.Error != null && x.Error.Generic == PerforceGenericCode.Empty); return records; } /// /// Execute the 'print' command for a single file /// /// Connection to the Perforce server /// Output file to redirect output to /// Specification for the files to print /// Token used to cancel the operation /// Response from the server static Task> TryPrintSingleInternalAsync(this IPerforceConnection connection, string outputFile, string fileSpec, CancellationToken cancellationToken = default) where T : class { // Build the argument list List arguments = new List(); arguments.Add("-o"); arguments.Add(outputFile); arguments.Add(fileSpec); return SingleResponseCommandAsync(connection, "print", arguments, null, cancellationToken); } #endregion #region p4 reconcile /// /// Open files for add, delete, and/or edit in order to reconcile a workspace with changes made outside of Perforce. /// /// Connection to the Perforce server /// Changelist to open files to /// Options for the command /// Files to be reverted /// Token used to cancel the operation /// Response from the server public static async Task> ReconcileAsync(this IPerforceConnection connection, int changeNumber, ReconcileOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { PerforceResponseList records = await TryReconcileAsync(connection, changeNumber, options, fileSpecs, cancellationToken); records.RemoveAll(x => x.Info != null); return records.Data; } /// /// Open files for add, delete, and/or edit in order to reconcile a workspace with changes made outside of Perforce. /// /// Connection to the Perforce server /// Changelist to open files to /// Options for the command /// Files to be reverted /// Token used to cancel the operation /// Response from the server public static async Task> TryReconcileAsync(this IPerforceConnection connection, int changeNumber, ReconcileOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { List arguments = new List(); if (changeNumber != -1) { arguments.Add($"-c{changeNumber}"); } if ((options & ReconcileOptions.Edit) != 0) { arguments.Add("-e"); } if ((options & ReconcileOptions.Add) != 0) { arguments.Add("-a"); } if ((options & ReconcileOptions.Delete) != 0) { arguments.Add("-d"); } if ((options & ReconcileOptions.PreviewOnly) != 0) { arguments.Add("-n"); } if ((options & ReconcileOptions.AllowWildcards) != 0) { arguments.Add("-f"); } if ((options & ReconcileOptions.NoIgnore) != 0) { arguments.Add("-I"); } if ((options & ReconcileOptions.LocalFileSyntax) != 0) { arguments.Add("-l"); } if ((options & ReconcileOptions.UseFileModification) != 0) { arguments.Add("-m"); } arguments.AddRange(fileSpecs.List); PerforceResponseList records = await CommandAsync(connection, "reconcile", arguments, null, cancellationToken); records.RemoveAll(x => x.Error != null && x.Error.Generic == PerforceGenericCode.Empty); return records; } #endregion #region p4 reopen /// /// Reopen a file /// /// Connection to the Perforce server /// Changelist to open files to /// New filetype /// Files to be reverted /// Token used to cancel the operation /// Response from the server public static async Task> ReopenAsync(this IPerforceConnection connection, int? changeNumber, string? fileType, string fileSpec, CancellationToken cancellationToken = default) { return (await TryReopenAsync(connection, changeNumber, fileType, fileSpec, cancellationToken)).Data; } /// /// Reopen a file /// /// Connection to the Perforce server /// Changelist to open files to /// New filetype /// Files to be reverted /// Token used to cancel the operation /// Response from the server public static async Task> TryReopenAsync(this IPerforceConnection connection, int? changeNumber, string? fileType, string fileSpec, CancellationToken cancellationToken = default) { List arguments = new List(); if (changeNumber != null) { if (changeNumber == PerforceReflection.DefaultChange) { arguments.Add("-cdefault"); } else { arguments.Add($"-c{changeNumber}"); } } if (fileType != null) { arguments.Add($"-t{fileType}"); } arguments.Add(fileSpec); PerforceResponseList records = await CommandAsync(connection, "reopen", arguments, null, cancellationToken); records.RemoveAll(x => x.Error != null && x.Error.Generic == PerforceGenericCode.Empty); return records; } #endregion #region p4 reload /// /// Reloads a client workspace /// /// Connection to the Perforce server /// Name of the client to reload /// The source server id /// Token used to cancel the operation /// Response from the server public static Task> ReloadClient(this IPerforceConnection connection, string clientName, string sourceServerId, CancellationToken cancellationToken = default) { List arguments = new List(); arguments.Add($"-c{clientName}"); arguments.Add($"-p{sourceServerId}"); return connection.CommandAsync("reload", arguments, null, null, cancellationToken); } #endregion p4 reload #region p4 resolve /// /// Resolve conflicts between file revisions. /// /// Connection to the Perforce server /// Changelist to open files to /// Options for the command /// Files to be reverted /// Token used to cancel the operation /// Response from the server public static async Task> ResolveAsync(this IPerforceConnection connection, int changeNumber, ResolveOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return (await TryResolveAsync(connection, changeNumber, options, fileSpecs, cancellationToken)).Data; } /// /// Resolve conflicts between file revisions. /// /// Connection to the Perforce server /// Changelist to open files to /// Options for the command /// Files to be reverted /// Token used to cancel the operation /// Response from the server public static async Task> TryResolveAsync(this IPerforceConnection connection, int changeNumber, ResolveOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { List arguments = new List(); if ((options & ResolveOptions.Automatic) != 0) { arguments.Add("-am"); } if ((options & ResolveOptions.AcceptYours) != 0) { arguments.Add("-ay"); } if ((options & ResolveOptions.AcceptTheirs) != 0) { arguments.Add("-at"); } if ((options & ResolveOptions.SafeAccept) != 0) { arguments.Add("-as"); } if ((options & ResolveOptions.ForceAccept) != 0) { arguments.Add("-af"); } if ((options & ResolveOptions.IgnoreWhitespaceOnly) != 0) { arguments.Add("-db"); } if ((options & ResolveOptions.IgnoreWhitespace) != 0) { arguments.Add("-dw"); } if ((options & ResolveOptions.IgnoreLineEndings) != 0) { arguments.Add("-dl"); } if ((options & ResolveOptions.ResolveAgain) != 0) { arguments.Add("-f"); } if ((options & ResolveOptions.PreviewOnly) != 0) { arguments.Add("-n"); } if (changeNumber != -1) { arguments.Add($"-c{changeNumber}"); } arguments.AddRange(fileSpecs.List); PerforceResponseList records = await CommandAsync(connection, "resolve", arguments, null, cancellationToken); records.RemoveAll(x => x.Error != null && x.Error.Generic == PerforceGenericCode.Empty); return records; } #endregion #region p4 revert /// /// Reverts files that have been added to a pending changelist. /// /// Connection to the Perforce server /// Changelist to add files to /// Revert another user’s open files. /// Options for the command /// Files to be reverted /// Token used to cancel the operation /// Response from the server public static async Task> RevertAsync(this IPerforceConnection connection, int changeNumber, string? clientName, RevertOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return (await TryRevertAsync(connection, changeNumber, clientName, options, fileSpecs, cancellationToken)).Data; } /// /// Reverts files that have been added to a pending changelist. /// /// Connection to the Perforce server /// Changelist to add files to /// Revert another user’s open files. /// Options for the command /// Files to be reverted /// Token used to cancel the operation /// Response from the server public static async Task> TryRevertAsync(this IPerforceConnection connection, int changeNumber, string? clientName, RevertOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { List arguments = new List(); if ((options & RevertOptions.Unchanged) != 0) { arguments.Add("-a"); } if ((options & RevertOptions.PreviewOnly) != 0) { arguments.Add("-n"); } if ((options & RevertOptions.KeepWorkspaceFiles) != 0) { arguments.Add("-k"); } if ((options & RevertOptions.DeleteAddedFiles) != 0) { arguments.Add("-w"); } if (changeNumber != -1) { arguments.Add($"-c{changeNumber}"); } if (clientName != null) { arguments.Add($"-C{clientName}"); } PerforceResponseList records = await BatchedCommandAsync(connection, "revert", arguments, fileSpecs.List, cancellationToken); records.RemoveAll(x => x.Error != null && x.Error.Generic == PerforceGenericCode.Empty); return records; } #endregion #region p4 server /// /// Gets information about the specified server /// /// Connection to the Perforce server /// The server identifier /// Token used to cancel the operation /// public static async Task GetServerAsync(this IPerforceConnection connection, string serverId, CancellationToken cancellationToken = default) { PerforceResponse response = await TryGetServerAsync(connection, serverId, cancellationToken); return response.Data; } /// /// Gets information about the specified server /// /// Connection to the Perforce server /// The server identifier /// Token used to cancel the operation /// public static Task> TryGetServerAsync(this IPerforceConnection connection, string serverId, CancellationToken cancellationToken = default) { List arguments = new List(); arguments.Add("-o"); arguments.Add(serverId); return SingleResponseCommandAsync(connection, "server", arguments, null, cancellationToken); } #endregion #region p4 shelve /// /// Shelves a set of files /// /// Connection to the Perforce server /// The change number to receive the shelved files /// Options for the command /// Files to sync /// Token used to cancel the operation /// Response from the server public static async Task> ShelveAsync(this IPerforceConnection connection, int changeNumber, ShelveOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return (await TryShelveAsync(connection, changeNumber, options, fileSpecs, cancellationToken)).Data; } /// /// Shelves a set of files /// /// Connection to the Perforce server /// The change number to receive the shelved files /// Options for the command /// Files to sync /// Token used to cancel the operation /// Response from the server public static async Task> TryShelveAsync(this IPerforceConnection connection, int changeNumber, ShelveOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { List arguments = new List(); arguments.Add($"-c{changeNumber}"); if ((options & ShelveOptions.OnlyChanged) != 0) { arguments.Add("-aleaveunchanged"); } if ((options & ShelveOptions.Overwrite) != 0) { arguments.Add("-f"); } arguments.AddRange(fileSpecs.List); PerforceResponseList records = await CommandAsync(connection, "shelve", arguments, null, cancellationToken); records.RemoveAll(x => x.Error != null && x.Error.Generic == PerforceGenericCode.Empty); return records; } /// /// Deletes files from a shelved changelist /// /// Connection to the Perforce server /// Changelist containing shelved files to be deleted /// Files to delete /// Token used to cancel the operation /// Response from the server public static async Task DeleteShelvedFilesAsync(this IPerforceConnection connection, int changeNumber, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { List responses = await TryDeleteShelvedFilesAsync(connection, changeNumber, fileSpecs, cancellationToken); PerforceResponse? errorResponse = responses.FirstOrDefault(x => x.Error != null && x.Error.Generic != PerforceGenericCode.Empty); if (errorResponse != null) { throw new PerforceException(errorResponse.Error!); } } /// /// Deletes files from a shelved changelist /// /// Connection to the Perforce server /// Changelist containing shelved files to be deleted /// Files to delete /// Token used to cancel the operation /// Response from the server public static Task> TryDeleteShelvedFilesAsync(this IPerforceConnection connection, int changeNumber, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { List arguments = new List(); arguments.Add("-d"); if (changeNumber != -1) { arguments.Add($"-c{changeNumber}"); } arguments.AddRange(fileSpecs.List); return connection.CommandAsync("shelve", arguments, null, null, cancellationToken); } #endregion #region p4 sizes /// /// Execute the 'sizes' command /// /// Connection to the Perforce server /// Options for the command /// List of file specifications to query /// Token used to cancel the operation /// List of response objects public static async Task> SizesAsync(this IPerforceConnection connection, SizesOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return (await TrySizesAsync(connection, options, -1, fileSpecs, cancellationToken)).Data; } /// /// Execute the 'sizes' command /// /// Connection to the Perforce server /// Options for the command /// List of file specifications to query /// Token used to cancel the operation /// List of response objects public static Task> TrySizesAsync(this IPerforceConnection connection, SizesOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TrySizesAsync(connection, options, -1, fileSpecs, cancellationToken); } /// /// Execute the 'sizes' command /// /// Connection to the Perforce server /// Options for the command /// Maximum number of results to return. Ignored if less than or equal to zero. /// List of file specifications to query /// Token used to cancel the operation /// List of response objects public static async Task> SizesAsync(this IPerforceConnection connection, SizesOptions options, int maxLines, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return (await TrySizesAsync(connection, options, maxLines, fileSpecs, cancellationToken)).Data; } /// /// Execute the 'sizes' command /// /// Connection to the Perforce server /// Options for the command /// Maximum number of results to return. Ignored if less than or equal to zero. /// List of file specifications to query /// Token used to cancel the operation /// List of response objects public static async Task> TrySizesAsync(this IPerforceConnection connection, SizesOptions options, int maxLines, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { List arguments = new List(); if ((options & SizesOptions.AllRevisions) != 0) { arguments.Add("-a"); } if ((options & SizesOptions.LimitToArchiveDepots) != 0) { arguments.Add("-A"); } if ((options & SizesOptions.CalculateSum) != 0) { arguments.Add("-s"); } if ((options & SizesOptions.DisplayForShelvedFilesOnly) != 0) { arguments.Add("-S"); } if ((options & SizesOptions.ExcludeLazyCopies) != 0) { arguments.Add("-z"); } if (maxLines > 0) { arguments.Add($"-m{maxLines}"); } PerforceResponseList records = await BatchedCommandAsync(connection, "sizes", arguments, fileSpecs.List, cancellationToken); records.RemoveAll(x => x.Error != null && x.Error.Generic == PerforceGenericCode.Empty); return records; } #endregion #region p4 stream /// /// Queries information about a stream /// /// Connection to the Perforce server /// Name of the stream to query /// Whether to include the stream view in the output /// Token used to cancel the operation /// Stream information record public static async Task GetStreamAsync(this IPerforceConnection connection, string streamName, bool includeView, CancellationToken cancellationToken = default) { return (await TryGetStreamAsync(connection, streamName, includeView, cancellationToken)).Data; } /// /// Queries information about a stream /// /// Connection to the Perforce server /// Name of the stream to query /// Whether to include the stream view in the output /// Token used to cancel the operation /// Stream information record public static Task> TryGetStreamAsync(this IPerforceConnection connection, string streamName, bool includeView, CancellationToken cancellationToken = default) { List arguments = new List { "-o" }; if (includeView) { arguments.Add("-v"); } arguments.Add(streamName); return SingleResponseCommandAsync(connection, "stream", arguments, null, cancellationToken); } /// /// Updates an existing stream /// /// Connection to the Perforce server /// Information of the stream to update /// Token used to cancel the operation /// Stream information record public static Task TryUpdateStreamAsync(this IPerforceConnection connection, StreamRecord record, CancellationToken cancellationToken = default) { List arguments = new() { "-i" }; return SingleResponseCommandAsync(connection, "stream", arguments, connection.SerializeRecord(record), null, cancellationToken); } /// /// Serializes a client record to a byte array /// /// Connection to the Perforce server /// The input record /// Serialized record data static byte[] SerializeRecord(this IPerforceConnection connection, StreamRecord input) { List> nameToValue = new List>(); void Add(string fieldName, string? value) { if (value != null) { nameToValue.Add(new KeyValuePair(fieldName, value)); } } Add("Stream", input.Stream); Add("Owner", input.Owner); Add("Name", input.Name); Add("Parent", input.Parent); Add("Type", input.Type); Add("Description", input.Description); Add("ParentView", input.ParentView); if (input.Options != StreamOptions.None) { nameToValue.Add(new KeyValuePair("Options", PerforceReflection.GetEnumText(typeof(StreamOptions), input.Options))); } if (input.Paths.Count > 0) { nameToValue.Add(new KeyValuePair("Paths", input.Paths)); } if (input.View.Count > 0) { nameToValue.Add(new KeyValuePair("View", input.View)); } if (input.ChangeView.Count > 0) { nameToValue.Add(new KeyValuePair("ChangeView", input.ChangeView)); } return connection.CreateRecord(nameToValue).Serialize(); } #endregion #region p4 streams /// /// Enumerates all streams in a depot /// /// Connection to the Perforce server /// The path for streams to enumerate (eg. "//UE4/...") /// Token used to cancel the operation /// List of streams matching the given criteria public static async Task> GetStreamsAsync(this IPerforceConnection connection, string? streamPath, CancellationToken cancellationToken = default) { return (await TryGetStreamsAsync(connection, streamPath, cancellationToken)).Data; } /// /// Enumerates all streams in a depot /// /// Connection to the Perforce server /// The path for streams to enumerate (eg. "//UE4/...") /// Token used to cancel the operation /// List of streams matching the given criteria public static Task> TryGetStreamsAsync(this IPerforceConnection connection, string? streamPath, CancellationToken cancellationToken = default) { return TryGetStreamsAsync(connection, streamPath, -1, null, false, cancellationToken); } /// /// Enumerates all streams in a depot /// /// Connection to the Perforce server /// 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 /// Token used to cancel the operation /// List of streams matching the given criteria public static async Task> GetStreamsAsync(this IPerforceConnection connection, string? streamPath, int maxResults, string? filter, bool unloaded, CancellationToken cancellationToken = default) { return (await TryGetStreamsAsync(connection, streamPath, maxResults, filter, unloaded, cancellationToken)).Data; } /// /// Enumerates all streams in a depot /// /// Connection to the Perforce server /// 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 /// Token used to cancel the operation /// List of streams matching the given criteria public static async Task> TryGetStreamsAsync(this IPerforceConnection connection, string? streamPath, int maxResults, string? filter, bool unloaded, CancellationToken cancellationToken = default) { // Build the command line List arguments = new List(); if (unloaded) { arguments.Add("-U"); } if (filter != null) { arguments.Add("-F"); arguments.Add(filter); } if (maxResults > 0) { arguments.Add($"-m{maxResults}"); } if (streamPath != null) { arguments.Add(streamPath); } // Execute the command PerforceResponseList records = await CommandAsync(connection, "streams", arguments, null, cancellationToken); records.RemoveAll(x => x.Error != null && x.Error.Generic == PerforceGenericCode.Empty); return records; } #endregion #region p4 submit /// /// Submits a pending changelist /// /// Connection to the Perforce server /// The changelist to submit /// Options for the command /// Token used to cancel the operation /// Response from the server public static async Task SubmitAsync(this IPerforceConnection connection, int changeNumber, SubmitOptions options, CancellationToken cancellationToken = default) { return (await TrySubmitAsync(connection, changeNumber, options, cancellationToken)).Data; } /// /// Submits a pending changelist /// /// Connection to the Perforce server /// The changelist to submit /// Options for the command /// Token used to cancel the operation /// Response from the server public static async Task> TrySubmitAsync(this IPerforceConnection connection, int changeNumber, SubmitOptions options, CancellationToken cancellationToken = default) { List arguments = new List(); if ((options & SubmitOptions.ReopenAsEdit) != 0) { arguments.Add("-r"); } if ((options & SubmitOptions.SubmitUnchanged) != 0) { arguments.Add("-f"); arguments.Add("submitunchanged"); } if ((options & SubmitOptions.RevertUnchanged) != 0) { arguments.Add("-f"); arguments.Add("revertunchanged"); } if ((options & SubmitOptions.LeaveUnchanged) != 0) { arguments.Add("-f"); arguments.Add("leaveunchanged"); } arguments.Add($"-c{changeNumber}"); PerforceResponseList responses = await CommandAsync(connection, "submit", arguments, null, cancellationToken); return ParseSubmitResponses(responses, connection.Logger); } /// /// Submits a shelved changelist /// /// Connection to the Perforce server /// The changelist to submit /// Token used to cancel the operation /// Response from the server public static async Task SubmitShelvedAsync(this IPerforceConnection connection, int changeNumber, CancellationToken cancellationToken = default) { return (await TrySubmitShelvedAsync(connection, changeNumber, cancellationToken)).Data; } /// /// Submits a pending changelist /// /// Connection to the Perforce server /// The changelist to submit /// Token used to cancel the operation /// Response from the server public static async Task> TrySubmitShelvedAsync(this IPerforceConnection connection, int changeNumber, CancellationToken cancellationToken = default) { List arguments = new List(); arguments.Add($"-e{changeNumber}"); PerforceResponseList responses = await CommandAsync(connection, "submit", arguments, null, cancellationToken); return ParseSubmitResponses(responses, connection.Logger); } static PerforceResponse ParseSubmitResponses(PerforceResponseList responses, ILogger logger) { SubmitRecord? success = null; PerforceResponse? error = null; foreach (PerforceResponse response in responses) { if (response.Error != null) { error ??= response; } else if (response.Info != null) { logger.LogInformation("Submit: {Info}", response.Info.Data); } else if (success == null) { success = response.Data; } else { success.Merge(response.Data); } } if (error != null) { return error; } else if (success != null) { return new PerforceResponse(success); } else { return responses[0]; } } #endregion #region p4 sync /// /// Syncs files from the server /// /// Connection to the Perforce server /// Files to sync /// Token used to cancel the operation /// Response from the server public static IAsyncEnumerable SyncAsync(this IPerforceConnection connection, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TrySyncAsync(connection, fileSpecs, cancellationToken).Select(x => x.Data); } /// /// Syncs files from the server /// /// Connection to the Perforce server /// Files to sync /// Token used to cancel the operation /// Response from the server public static IAsyncEnumerable> TrySyncAsync(this IPerforceConnection connection, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TrySyncAsync(connection, SyncOptions.None, -1, fileSpecs, cancellationToken); } /// /// Syncs files from the server /// /// Connection to the Perforce server /// Options for the command /// Files to sync /// Token used to cancel the operation /// Response from the server public static IAsyncEnumerable SyncAsync(this IPerforceConnection connection, SyncOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TrySyncAsync(connection, options, fileSpecs, cancellationToken).Select(x => x.Data); } /// /// Syncs files from the server /// /// Connection to the Perforce server /// Options for the command /// Files to sync /// Token used to cancel the operation /// Response from the server public static IAsyncEnumerable> TrySyncAsync(this IPerforceConnection connection, SyncOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TrySyncAsync(connection, options, -1, fileSpecs, cancellationToken); } /// /// Syncs files from the server /// /// Connection to the Perforce server /// Options for the command /// Syncs only the first number of files specified. /// Files to sync /// Token used to cancel the operation /// Response from the server public static IAsyncEnumerable SyncAsync(this IPerforceConnection connection, SyncOptions options, int maxFiles, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TrySyncAsync(connection, options, maxFiles, fileSpecs, cancellationToken).Select(x => x.Data); } /// /// Syncs files from the server /// /// Connection to the Perforce server /// Options for the command /// Syncs only the first number of files specified. /// Files to sync /// Token used to cancel the operation /// Response from the server public static IAsyncEnumerable> TrySyncAsync(this IPerforceConnection connection, SyncOptions options, int maxFiles, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TrySyncAsync(connection, options, maxFiles, -1, -1, -1, -1, -1, fileSpecs, cancellationToken); } /// /// Syncs files from the server /// /// Connection to the Perforce 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 /// Token used to cancel the operation /// Response from the server public static IAsyncEnumerable SyncAsync(this IPerforceConnection connection, SyncOptions options, int maxFiles, int numThreads, int batch, int batchSize, int min, int minSize, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TrySyncAsync(connection, options, maxFiles, numThreads, batch, batchSize, min, minSize, fileSpecs, cancellationToken).Select(x => x.Data); } /// /// Syncs files from the server /// /// Connection to the Perforce 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 /// Token used to cancel the operation /// Response from the server public static IAsyncEnumerable> TrySyncAsync(this IPerforceConnection connection, SyncOptions options, int maxFiles, int numThreads, int batch, int batchSize, int min, int minSize, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { // Perforce annoyingly returns 'up-to-date' as an error. Ignore it. IAsyncEnumerable> records = SyncInternalAsync(connection, options, maxFiles, numThreads, batch, batchSize, min, minSize, fileSpecs, false, cancellationToken); records = records.Where(x => x.Error == null || x.Error.Generic != PerforceGenericCode.Empty); return records; } /// /// Syncs files from the server /// /// Connection to the Perforce server /// Options for the command /// Syncs only the first number of files specified. /// Files to sync /// Token used to cancel the operation /// Response from the server public static async Task> SyncQuietAsync(this IPerforceConnection connection, SyncOptions options, int maxFiles, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { PerforceResponseList records = await TrySyncQuietAsync(connection, options, maxFiles, fileSpecs, cancellationToken); records.RemoveAll(x => (x.Error != null && x.Error.Generic == PerforceGenericCode.Empty) || x.Info != null); return records.Data; } /// /// Syncs files from the server /// /// Connection to the Perforce server /// Options for the command /// Syncs only the first number of files specified. /// Files to sync /// Token used to cancel the operation /// Response from the server public static Task> TrySyncQuietAsync(this IPerforceConnection connection, SyncOptions options, int maxFiles, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TrySyncQuietAsync(connection, options, maxFiles, -1, -1, -1, -1, -1, fileSpecs, cancellationToken); } /// /// Syncs files from the server without returning detailed file info /// /// Connection to the Perforce 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 /// Token used to cancel the operation /// Response from the server public static async Task> SyncQuietAsync(this IPerforceConnection connection, SyncOptions options, int maxFiles, int numThreads, int batch, int batchSize, int min, int minSize, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return (await TrySyncQuietAsync(connection, options, maxFiles, numThreads, batch, batchSize, min, minSize, fileSpecs, cancellationToken)).Data; } /// /// Syncs files from the server without returning detailed file info /// /// Connection to the Perforce 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 /// Token used to cancel the operation /// Response from the server public static async Task> TrySyncQuietAsync(this IPerforceConnection connection, SyncOptions options, int maxFiles, int numThreads, int batch, int batchSize, int min, int minSize, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return new PerforceResponseList(await SyncInternalAsync(connection, options, maxFiles, numThreads, batch, batchSize, min, minSize, fileSpecs, true, cancellationToken).ToListAsync(cancellationToken)); } /// /// Gets arguments for a sync command /// /// Connection to the Perforce 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 /// Whether to use quiet output /// Token used to cancel the operation /// Arguments for the command private static IAsyncEnumerable> SyncInternalAsync(this IPerforceConnection connection, SyncOptions options, int maxFiles, int numThreads, int batch, int batchSize, int min, int minSize, FileSpecList fileSpecs, bool quiet, CancellationToken cancellationToken = default) where T : class { List arguments = new List(); if ((options & SyncOptions.Force) != 0) { arguments.Add("-f"); } if ((options & SyncOptions.KeepWorkspaceFiles) != 0) { arguments.Add("-k"); } if ((options & SyncOptions.FullDepotSyntax) != 0) { arguments.Add("-L"); } if ((options & SyncOptions.PreviewOnly) != 0) { arguments.Add("-n"); } if ((options & SyncOptions.NetworkPreviewOnly) != 0) { arguments.Add("-N"); } if ((options & SyncOptions.DoNotUpdateHaveList) != 0) { arguments.Add("-p"); } if (quiet) { arguments.Add("-q"); } if ((options & SyncOptions.ReopenMovedFiles) != 0) { arguments.Add("-r"); } if ((options & SyncOptions.Safe) != 0) { arguments.Add("-s"); } if (maxFiles != -1) { arguments.Add($"-m{maxFiles}"); } // Using multiple threads is not supported through p4.exe due to threaded output not being parsable if (numThreads != -1 && (connection is NativePerforceConnection)) { StringBuilder argument = new StringBuilder($"--parallel=threads={numThreads}"); if (batch != -1) { argument.Append($",batch={batch}"); } if (batchSize != -1) { argument.Append($",batchsize={batchSize}"); } if (min != -1) { argument.Append($",min={min}"); } if (minSize != -1) { argument.Append($",minsize={minSize}"); } arguments.Add(argument.ToString()); } return StreamCommandAsync(connection, "sync", arguments, fileSpecs.List, null, cancellationToken); } #endregion #region p4 unshelve /// /// Restore shelved files from a pending change into a workspace /// /// Connection to the Perforce server /// 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 /// Token used to cancel the operation /// Response from the server public static async Task> UnshelveAsync(this IPerforceConnection connection, int changeNumber, int intoChangeNumber, string? usingBranchSpec, string? usingStream, string? forceParentStream, UnshelveOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return (await TryUnshelveAsync(connection, changeNumber, intoChangeNumber, usingBranchSpec, usingStream, forceParentStream, options, fileSpecs, cancellationToken)).Data; } /// /// Restore shelved files from a pending change into a workspace /// /// Connection to the Perforce server /// 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 /// Token used to cancel the operation /// Response from the server public static async Task> TryUnshelveAsync(this IPerforceConnection connection, int changeNumber, int intoChangeNumber, string? usingBranchSpec, string? usingStream, string? forceParentStream, UnshelveOptions options, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { List arguments = new List(); arguments.Add($"-s{changeNumber}"); if ((options & UnshelveOptions.ForceOverwrite) != 0) { arguments.Add("-f"); } if ((options & UnshelveOptions.PreviewOnly) != 0) { arguments.Add("-n"); } if (intoChangeNumber != -1) { arguments.Add($"-c{intoChangeNumber}"); } if (usingBranchSpec != null) { arguments.Add($"-b{usingBranchSpec}"); } if (usingStream != null) { arguments.Add($"-S{usingStream}"); } if (forceParentStream != null) { arguments.Add($"-P{forceParentStream}"); } arguments.AddRange(fileSpecs.List); PerforceResponseList records = await CommandAsync(connection, "unshelve", arguments, null, cancellationToken); records.RemoveAll(x => x.Error != null && x.Error.Generic == PerforceGenericCode.Empty); return records; } #endregion #region p4 user /// /// Enumerates all streams in a depot /// /// Connection to the Perforce server /// Name of the user to fetch information for /// Token used to cancel the operation /// Response from the server public static async Task GetUserAsync(this IPerforceConnection connection, string userName, CancellationToken cancellationToken = default) { return (await TryGetUserAsync(connection, userName, cancellationToken))[0].Data; } /// /// Enumerates all streams in a depot /// /// Connection to the Perforce server /// Name of the user to fetch information for /// Token used to cancel the operation /// Response from the server public static Task> TryGetUserAsync(this IPerforceConnection connection, string userName, CancellationToken cancellationToken = default) { List arguments = new List(); arguments.Add("-o"); arguments.Add(userName); return CommandAsync(connection, "user", arguments, null, cancellationToken); } #endregion #region p4 users /// /// Enumerates all streams in a depot /// /// Connection to the Perforce server /// Options for the command /// Maximum number of results to return /// Token used to cancel the operation /// Response from the server public static async Task> GetUsersAsync(this IPerforceConnection connection, UsersOptions options, int maxResults, CancellationToken cancellationToken = default) { return (await TryGetUsersAsync(connection, options, maxResults, cancellationToken)).Data; } /// /// Enumerates all streams in a depot /// /// Connection to the Perforce server /// Options for the command /// Maximum number of results to return /// Token used to cancel the operation /// Response from the server public static async Task> TryGetUsersAsync(this IPerforceConnection connection, UsersOptions options, int maxResults, CancellationToken cancellationToken = default) { List arguments = new List(); if ((options & UsersOptions.IncludeServiceUsers) != 0) { arguments.Add("-a"); } if ((options & UsersOptions.OnlyMasterServer) != 0) { arguments.Add("-c"); } if ((options & UsersOptions.IncludeLoginInfo) != 0) { arguments.Add("-l"); } if ((options & UsersOptions.OnlyReplicaServer) != 0) { arguments.Add("-r"); } if (maxResults > 0) { arguments.Add($"-m{maxResults}"); } PerforceResponseList records = await CommandAsync(connection, "users", arguments, null, cancellationToken); records.RemoveAll(x => x.Error != null && x.Error.Generic == PerforceGenericCode.Empty); return records; } #endregion #region p4 where /// /// Retrieves the location of a file of set of files in the workspace /// /// Connection to the Perforce server /// Patterns for the files to query /// Token used to cancel the operation /// List of responses from the server public static IAsyncEnumerable WhereAsync(this IPerforceConnection connection, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { return TryWhereAsync(connection, fileSpecs, cancellationToken).Select(x => x.Data); } /// /// Retrieves the location of a file of set of files in the workspace /// /// Connection to the Perforce server /// Patterns for the files to query /// Token used to cancel the operation /// List of responses from the server public static IAsyncEnumerable> TryWhereAsync(this IPerforceConnection connection, FileSpecList fileSpecs, CancellationToken cancellationToken = default) { IAsyncEnumerable> records = StreamCommandAsync(connection, "where", new List(), fileSpecs.List, null, cancellationToken); return records.Where(x => x.Error == null || x.Error.Generic != PerforceGenericCode.Empty); } #endregion #region p4 undo /// /// perform undo on a changelist (p4 undo -c [targetCL] //...@undoCL) /// /// Connection to the Perforce server /// Changelist number to undo /// Changelist number to receive the changes /// Token used to cancel the operation /// Response from the server public static async Task UndoChangeAsync(this IPerforceConnection connection, int changeNumberToUndo, int changeNumber, CancellationToken cancellationToken = default) { (await TryUndoChangeAsync(connection, changeNumberToUndo, changeNumber, cancellationToken))[0].EnsureSuccess(); } /// /// perform undo on a changelist (p4 undo -c [targetCL] //...@undoCL) /// /// Connection to the Perforce server /// Changelist number to undo /// Changelist number to receive the changes /// Token used to cancel the operation /// Response from the server public static async Task> TryUndoChangeAsync(this IPerforceConnection connection, int changeNumberToUndo, int changeNumber, CancellationToken cancellationToken = default) { List arguments = new List(); arguments.Add($"-c{changeNumber}"); arguments.Add($"//...@{changeNumberToUndo}"); return await CommandAsync(connection, "undo", arguments, null, cancellationToken); } #endregion #region p4 annotate /// /// Runs the annotate p4 command /// /// Connection to the Perforce server /// Depot path to the file /// Options for the anotate command /// Token used to cancel the operation /// List of annotate records public static async Task> AnnotateAsync(this IPerforceConnection connection, string fileSpec, AnnotateOptions options, CancellationToken cancellationToken = default) { return (await TryAnnotateAsync(connection, fileSpec, options, cancellationToken)).Data; } /// /// Runs the annotate p4 command /// /// Connection to the Perforce server /// Depot path to the file /// Options for the anotate command /// Token used to cancel the operation /// List of annotate records public static Task> TryAnnotateAsync(this IPerforceConnection connection, string fileSpec, AnnotateOptions options, CancellationToken cancellationToken = default) { List arguments = new List(); if ((options & AnnotateOptions.IncludeDeletedFilesAndLines) != 0) { arguments.Add("-a"); } if ((options & AnnotateOptions.IgnoreWhiteSpaceChanges) != 0) { arguments.Add("-db"); } if ((options & AnnotateOptions.OutputUserAndDate) != 0) { arguments.Add("-u"); } if ((options & AnnotateOptions.FollowIntegrations) != 0) { arguments.Add("-I"); } arguments.Add(fileSpec); return CommandAsync(connection, "annotate", arguments, null, cancellationToken); } #endregion #region Other /// /// Gets the current stream that a client is connected to /// /// Connection to the Perforce server /// Token used to cancel the operation /// public static async Task GetCurrentStreamAsync(this IPerforceConnection connection, CancellationToken cancellationToken = default) { return await TryGetCurrentStreamAsync(connection, cancellationToken); } /// /// Gets the current stream that a client is connected to /// /// Connection to the Perforce server /// Token used to cancel the operation /// public static async Task TryGetCurrentStreamAsync(this IPerforceConnection connection, CancellationToken cancellationToken = default) { PerforceResponse response = await connection.TryGetClientAsync(null, cancellationToken); if (response.Succeeded) { return response.Data.Stream; } else { return null; } } #endregion } }