You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
731 lines
26 KiB
C#
731 lines
26 KiB
C#
|
|
// Copyright Epic Games, Inc. All Rights Reserved.
|
||
|
|
|
||
|
|
using EpicGames.Core;
|
||
|
|
using EpicGames.Perforce;
|
||
|
|
using Microsoft.Extensions.Logging;
|
||
|
|
using Serilog;
|
||
|
|
using System;
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using System.Diagnostics;
|
||
|
|
using System.Globalization;
|
||
|
|
using System.Linq;
|
||
|
|
using System.Net;
|
||
|
|
using System.Runtime.InteropServices;
|
||
|
|
using System.Text.RegularExpressions;
|
||
|
|
using UnrealGameSync;
|
||
|
|
|
||
|
|
namespace UnrealGameSyncCmd
|
||
|
|
{
|
||
|
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||
|
|
|
||
|
|
sealed class UserErrorException : Exception
|
||
|
|
{
|
||
|
|
public LogEvent Event { get; }
|
||
|
|
public int Code { get; }
|
||
|
|
|
||
|
|
public UserErrorException(LogEvent Event)
|
||
|
|
: base(Event.ToString())
|
||
|
|
{
|
||
|
|
this.Event = Event;
|
||
|
|
this.Code = 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
public UserErrorException(string Message, params object[] Args)
|
||
|
|
: this(LogEvent.Create(LogLevel.Error, Message, Args))
|
||
|
|
{
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public class Program
|
||
|
|
{
|
||
|
|
class CommandInfo
|
||
|
|
{
|
||
|
|
public string Name { get; }
|
||
|
|
public Type Type { get; }
|
||
|
|
public string Usage { get; }
|
||
|
|
public string Brief { get; }
|
||
|
|
|
||
|
|
public CommandInfo(string Name, Type Type, string Usage, string Brief)
|
||
|
|
{
|
||
|
|
this.Name = Name;
|
||
|
|
this.Type = Type;
|
||
|
|
this.Usage = Usage;
|
||
|
|
this.Brief = Brief;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static CommandInfo[] Commands =
|
||
|
|
{
|
||
|
|
new CommandInfo("init", typeof(InitCommand),
|
||
|
|
"ugs init [stream-path] [-client=..] [-server=..] [-user=..] [-branch=..] [-project=..]",
|
||
|
|
"Create a client for the given stream, or initializes an existing client for use by UGS."
|
||
|
|
),
|
||
|
|
new CommandInfo("switch", typeof(SwitchCommand),
|
||
|
|
"ugs switch [project name|project path|stream]",
|
||
|
|
"Changes the active project to the one in the workspace with the given name, or switches to a new stream."
|
||
|
|
),
|
||
|
|
new CommandInfo("config", typeof(ConfigCommand),
|
||
|
|
"ugs config",
|
||
|
|
"Updates the configuration for the current workspace."
|
||
|
|
),
|
||
|
|
new CommandInfo("sync", typeof(SyncCommand),
|
||
|
|
"ugs sync [change|'latest'] [-build] [-only]",
|
||
|
|
"Syncs the current workspace to the given changelist, optionally removing all local state."
|
||
|
|
),
|
||
|
|
new CommandInfo("build", typeof(BuildCommand),
|
||
|
|
"ugs build [id] [-list]",
|
||
|
|
"Runs the default build steps for the current project, or a particular step referenced by id."
|
||
|
|
),
|
||
|
|
new CommandInfo("status", typeof(StatusCommand),
|
||
|
|
"ugs status [-update]",
|
||
|
|
"Shows the status of the currently synced branch."
|
||
|
|
)
|
||
|
|
};
|
||
|
|
|
||
|
|
class CommandContext
|
||
|
|
{
|
||
|
|
public CommandLineArguments Arguments { get; }
|
||
|
|
public ILogger Logger { get; }
|
||
|
|
public ILoggerFactory LoggerFactory { get; }
|
||
|
|
public UserSettings UserSettings { get; }
|
||
|
|
|
||
|
|
public CommandContext(CommandLineArguments Arguments, ILogger Logger, ILoggerFactory LoggerFactory, UserSettings UserSettings)
|
||
|
|
{
|
||
|
|
this.Arguments = Arguments;
|
||
|
|
this.Logger = Logger;
|
||
|
|
this.LoggerFactory = LoggerFactory;
|
||
|
|
this.UserSettings = UserSettings;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class ProjectConfigOptions
|
||
|
|
{
|
||
|
|
[CommandLine("-Server=")]
|
||
|
|
public string? ServerAndPort { get; set; }
|
||
|
|
|
||
|
|
[CommandLine("-User=")]
|
||
|
|
public string? UserName { get; set; }
|
||
|
|
|
||
|
|
public void ApplyTo(UserWorkspaceSettings Settings)
|
||
|
|
{
|
||
|
|
if (ServerAndPort != null)
|
||
|
|
{
|
||
|
|
Settings.ServerAndPort = (ServerAndPort.Length == 0) ? null : ServerAndPort;
|
||
|
|
}
|
||
|
|
if (UserName != null)
|
||
|
|
{
|
||
|
|
Settings.UserName = (UserName.Length == 0) ? null : UserName;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class ProjectInitOptions : ProjectConfigOptions
|
||
|
|
{
|
||
|
|
[CommandLine("-Client=")]
|
||
|
|
public string? ClientName { get; set; }
|
||
|
|
|
||
|
|
[CommandLine("-Branch=")]
|
||
|
|
public string? BranchPath { get; set; }
|
||
|
|
|
||
|
|
[CommandLine("-Project=")]
|
||
|
|
public string? ProjectName { get; set; }
|
||
|
|
}
|
||
|
|
|
||
|
|
public static async Task<int> Main(string[] RawArgs)
|
||
|
|
{
|
||
|
|
DirectoryReference GlobalConfigFolder = DirectoryReference.Combine(DirectoryReference.GetSpecialFolder(Environment.SpecialFolder.LocalApplicationData)!, "UnrealGameSync");
|
||
|
|
DirectoryReference LogFolder = GlobalConfigFolder;
|
||
|
|
|
||
|
|
Serilog.ILogger SerilogLogger = new LoggerConfiguration()
|
||
|
|
.Enrich.FromLogContext()
|
||
|
|
.WriteTo.Console(Serilog.Events.LogEventLevel.Information, outputTemplate: "{Message:lj}{NewLine}")
|
||
|
|
.WriteTo.File(FileReference.Combine(LogFolder, "ugs-cmd.txt").FullName, Serilog.Events.LogEventLevel.Debug, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true, fileSizeLimitBytes: 20 * 1024 * 1024, retainedFileCountLimit: 10)
|
||
|
|
.CreateLogger();
|
||
|
|
|
||
|
|
using ILoggerFactory LoggerFactory = new Serilog.Extensions.Logging.SerilogLoggerFactory(SerilogLogger, true);
|
||
|
|
ILogger Logger = LoggerFactory.CreateLogger("Main");
|
||
|
|
try
|
||
|
|
{
|
||
|
|
ConfigFile ConfigFile = new ConfigFile();
|
||
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||
|
|
{
|
||
|
|
ConfigFile.TryLoad(FileReference.Combine(GlobalConfigFolder, "UnrealGameSync.ini"), Logger);
|
||
|
|
}
|
||
|
|
|
||
|
|
UserSettings UserSettings = new UserSettings(null!, ConfigFile);
|
||
|
|
|
||
|
|
CommandLineArguments Args = new CommandLineArguments(RawArgs);
|
||
|
|
|
||
|
|
string? CommandName;
|
||
|
|
if (!Args.TryGetPositionalArgument(out CommandName))
|
||
|
|
{
|
||
|
|
PrintHelp();
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
CommandInfo? Command = Commands.FirstOrDefault(x => x.Name.Equals(CommandName, StringComparison.OrdinalIgnoreCase));
|
||
|
|
if (Command == null)
|
||
|
|
{
|
||
|
|
Logger.LogError($"unknown command '{CommandName}'");
|
||
|
|
Console.WriteLine();
|
||
|
|
PrintHelp();
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
Command Instance = (Command)Activator.CreateInstance(Command.Type)!;
|
||
|
|
await Instance.ExecuteAsync(new CommandContext(Args, Logger, LoggerFactory, UserSettings));
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
catch (UserErrorException Ex)
|
||
|
|
{
|
||
|
|
Logger.Log(Ex.Event.Level, "{Message}", Ex.Event.ToString());
|
||
|
|
return Ex.Code;
|
||
|
|
}
|
||
|
|
catch (PerforceException Ex)
|
||
|
|
{
|
||
|
|
Logger.LogError(Ex, "{Message}", Ex.Message);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
catch (Exception Ex)
|
||
|
|
{
|
||
|
|
Logger.LogError(Ex, "Unhandled exception.\n{Str}", Ex);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static void PrintHelp()
|
||
|
|
{
|
||
|
|
Console.WriteLine("Usage:");
|
||
|
|
foreach (CommandInfo Command in Commands)
|
||
|
|
{
|
||
|
|
Console.WriteLine();
|
||
|
|
ConsoleUtils.WriteLineWithWordWrap(Command.Usage, 2, 8);
|
||
|
|
ConsoleUtils.WriteLineWithWordWrap(Command.Brief, 4, 4);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public static UserWorkspaceSettings? ReadOptionalUserWorkspaceSettings()
|
||
|
|
{
|
||
|
|
DirectoryReference? Dir = DirectoryReference.GetCurrentDirectory();
|
||
|
|
for (; Dir != null; Dir = Dir.ParentDirectory)
|
||
|
|
{
|
||
|
|
UserWorkspaceSettings? Settings;
|
||
|
|
if (UserWorkspaceSettings.TryLoad(Dir, out Settings))
|
||
|
|
{
|
||
|
|
return Settings;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static UserWorkspaceSettings ReadRequiredUserWorkspaceSettings()
|
||
|
|
{
|
||
|
|
UserWorkspaceSettings? Settings = ReadOptionalUserWorkspaceSettings();
|
||
|
|
if (Settings == null)
|
||
|
|
{
|
||
|
|
throw new UserErrorException("Unable to find UGS workspace in current directory.");
|
||
|
|
}
|
||
|
|
return Settings;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static async Task<UserWorkspaceState> ReadWorkspaceState(IPerforceConnection PerforceClient, UserWorkspaceSettings Settings, UserSettings UserSettings, ILogger Logger)
|
||
|
|
{
|
||
|
|
UserWorkspaceState State = UserSettings.FindOrAddWorkspaceState(Settings);
|
||
|
|
if (State.SettingsTimeUtc != Settings.LastModifiedTimeUtc)
|
||
|
|
{
|
||
|
|
Logger.LogDebug("Updating state due to modified settings timestamp");
|
||
|
|
ProjectInfo Info = await ProjectInfo.CreateAsync(PerforceClient, Settings, CancellationToken.None);
|
||
|
|
State.UpdateCachedProjectInfo(Info, Settings.LastModifiedTimeUtc);
|
||
|
|
State.Save();
|
||
|
|
}
|
||
|
|
return State;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static Task<IPerforceConnection> ConnectAsync(string? ServerAndPort, string? UserName, string? ClientName, ILoggerFactory LoggerFactory)
|
||
|
|
{
|
||
|
|
PerforceSettings Settings = new PerforceSettings(PerforceSettings.Default);
|
||
|
|
Settings.ClientName = ClientName;
|
||
|
|
Settings.PreferNativeClient = true;
|
||
|
|
if (!String.IsNullOrEmpty(ServerAndPort))
|
||
|
|
{
|
||
|
|
Settings.ServerAndPort = ServerAndPort;
|
||
|
|
}
|
||
|
|
if (!String.IsNullOrEmpty(UserName))
|
||
|
|
{
|
||
|
|
Settings.UserName = UserName;
|
||
|
|
}
|
||
|
|
|
||
|
|
return PerforceConnection.CreateAsync(Settings, LoggerFactory.CreateLogger("Perforce"));
|
||
|
|
}
|
||
|
|
|
||
|
|
public static Task<IPerforceConnection> ConnectAsync(UserWorkspaceSettings Settings, ILoggerFactory LoggerFactory)
|
||
|
|
{
|
||
|
|
return ConnectAsync(Settings.ServerAndPort, Settings.UserName, Settings.ClientName, LoggerFactory);
|
||
|
|
}
|
||
|
|
|
||
|
|
static string[] ReadSyncFilter(UserWorkspaceSettings WorkspaceSettings, UserSettings UserSettings, ConfigFile ProjectConfig)
|
||
|
|
{
|
||
|
|
Dictionary<Guid, WorkspaceSyncCategory> SyncCategories = ConfigUtils.GetSyncCategories(ProjectConfig);
|
||
|
|
string[] CombinedSyncFilter = UserSettings.GetCombinedSyncFilter(SyncCategories, UserSettings.SyncView, UserSettings.SyncCategories, WorkspaceSettings.SyncView, WorkspaceSettings.SyncCategoriesDict);
|
||
|
|
|
||
|
|
ConfigSection PerforceSection = ProjectConfig.FindSection("Perforce");
|
||
|
|
if (PerforceSection != null)
|
||
|
|
{
|
||
|
|
IEnumerable<string> AdditionalPaths = PerforceSection.GetValues("AdditionalPathsToSync", new string[0]);
|
||
|
|
CombinedSyncFilter = AdditionalPaths.Union(CombinedSyncFilter).ToArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
return CombinedSyncFilter;
|
||
|
|
}
|
||
|
|
|
||
|
|
static async Task<string> FindProjectPathAsync(IPerforceConnection Perforce, string ClientName, string BranchPath, string? ProjectName)
|
||
|
|
{
|
||
|
|
using IPerforceConnection PerforceClient = await PerforceConnection.CreateAsync(new PerforceSettings(Perforce.Settings) { ClientName = ClientName }, Perforce.Logger);
|
||
|
|
|
||
|
|
// Find or validate the selected project
|
||
|
|
string SearchPath;
|
||
|
|
if (ProjectName == null)
|
||
|
|
{
|
||
|
|
SearchPath = $"//{ClientName}{BranchPath}/*.uprojectdirs";
|
||
|
|
}
|
||
|
|
else if (ProjectName.Contains('.'))
|
||
|
|
{
|
||
|
|
SearchPath = $"//{ClientName}{BranchPath}/{ProjectName.TrimStart('/')}";
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
SearchPath = $"//{ClientName}{BranchPath}/.../{ProjectName}.uproject";
|
||
|
|
}
|
||
|
|
|
||
|
|
List<FStatRecord> ProjectFileRecords = await PerforceClient.FStatAsync(FStatOptions.ClientFileInPerforceSyntax, SearchPath).ToListAsync();
|
||
|
|
ProjectFileRecords.RemoveAll(x => !x.IsMapped);
|
||
|
|
|
||
|
|
List<string> Paths = ProjectFileRecords.Select(x => PerforceUtils.GetClientRelativePath(x.ClientFile!)).Distinct(StringComparer.Ordinal).ToList();
|
||
|
|
if (Paths.Count == 0)
|
||
|
|
{
|
||
|
|
throw new UserErrorException("No project file found matching {SearchPath}", SearchPath);
|
||
|
|
}
|
||
|
|
if (Paths.Count > 1)
|
||
|
|
{
|
||
|
|
throw new UserErrorException("Multiple projects found matching {SearchPath}: {Paths}", SearchPath, String.Join(", ", Paths));
|
||
|
|
}
|
||
|
|
|
||
|
|
return "/" + Paths[0];
|
||
|
|
}
|
||
|
|
|
||
|
|
abstract class Command
|
||
|
|
{
|
||
|
|
public abstract Task ExecuteAsync(CommandContext Context);
|
||
|
|
}
|
||
|
|
|
||
|
|
class InitCommand : Command
|
||
|
|
{
|
||
|
|
public override async Task ExecuteAsync(CommandContext Context)
|
||
|
|
{
|
||
|
|
ILogger Logger = Context.Logger;
|
||
|
|
|
||
|
|
// Get the positional argument indicating the file to look for
|
||
|
|
string? InitName;
|
||
|
|
Context.Arguments.TryGetPositionalArgument(out InitName);
|
||
|
|
|
||
|
|
// Get the config settings from the command line
|
||
|
|
ProjectInitOptions Options = new ProjectInitOptions();
|
||
|
|
Context.Arguments.ApplyTo(Options);
|
||
|
|
Context.Arguments.CheckAllArgumentsUsed();
|
||
|
|
|
||
|
|
// Get the host name
|
||
|
|
using IPerforceConnection Perforce = await ConnectAsync(Options.ServerAndPort, Options.UserName, null, Context.LoggerFactory);
|
||
|
|
InfoRecord PerforceInfo = await Perforce.GetInfoAsync(InfoOptions.ShortOutput);
|
||
|
|
string HostName = PerforceInfo.ClientHost ?? Dns.GetHostName();
|
||
|
|
|
||
|
|
// Create the perforce connection
|
||
|
|
if (InitName != null)
|
||
|
|
{
|
||
|
|
await InitNewClientAsync(Perforce, Context, InitName, HostName, Options, Logger);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
await InitExistingClientAsync(Perforce, Context, HostName, Options, Logger);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async Task InitNewClientAsync(IPerforceConnection Perforce, CommandContext Context, string StreamName, string HostName, ProjectInitOptions Options, ILogger Logger)
|
||
|
|
{
|
||
|
|
Logger.LogInformation("Checking stream...");
|
||
|
|
|
||
|
|
// Get the given stream
|
||
|
|
PerforceResponse<StreamRecord> StreamResponse = await Perforce.TryGetStreamAsync(StreamName, true);
|
||
|
|
if (!StreamResponse.Succeeded)
|
||
|
|
{
|
||
|
|
throw new UserErrorException($"Unable to find stream '{StreamName}'");
|
||
|
|
}
|
||
|
|
StreamRecord Stream = StreamResponse.Data;
|
||
|
|
|
||
|
|
// Get the new directory for the client
|
||
|
|
DirectoryReference ClientDir = DirectoryReference.Combine(DirectoryReference.GetCurrentDirectory(), Stream.Stream.Replace('/', '+'));
|
||
|
|
DirectoryReference.CreateDirectory(ClientDir);
|
||
|
|
|
||
|
|
// Make up a new client name
|
||
|
|
string ClientName = Options.ClientName ?? Regex.Replace($"{Perforce.Settings.UserName}_{HostName}_{Stream.Stream.Trim('/')}", "[^0-9a-zA-Z_.-]", "+");
|
||
|
|
|
||
|
|
// Check there are no existing clients under the current path
|
||
|
|
List<ClientsRecord> Clients = await FindExistingClients(Perforce, HostName, ClientDir);
|
||
|
|
if (Clients.Count > 0)
|
||
|
|
{
|
||
|
|
if (Clients.Count == 1 && ClientName.Equals(Clients[0].Name, StringComparison.OrdinalIgnoreCase) && ClientDir == TryParseRoot(Clients[0].Root))
|
||
|
|
{
|
||
|
|
Logger.LogInformation("Reusing existing client for {ClientDir} ({ClientName})", ClientDir, Options.ClientName);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
throw new UserErrorException("Current directory is already within a Perforce workspace ({ClientName})", Clients[0].Name);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create the new client
|
||
|
|
ClientRecord Client = new ClientRecord(ClientName, Perforce.Settings.UserName, ClientDir.FullName);
|
||
|
|
Client.Host = HostName;
|
||
|
|
Client.Stream = Stream.Stream;
|
||
|
|
Client.Options = ClientOptions.Rmdir;
|
||
|
|
await Perforce.CreateClientAsync(Client);
|
||
|
|
|
||
|
|
// Branch root is currently hard-coded at the root
|
||
|
|
string BranchPath = Options.BranchPath ?? String.Empty;
|
||
|
|
string ProjectPath = await FindProjectPathAsync(Perforce, ClientName, BranchPath, Options.ProjectName);
|
||
|
|
|
||
|
|
// Create the settings object
|
||
|
|
UserWorkspaceSettings Settings = new UserWorkspaceSettings();
|
||
|
|
Settings.RootDir = ClientDir;
|
||
|
|
Settings.Init(Perforce.Settings.ServerAndPort, Perforce.Settings.UserName, ClientName, BranchPath, ProjectPath);
|
||
|
|
Options.ApplyTo(Settings);
|
||
|
|
Settings.Save();
|
||
|
|
|
||
|
|
Logger.LogInformation("Initialized {ClientName} with root at {RootDir}", ClientName, ClientDir);
|
||
|
|
}
|
||
|
|
|
||
|
|
static DirectoryReference? TryParseRoot(string Root)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
return new DirectoryReference(Root);
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async Task InitExistingClientAsync(IPerforceConnection Perforce, CommandContext Context, string HostName, ProjectInitOptions Options, ILogger Logger)
|
||
|
|
{
|
||
|
|
DirectoryReference CurrentDir = DirectoryReference.GetCurrentDirectory();
|
||
|
|
|
||
|
|
// Make sure the client name is set
|
||
|
|
string? ClientName = Options.ClientName;
|
||
|
|
if (ClientName == null)
|
||
|
|
{
|
||
|
|
List<ClientsRecord> Clients = await FindExistingClients(Perforce, HostName, CurrentDir);
|
||
|
|
if (Clients.Count == 0)
|
||
|
|
{
|
||
|
|
throw new UserErrorException("Unable to find client for {HostName} under {ClientDir}", HostName, CurrentDir);
|
||
|
|
}
|
||
|
|
if (Clients.Count > 1)
|
||
|
|
{
|
||
|
|
throw new UserErrorException("Multiple clients found for {HostName} under {ClientDir}: {ClientList}", HostName, CurrentDir, String.Join(", ", Clients.Select(x => x.Name)));
|
||
|
|
}
|
||
|
|
|
||
|
|
ClientName = Clients[0].Name;
|
||
|
|
Logger.LogInformation("Found client {ClientName}", ClientName);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get the client info
|
||
|
|
ClientRecord Client = await Perforce.GetClientAsync(Options.ClientName);
|
||
|
|
DirectoryReference ClientDir = new DirectoryReference(Client.Root);
|
||
|
|
|
||
|
|
// If a project path was specified in local syntax, try to convert it to client-relative syntax
|
||
|
|
string? ProjectName = Options.ProjectName;
|
||
|
|
if (Options.ProjectName != null && Options.ProjectName.Contains('.'))
|
||
|
|
{
|
||
|
|
Options.ProjectName = FileReference.Combine(CurrentDir, Options.ProjectName).MakeRelativeTo(ClientDir).Replace('\\', '/');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Branch root is currently hard-coded at the root
|
||
|
|
string BranchPath = Options.BranchPath ?? String.Empty;
|
||
|
|
string ProjectPath = await FindProjectPathAsync(Perforce, ClientName, BranchPath, ProjectName);
|
||
|
|
|
||
|
|
// Create the settings object
|
||
|
|
UserWorkspaceSettings Settings = new UserWorkspaceSettings();
|
||
|
|
Settings.RootDir = ClientDir;
|
||
|
|
Settings.Init(Perforce.Settings.ServerAndPort, Perforce.Settings.UserName, ClientName, BranchPath, ProjectPath);
|
||
|
|
Options.ApplyTo(Settings);
|
||
|
|
Settings.Save();
|
||
|
|
|
||
|
|
Logger.LogInformation("Initialized workspace at {RootDir} for {ClientProject}", ClientDir, Settings.ClientProjectPath);
|
||
|
|
}
|
||
|
|
|
||
|
|
static async Task<List<ClientsRecord>> FindExistingClients(IPerforceConnection Perforce, string HostName, DirectoryReference ClientDir)
|
||
|
|
{
|
||
|
|
List<ClientsRecord> MatchingClients = new List<ClientsRecord>();
|
||
|
|
|
||
|
|
List<ClientsRecord> Clients = await Perforce.GetClientsAsync(ClientsOptions.None, Perforce.Settings.UserName);
|
||
|
|
foreach (ClientsRecord Client in Clients)
|
||
|
|
{
|
||
|
|
if (!String.IsNullOrEmpty(Client.Root) && !String.IsNullOrEmpty(Client.Host) && String.Compare(HostName, Client.Host, StringComparison.OrdinalIgnoreCase) == 0)
|
||
|
|
{
|
||
|
|
DirectoryReference? RootDir;
|
||
|
|
try
|
||
|
|
{
|
||
|
|
RootDir = new DirectoryReference(Client.Root);
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
RootDir = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (RootDir != null && ClientDir.IsUnderDirectory(RootDir))
|
||
|
|
{
|
||
|
|
MatchingClients.Add(Client);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return MatchingClients;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class SyncCommand : Command
|
||
|
|
{
|
||
|
|
class SyncOptions
|
||
|
|
{
|
||
|
|
[CommandLine("-Only")]
|
||
|
|
public bool SingleChange { get; set; }
|
||
|
|
|
||
|
|
[CommandLine("-Build")]
|
||
|
|
public bool Build { get; set; }
|
||
|
|
}
|
||
|
|
|
||
|
|
public override async Task ExecuteAsync(CommandContext Context)
|
||
|
|
{
|
||
|
|
ILogger Logger = Context.Logger;
|
||
|
|
Context.Arguments.TryGetPositionalArgument(out string? ChangeString);
|
||
|
|
|
||
|
|
SyncOptions SyncOptions = new SyncOptions();
|
||
|
|
Context.Arguments.ApplyTo(SyncOptions);
|
||
|
|
|
||
|
|
Context.Arguments.CheckAllArgumentsUsed();
|
||
|
|
|
||
|
|
UserWorkspaceSettings Settings = ReadRequiredUserWorkspaceSettings();
|
||
|
|
using IPerforceConnection PerforceClient = await ConnectAsync(Settings, Context.LoggerFactory);
|
||
|
|
UserWorkspaceState State = await ReadWorkspaceState(PerforceClient, Settings, Context.UserSettings, Logger);
|
||
|
|
|
||
|
|
ChangeString ??= "latest";
|
||
|
|
|
||
|
|
int Change;
|
||
|
|
if (!int.TryParse(ChangeString, out Change))
|
||
|
|
{
|
||
|
|
if (String.Equals(ChangeString, "latest", StringComparison.OrdinalIgnoreCase))
|
||
|
|
{
|
||
|
|
List<ChangesRecord> Changes = await PerforceClient.GetChangesAsync(ChangesOptions.None, 1, ChangeStatus.Submitted, $"//{Settings.ClientName}/...");
|
||
|
|
Change = Changes[0].Number;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
throw new UserErrorException("Unknown change type for sync '{Change}'", ChangeString);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
WorkspaceUpdateOptions Options = SyncOptions.SingleChange? WorkspaceUpdateOptions.SyncSingleChange : WorkspaceUpdateOptions.Sync;
|
||
|
|
if (SyncOptions.Build)
|
||
|
|
{
|
||
|
|
Options |= WorkspaceUpdateOptions.Build;
|
||
|
|
}
|
||
|
|
|
||
|
|
ProjectInfo ProjectInfo = State.CreateProjectInfo();
|
||
|
|
UserProjectSettings ProjectSettings = Context.UserSettings.FindOrAddProjectSettings(ProjectInfo);
|
||
|
|
|
||
|
|
ConfigFile ProjectConfig = await ConfigUtils.ReadProjectConfigFileAsync(PerforceClient, ProjectInfo, Logger, CancellationToken.None);
|
||
|
|
string[] SyncFilter = ReadSyncFilter(Settings, Context.UserSettings, ProjectConfig);
|
||
|
|
|
||
|
|
WorkspaceUpdateContext UpdateContext = new WorkspaceUpdateContext(Change, Options, BuildConfig.Development, SyncFilter, ProjectSettings.BuildSteps, null);
|
||
|
|
|
||
|
|
WorkspaceUpdate Update = new WorkspaceUpdate(UpdateContext);
|
||
|
|
await Update.ExecuteAsync(PerforceClient.Settings, ProjectInfo, State, Context.Logger, CancellationToken.None);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class ConfigCommand : Command
|
||
|
|
{
|
||
|
|
public override async Task ExecuteAsync(CommandContext Context)
|
||
|
|
{
|
||
|
|
ILogger Logger = Context.Logger;
|
||
|
|
|
||
|
|
UserWorkspaceSettings Settings = ReadRequiredUserWorkspaceSettings();
|
||
|
|
if (!Context.Arguments.GetUnusedArguments().Any())
|
||
|
|
{
|
||
|
|
ProcessStartInfo StartInfo = new ProcessStartInfo();
|
||
|
|
StartInfo.FileName = Settings.ConfigFile.FullName;
|
||
|
|
StartInfo.UseShellExecute = true;
|
||
|
|
using (Process? Editor = Process.Start(StartInfo))
|
||
|
|
{
|
||
|
|
if (Editor != null)
|
||
|
|
{
|
||
|
|
await Editor.WaitForExitAsync();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
ProjectConfigOptions Options = new ProjectConfigOptions();
|
||
|
|
Context.Arguments.ApplyTo(Options);
|
||
|
|
Context.Arguments.CheckAllArgumentsUsed(Context.Logger);
|
||
|
|
|
||
|
|
Options.ApplyTo(Settings);
|
||
|
|
Settings.Save();
|
||
|
|
|
||
|
|
Logger.LogInformation("Updated {ConfigFile}", Settings.ConfigFile);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class BuildCommand : Command
|
||
|
|
{
|
||
|
|
public override async Task ExecuteAsync(CommandContext Context)
|
||
|
|
{
|
||
|
|
ILogger Logger = Context.Logger;
|
||
|
|
Context.Arguments.TryGetPositionalArgument(out string? Target);
|
||
|
|
bool ListOnly = Context.Arguments.HasOption("-List");
|
||
|
|
Context.Arguments.CheckAllArgumentsUsed();
|
||
|
|
|
||
|
|
UserWorkspaceSettings Settings = ReadRequiredUserWorkspaceSettings();
|
||
|
|
using IPerforceConnection PerforceClient = await ConnectAsync(Settings, Context.LoggerFactory);
|
||
|
|
UserWorkspaceState State = await ReadWorkspaceState(PerforceClient, Settings, Context.UserSettings, Logger);
|
||
|
|
|
||
|
|
ProjectInfo ProjectInfo = State.CreateProjectInfo();
|
||
|
|
|
||
|
|
if (ListOnly)
|
||
|
|
{
|
||
|
|
ConfigFile ProjectConfig = await ConfigUtils.ReadProjectConfigFileAsync(PerforceClient, ProjectInfo, Logger, CancellationToken.None);
|
||
|
|
|
||
|
|
FileReference EditorTarget = ConfigUtils.GetEditorTargetFile(ProjectInfo, ProjectConfig);
|
||
|
|
|
||
|
|
Dictionary<Guid, ConfigObject> BuildStepObjects = ConfigUtils.GetDefaultBuildStepObjects(ProjectInfo, EditorTarget.GetFileNameWithoutAnyExtensions(), BuildConfig.Development, ProjectConfig, false);
|
||
|
|
|
||
|
|
Logger.LogInformation("Available build steps:");
|
||
|
|
Logger.LogInformation("");
|
||
|
|
Logger.LogInformation(" Id | Description | Type | Enabled");
|
||
|
|
Logger.LogInformation(" -------------------------------------|------------------------------------------|------------|-----------------");
|
||
|
|
foreach (BuildStep BuildStep in BuildStepObjects.Values.Select(x => new BuildStep(x)).OrderBy(x => x.OrderIndex))
|
||
|
|
{
|
||
|
|
Logger.LogInformation(" {Id,-36} | {Name,-40} | {Type,-10} | {Enabled,-8}", BuildStep.UniqueId, BuildStep.Description, BuildStep.Type, BuildStep.bNormalSync);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
HashSet<Guid>? Steps = null;
|
||
|
|
if (Target != null)
|
||
|
|
{
|
||
|
|
Guid Id;
|
||
|
|
if (!Guid.TryParse(Target, out Id))
|
||
|
|
{
|
||
|
|
Logger.LogError("Unable to parse '{Target}' as a GUID. Pass -List to show all available build steps and their identifiers.", Target);
|
||
|
|
}
|
||
|
|
Steps = new HashSet<Guid> { Id };
|
||
|
|
}
|
||
|
|
|
||
|
|
WorkspaceUpdateContext UpdateContext = new WorkspaceUpdateContext(State.CurrentChangeNumber, WorkspaceUpdateOptions.Build, BuildConfig.Development, null, new List<ConfigObject>(), Steps);
|
||
|
|
|
||
|
|
WorkspaceUpdate Update = new WorkspaceUpdate(UpdateContext);
|
||
|
|
await Update.ExecuteAsync(PerforceClient.Settings, ProjectInfo, State, Context.Logger, CancellationToken.None);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class StatusCommand : Command
|
||
|
|
{
|
||
|
|
public override async Task ExecuteAsync(CommandContext Context)
|
||
|
|
{
|
||
|
|
ILogger Logger = Context.Logger;
|
||
|
|
bool bUpdate = Context.Arguments.HasOption("-Update");
|
||
|
|
Context.Arguments.CheckAllArgumentsUsed();
|
||
|
|
|
||
|
|
UserWorkspaceSettings Settings = ReadRequiredUserWorkspaceSettings();
|
||
|
|
Logger.LogInformation("User: {UserName}", Settings.UserName);
|
||
|
|
Logger.LogInformation("Server: {ServerAndPort}", Settings.ServerAndPort);
|
||
|
|
Logger.LogInformation("Project: {ClientProjectPath}", Settings.ClientProjectPath);
|
||
|
|
|
||
|
|
using IPerforceConnection PerforceClient = await ConnectAsync(Settings, Context.LoggerFactory);
|
||
|
|
|
||
|
|
UserWorkspaceState State = await ReadWorkspaceState(PerforceClient, Settings, Context.UserSettings, Logger);
|
||
|
|
if (bUpdate)
|
||
|
|
{
|
||
|
|
ProjectInfo NewProjectInfo = await ProjectInfo.CreateAsync(PerforceClient, Settings, CancellationToken.None);
|
||
|
|
State.UpdateCachedProjectInfo(NewProjectInfo, Settings.LastModifiedTimeUtc);
|
||
|
|
}
|
||
|
|
|
||
|
|
string StreamOrBranchName = State.StreamName ?? Settings.BranchPath.TrimStart('/');
|
||
|
|
if (State.LastSyncResultMessage == null)
|
||
|
|
{
|
||
|
|
Logger.LogInformation("Not currently synced to {Stream}", StreamOrBranchName);
|
||
|
|
}
|
||
|
|
else if (State.LastSyncResult == WorkspaceUpdateResult.Success)
|
||
|
|
{
|
||
|
|
Logger.LogInformation("Synced to {Stream} CL {Change}", StreamOrBranchName, State.LastSyncChangeNumber);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
Logger.LogWarning("Last sync to {Stream} CL {Change} failed: {Result}", StreamOrBranchName, State.LastSyncChangeNumber, State.LastSyncResultMessage);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class SwitchCommand : Command
|
||
|
|
{
|
||
|
|
public override async Task ExecuteAsync(CommandContext Context)
|
||
|
|
{
|
||
|
|
// Get the positional argument indicating the file to look for
|
||
|
|
string? TargetName;
|
||
|
|
if (!Context.Arguments.TryGetPositionalArgument(out TargetName))
|
||
|
|
{
|
||
|
|
throw new UserErrorException("Missing stream or project name to switch to.");
|
||
|
|
}
|
||
|
|
|
||
|
|
bool Force = TargetName.StartsWith("//", StringComparison.Ordinal) && Context.Arguments.HasOption("-Force");
|
||
|
|
|
||
|
|
// Finish argument parsing
|
||
|
|
Context.Arguments.CheckAllArgumentsUsed();
|
||
|
|
|
||
|
|
// Get a connection to the client for this workspace
|
||
|
|
UserWorkspaceSettings Settings = ReadRequiredUserWorkspaceSettings();
|
||
|
|
using IPerforceConnection PerforceClient = await ConnectAsync(Settings, Context.LoggerFactory);
|
||
|
|
|
||
|
|
// Check whether we're switching stream or project
|
||
|
|
if (TargetName.StartsWith("//", StringComparison.Ordinal))
|
||
|
|
{
|
||
|
|
await SwitchStreamAsync(PerforceClient, TargetName, Force, Context.Logger);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
await SwitchProjectAsync(PerforceClient, Settings, TargetName, Context.Logger);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task SwitchStreamAsync(IPerforceConnection PerforceClient, string StreamName, bool Force, ILogger Logger)
|
||
|
|
{
|
||
|
|
if (!Force && await PerforceClient.OpenedAsync(OpenedOptions.None, FileSpecList.Any).AnyAsync())
|
||
|
|
{
|
||
|
|
throw new UserErrorException("Client {ClientName} has files opened. Use -Force to switch anyway.", PerforceClient.Settings.ClientName!);
|
||
|
|
}
|
||
|
|
|
||
|
|
await PerforceClient.SwitchClientToStreamAsync(PerforceClient.Settings.ClientName!, StreamName, SwitchClientOptions.IgnoreOpenFiles);
|
||
|
|
|
||
|
|
Logger.LogInformation("Switched to stream {StreamName}", StreamName);
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task SwitchProjectAsync(IPerforceConnection PerforceClient, UserWorkspaceSettings Settings, string ProjectName, ILogger Logger)
|
||
|
|
{
|
||
|
|
Settings.ProjectPath = await FindProjectPathAsync(PerforceClient, Settings.ClientName, Settings.BranchPath, ProjectName);
|
||
|
|
Settings.Save();
|
||
|
|
Logger.LogInformation("Switched to project {ProjectPath}", Settings.ClientProjectPath);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|