diff --git a/build/Version.props b/build/Version.props
index 74c77f27a7..15258f13a1 100644
--- a/build/Version.props
+++ b/build/Version.props
@@ -3,12 +3,12 @@
- 6.8.0
- 5.1.0
- 10.6.0
+ 6.9.0
+ 5.2.0
+ 10.7.0
7.0.0
- 13.6.0
- 15.6.0
+ 13.7.0
+ 16.0.0
7.2.0
5.9.0
1.4.1
diff --git a/src/Tgstation.Server.Api/Models/JobCode.cs b/src/Tgstation.Server.Api/Models/JobCode.cs
index be5db8d5a6..1c20107b59 100644
--- a/src/Tgstation.Server.Api/Models/JobCode.cs
+++ b/src/Tgstation.Server.Api/Models/JobCode.cs
@@ -108,5 +108,11 @@ namespace Tgstation.Server.Api.Models
///
[Description("Reconnect chat bot")]
ReconnectChatBot,
+
+ ///
+ /// When a repository is recloned.
+ ///
+ [Description("Reclone repository")]
+ RepositoryReclone,
}
}
diff --git a/src/Tgstation.Server.Api/Models/Response/AdministrationResponse.cs b/src/Tgstation.Server.Api/Models/Response/AdministrationResponse.cs
index 55d7f646e5..2db5d354a7 100644
--- a/src/Tgstation.Server.Api/Models/Response/AdministrationResponse.cs
+++ b/src/Tgstation.Server.Api/Models/Response/AdministrationResponse.cs
@@ -16,5 +16,10 @@ namespace Tgstation.Server.Api.Models.Response
/// The latest available version of the Tgstation.Server.Host assembly from the upstream repository. If is not equal to 4 the update cannot be applied due to API changes.
///
public Version? LatestVersion { get; set; }
+
+ ///
+ /// This response is cached. This field indicates the when it was generated.
+ ///
+ public DateTimeOffset? GeneratedAt { get; set; }
}
}
diff --git a/src/Tgstation.Server.Api/Models/Response/DreamDaemonResponse.cs b/src/Tgstation.Server.Api/Models/Response/DreamDaemonResponse.cs
index cad5094d1a..d0b86cdaa4 100644
--- a/src/Tgstation.Server.Api/Models/Response/DreamDaemonResponse.cs
+++ b/src/Tgstation.Server.Api/Models/Response/DreamDaemonResponse.cs
@@ -59,5 +59,11 @@ namespace Tgstation.Server.Api.Models.Response
///
[ResponseOptions]
public bool? CurrentAllowWebclient { get; set; }
+
+ ///
+ /// The amount of RAM in use by the game server in bytes.
+ ///
+ [ResponseOptions]
+ public long? ImmediateMemoryUsage { get; set; }
}
}
diff --git a/src/Tgstation.Server.Api/Rights/RepositoryRights.cs b/src/Tgstation.Server.Api/Rights/RepositoryRights.cs
index f9d134820b..407437ce5d 100644
--- a/src/Tgstation.Server.Api/Rights/RepositoryRights.cs
+++ b/src/Tgstation.Server.Api/Rights/RepositoryRights.cs
@@ -82,5 +82,10 @@ namespace Tgstation.Server.Api.Rights
/// User may change submodule update settings.
///
ChangeSubmoduleUpdate = 1 << 13,
+
+ ///
+ /// User may trigger repository recloning.
+ ///
+ Reclone = 1 << 14,
}
}
diff --git a/src/Tgstation.Server.Client/AdministrationClient.cs b/src/Tgstation.Server.Client/AdministrationClient.cs
index 2d6841795c..68c7fecd34 100644
--- a/src/Tgstation.Server.Client/AdministrationClient.cs
+++ b/src/Tgstation.Server.Client/AdministrationClient.cs
@@ -24,7 +24,7 @@ namespace Tgstation.Server.Client
}
///
- public ValueTask Read(CancellationToken cancellationToken) => ApiClient.Read(Routes.Administration, cancellationToken);
+ public ValueTask Read(bool forceFresh, CancellationToken cancellationToken) => ApiClient.Read($"{Routes.Administration}?fresh={forceFresh}", cancellationToken);
///
public async ValueTask Update(
diff --git a/src/Tgstation.Server.Client/Components/DreamDaemonClient.cs b/src/Tgstation.Server.Client/Components/DreamDaemonClient.cs
index cf4eb3ea87..1ea2dccb3b 100644
--- a/src/Tgstation.Server.Client/Components/DreamDaemonClient.cs
+++ b/src/Tgstation.Server.Client/Components/DreamDaemonClient.cs
@@ -43,7 +43,10 @@ namespace Tgstation.Server.Client.Components
public ValueTask Restart(CancellationToken cancellationToken) => apiClient.Patch(Routes.DreamDaemon, instance.Id!.Value, cancellationToken);
///
- public ValueTask Read(CancellationToken cancellationToken) => apiClient.Read(Routes.DreamDaemon, instance.Id!.Value, cancellationToken);
+ public ValueTask Read(CancellationToken cancellationToken) => apiClient.Read(
+ Routes.DreamDaemon,
+ instance.Id!.Value,
+ cancellationToken);
///
public ValueTask Update(DreamDaemonRequest dreamDaemon, CancellationToken cancellationToken) => apiClient.Update(Routes.DreamDaemon, dreamDaemon ?? throw new ArgumentNullException(nameof(dreamDaemon)), instance.Id!.Value, cancellationToken);
diff --git a/src/Tgstation.Server.Client/Components/IDreamDaemonClient.cs b/src/Tgstation.Server.Client/Components/IDreamDaemonClient.cs
index 7a7a6ead7c..823c8d6a0c 100644
--- a/src/Tgstation.Server.Client/Components/IDreamDaemonClient.cs
+++ b/src/Tgstation.Server.Client/Components/IDreamDaemonClient.cs
@@ -16,7 +16,7 @@ namespace Tgstation.Server.Client.Components
///
/// The for the operation.
/// A resulting in the information.
- ValueTask Read(CancellationToken cancellationToken);
+ ValueTask Read(CancellationToken cancellationToken = default);
///
/// Start .
diff --git a/src/Tgstation.Server.Client/Components/IRepositoryClient.cs b/src/Tgstation.Server.Client/Components/IRepositoryClient.cs
index 9d3bef806c..51d8bdfd80 100644
--- a/src/Tgstation.Server.Client/Components/IRepositoryClient.cs
+++ b/src/Tgstation.Server.Client/Components/IRepositoryClient.cs
@@ -40,5 +40,12 @@ namespace Tgstation.Server.Client.Components
/// The for the operation.
/// A resulting in the .
ValueTask Delete(CancellationToken cancellationToken);
+
+ ///
+ /// Deletes and reclones the repository.
+ ///
+ /// The for the operation.
+ /// A resulting in the .
+ ValueTask Reclone(CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Client/Components/RepositoryClient.cs b/src/Tgstation.Server.Client/Components/RepositoryClient.cs
index 124f1ec5b2..5711612024 100644
--- a/src/Tgstation.Server.Client/Components/RepositoryClient.cs
+++ b/src/Tgstation.Server.Client/Components/RepositoryClient.cs
@@ -44,5 +44,8 @@ namespace Tgstation.Server.Client.Components
///
public ValueTask Update(RepositoryUpdateRequest repository, CancellationToken cancellationToken) => apiClient.Update(Routes.Repository, repository ?? throw new ArgumentNullException(nameof(repository)), instance.Id!.Value, cancellationToken);
+
+ ///
+ public ValueTask Reclone(CancellationToken cancellationToken) => apiClient.Patch(Routes.Repository, instance.Id!.Value, cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Client/IAdministrationClient.cs b/src/Tgstation.Server.Client/IAdministrationClient.cs
index 38d0898345..6d5d88f951 100644
--- a/src/Tgstation.Server.Client/IAdministrationClient.cs
+++ b/src/Tgstation.Server.Client/IAdministrationClient.cs
@@ -17,9 +17,10 @@ namespace Tgstation.Server.Client
///
/// Get the represented by the .
///
+ /// If the response will be forcefully regenerated.
/// The for the operation.
/// A resulting in the represented by the .
- ValueTask Read(CancellationToken cancellationToken);
+ ValueTask Read(bool forceFresh = false, CancellationToken cancellationToken = default);
///
/// Updates the setttings.
diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/RevisionCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/RevisionCommand.cs
index 39ed128d59..1bd2a8b91e 100644
--- a/src/Tgstation.Server.Host/Components/Chat/Commands/RevisionCommand.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/Commands/RevisionCommand.cs
@@ -50,7 +50,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands
public async ValueTask Invoke(string arguments, ChatUser user, CancellationToken cancellationToken)
{
string result;
- if (arguments.Split(' ').Any(x => x.ToUpperInvariant() == "--REPO"))
+ if (arguments.Split(' ').Any(x => x.Equals("--repo", StringComparison.OrdinalIgnoreCase)))
{
if (repositoryManager.CloneInProgress || repositoryManager.InUse)
return new MessageContent
@@ -58,15 +58,13 @@ namespace Tgstation.Server.Host.Components.Chat.Commands
Text = "Repository busy! Try again later",
};
- using (var repo = await repositoryManager.LoadRepository(cancellationToken))
- {
- if (repo == null)
- return new MessageContent
- {
- Text = "Repository unavailable!",
- };
- result = repo.Head;
- }
+ using var repo = await repositoryManager.LoadRepository(cancellationToken);
+ if (repo == null)
+ return new MessageContent
+ {
+ Text = "Repository unavailable!",
+ };
+ result = repo.Head;
}
else
{
diff --git a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs
index e552d16b40..608d3addb5 100644
--- a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs
+++ b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs
@@ -157,6 +157,8 @@ namespace Tgstation.Server.Host.Components.Engine
{
Logger.LogTrace("OD repo seems to already exist, attempting load and fetch...");
repo = await repositoryManager.LoadRepository(cancellationToken);
+ if (repo == null)
+ throw new JobException("Can't load OpenDream repository! Please delete cache from disk!");
await repo!.FetchOrigin(
progressSection1,
@@ -176,6 +178,7 @@ namespace Tgstation.Server.Host.Components.Engine
null,
null,
true,
+ false,
progressSection2,
cancellationToken);
diff --git a/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs
index 35bcbf63dc..ce2c7704e2 100644
--- a/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs
+++ b/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs
@@ -224,7 +224,7 @@ namespace Tgstation.Server.Host.Components.Engine
///
protected override string GetDreamDaemonName(Version byondVersion, out bool supportsCli)
{
- supportsCli = byondVersion >= DDExeVersion;
+ supportsCli = byondVersion >= DDExeVersion && !sessionConfiguration.ForceUseDreamDaemonExe;
return supportsCli ? "dd.exe" : "dreamdaemon.exe";
}
diff --git a/src/Tgstation.Server.Host/Components/Events/EventType.cs b/src/Tgstation.Server.Host/Components/Events/EventType.cs
index 5d8c0b9054..711d6ca246 100644
--- a/src/Tgstation.Server.Host/Components/Events/EventType.cs
+++ b/src/Tgstation.Server.Host/Components/Events/EventType.cs
@@ -12,7 +12,7 @@
RepoResetOrigin,
///
- /// Parameters: Checkout target.
+ /// Parameters: Checkout target, hard reset flag (If "True", this is actually a hard reset, not a checkout).
///
[EventScript("RepoCheckout")]
RepoCheckout,
diff --git a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs
index 65bcd1d61e..e736c4873e 100644
--- a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs
+++ b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs
@@ -47,6 +47,7 @@ namespace Tgstation.Server.Host.Components.Repository
/// The optional username used for fetching from submodule repositories.
/// The optional password used for fetching from submodule repositories.
/// If a submodule update should be attempted after the merge.
+ /// If a hard reset to the target committish should be performed instead of a checkout.
/// The optional to report progress of the operation.
/// The for the operation.
/// A representing the running operation.
@@ -55,6 +56,7 @@ namespace Tgstation.Server.Host.Components.Repository
string? username,
string? password,
bool updateSubmodules,
+ bool moveCurrentReference,
JobProgressReporter? progressReporter,
CancellationToken cancellationToken);
diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs
index 972fae31cc..6873d42957 100644
--- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs
+++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs
@@ -295,7 +295,7 @@ namespace Tgstation.Server.Host.Components.Repository
var revertTo = originalCommit.CanonicalName ?? originalCommit.Tip.Sha;
logger.LogDebug("Merge conflict, aborting and reverting to {revertTarget}", revertTo);
progressReporter.ReportProgress(0);
- RawCheckout(revertTo, progressReporter.CreateSection("Hard Reset to {revertTo}", 1.0), cancellationToken);
+ RawCheckout(revertTo, false, progressReporter.CreateSection("Hard Reset to {revertTo}", 1.0), cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
}
@@ -376,19 +376,21 @@ namespace Tgstation.Server.Host.Components.Repository
string? username,
string? password,
bool updateSubmodules,
+ bool moveCurrentReference,
JobProgressReporter? progressReporter,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(committish);
logger.LogDebug("Checkout object: {committish}...", committish);
- await eventConsumer.HandleEvent(EventType.RepoCheckout, new List { committish }, false, cancellationToken);
+ await eventConsumer.HandleEvent(EventType.RepoCheckout, new List { committish, moveCurrentReference.ToString() }, false, cancellationToken);
await Task.Factory.StartNew(
() =>
{
libGitRepo.RemoveUntrackedFiles();
RawCheckout(
committish,
+ moveCurrentReference,
progressReporter?.CreateSection(null, updateSubmodules ? 2.0 / 3 : 1.0),
cancellationToken);
},
@@ -879,9 +881,10 @@ namespace Tgstation.Server.Host.Components.Repository
/// Runs a blocking force checkout to .
///
/// The committish to checkout.
+ /// If a hard reset should actually be performed.
/// The optional for the operation.
/// The for the operation.
- void RawCheckout(string committish, JobProgressReporter? progressReporter, CancellationToken cancellationToken)
+ void RawCheckout(string committish, bool moveCurrentReference, JobProgressReporter? progressReporter, CancellationToken cancellationToken)
{
logger.LogTrace("Checkout: {committish}", committish);
@@ -900,34 +903,52 @@ namespace Tgstation.Server.Host.Components.Repository
cancellationToken.ThrowIfCancellationRequested();
- void RunCheckout() => commands.Checkout(
- libGitRepo,
- checkoutOptions,
- committish);
-
- try
+ if (moveCurrentReference)
{
- RunCheckout();
+ if (Reference == NoReference)
+ throw new InvalidOperationException("Cannot move current reference when not on reference!");
+
+ var gitObject = libGitRepo.Lookup(committish);
+ if (gitObject == null)
+ throw new JobException($"Could not find committish: {committish}");
+
+ var commit = gitObject.Peel();
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ libGitRepo.Reset(ResetMode.Hard, commit, checkoutOptions);
}
- catch (NotFoundException)
+ else
{
- // Maybe (likely) a remote?
- var remoteName = $"origin/{committish}";
- var remoteBranch = libGitRepo.Branches.FirstOrDefault(
- branch => branch.FriendlyName.Equals(remoteName, StringComparison.Ordinal));
- cancellationToken.ThrowIfCancellationRequested();
+ void RunCheckout() => commands.Checkout(
+ libGitRepo,
+ checkoutOptions,
+ committish);
- if (remoteBranch == default)
- throw;
+ try
+ {
+ RunCheckout();
+ }
+ catch (NotFoundException)
+ {
+ // Maybe (likely) a remote?
+ var remoteName = $"origin/{committish}";
+ var remoteBranch = libGitRepo.Branches.FirstOrDefault(
+ branch => branch.FriendlyName.Equals(remoteName, StringComparison.Ordinal));
+ cancellationToken.ThrowIfCancellationRequested();
- logger.LogDebug("Creating local branch for {remoteBranchFriendlyName}...", remoteBranch.FriendlyName);
- var branch = libGitRepo.CreateBranch(committish, remoteBranch.Tip);
+ if (remoteBranch == default)
+ throw;
- libGitRepo.Branches.Update(branch, branchUpdate => branchUpdate.TrackedBranch = remoteBranch.CanonicalName);
+ logger.LogDebug("Creating local branch for {remoteBranchFriendlyName}...", remoteBranch.FriendlyName);
+ var branch = libGitRepo.CreateBranch(committish, remoteBranch.Tip);
- cancellationToken.ThrowIfCancellationRequested();
+ libGitRepo.Branches.Update(branch, branchUpdate => branchUpdate.TrackedBranch = remoteBranch.CanonicalName);
- RunCheckout();
+ cancellationToken.ThrowIfCancellationRequested();
+
+ RunCheckout();
+ }
}
cancellationToken.ThrowIfCancellationRequested();
diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryUpdateService.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryUpdateService.cs
index a7aca78c4b..9861a85418 100644
--- a/src/Tgstation.Server.Host/Components/Repository/RepositoryUpdateService.cs
+++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryUpdateService.cs
@@ -22,11 +22,6 @@ namespace Tgstation.Server.Host.Components.Repository
///
sealed class RepositoryUpdateService
{
- ///
- /// The for the .
- ///
- readonly RepositoryUpdateRequest model;
-
///
/// The current for the .
///
@@ -50,19 +45,16 @@ namespace Tgstation.Server.Host.Components.Repository
///
/// Initializes a new instance of the class.
///
- /// The value of .
/// The value of .
/// The value of .
/// The value of .
/// The value of .
public RepositoryUpdateService(
- RepositoryUpdateRequest model,
RepositorySettings currentModel,
User initiatingUser,
ILogger logger,
long instanceId)
{
- this.model = model ?? throw new ArgumentNullException(nameof(model));
this.currentModel = currentModel ?? throw new ArgumentNullException(nameof(currentModel));
this.initiatingUser = initiatingUser ?? throw new ArgumentNullException(nameof(initiatingUser));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -139,24 +131,25 @@ namespace Tgstation.Server.Host.Components.Repository
///
/// The job entrypoint used by to update the repository's current HEAD.
///
- /// The the job is running on. only when performing an instance move operation.
+ /// The .
+ /// The the job is running on.
/// The for the operation.
- /// The running , ignored.
/// The for the job.
/// The for the operation.
/// A representing the running operation.
#pragma warning disable CA1502, CA1506 // TODO: Decomplexify
public async ValueTask RepositoryUpdateJob(
+ RepositoryUpdateRequest model,
IInstanceCore? instance,
IDatabaseContextFactory databaseContextFactory,
- Job job,
JobProgressReporter progressReporter,
CancellationToken cancellationToken)
#pragma warning restore CA1502, CA1506
{
+ ArgumentNullException.ThrowIfNull(model);
ArgumentNullException.ThrowIfNull(instance);
-
- _ = job; // shuts up an IDE warning
+ ArgumentNullException.ThrowIfNull(databaseContextFactory);
+ ArgumentNullException.ThrowIfNull(progressReporter);
var repoManager = instance.RepositoryManager;
using var repo = await repoManager.LoadRepository(cancellationToken) ?? throw new JobException(ErrorCode.RepoMissing);
@@ -315,6 +308,7 @@ namespace Tgstation.Server.Host.Components.Repository
currentModel.AccessUser,
currentModel.AccessToken,
updateSubmodules,
+ false,
NextProgressReporter("Checkout"),
cancellationToken);
await CallLoadRevInfo(); // we've either seen origin before or what we're checking out is on origin
@@ -579,6 +573,7 @@ namespace Tgstation.Server.Host.Components.Repository
currentModel.AccessUser,
currentModel.AccessToken,
true,
+ false,
progressReporter.CreateSection($"Checkout {startReference ?? startSha[..7]}", secondStep ? 0.5 : 1.0),
default);
@@ -588,5 +583,86 @@ namespace Tgstation.Server.Host.Components.Repository
throw;
}
}
+
+ ///
+ /// The job entrypoint used by to reclone a repository.
+ ///
+ /// The the job is running on.
+ /// The for the operation.
+ /// The for the job.
+ /// The for the operation.
+ /// A representing the running operation.
+ public async ValueTask RepositoryRecloneJob(
+ IInstanceCore? instance,
+ IDatabaseContextFactory databaseContextFactory,
+ JobProgressReporter progressReporter,
+ CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(instance);
+ ArgumentNullException.ThrowIfNull(databaseContextFactory);
+ ArgumentNullException.ThrowIfNull(progressReporter);
+
+ progressReporter.StageName = "Loading Old Repository";
+
+ Uri origin;
+ string? oldReference;
+ string oldSha;
+ ValueTask deleteTask;
+ using (var oldRepo = await instance.RepositoryManager.LoadRepository(cancellationToken))
+ {
+ if (oldRepo == null)
+ throw new JobException(ErrorCode.RepoMissing);
+
+ origin = oldRepo.Origin;
+ oldSha = oldRepo.Head;
+ oldReference = oldRepo.Reference;
+ if (oldReference == Repository.NoReference)
+ oldReference = null;
+
+ progressReporter.StageName = "Deleting Old Repository";
+ deleteTask = instance.RepositoryManager.DeleteRepository(cancellationToken);
+ }
+
+ await deleteTask;
+ progressReporter.ReportProgress(0.1);
+ IRepository newRepo;
+ try
+ {
+ newRepo = await instance.RepositoryManager.CloneRepository(
+ origin,
+ oldReference,
+ currentModel.AccessUser,
+ currentModel.AccessToken,
+ progressReporter.CreateSection("Cloning New Repository", 0.8),
+ true, // TODO: Make configurable maybe...
+ cancellationToken)
+ ?? throw new JobException("A race condition occurred while recloning the repository. Somehow, it was fully cloned instantly after being deleted!"); // I'll take lines of code that should never be hit for $10k
+ }
+ catch (Exception ex) when (ex is not JobException)
+ {
+ logger.LogWarning("Reclone failed, clearing credentials!");
+
+ // need to clear credentials here
+ await databaseContextFactory.UseContextTaskReturn(context =>
+ {
+ context.RepositorySettings.Attach(currentModel);
+ currentModel.AccessUser = null;
+ currentModel.AccessToken = null;
+ return context.Save(CancellationToken.None); // DCT: Must always run
+ });
+
+ throw;
+ }
+
+ using (newRepo)
+ await newRepo.CheckoutObject(
+ oldSha,
+ currentModel.AccessUser,
+ currentModel.AccessToken,
+ false,
+ oldReference != null,
+ progressReporter.CreateSection("Checking out previous Detached Commit", 0.1),
+ cancellationToken);
+ }
}
}
diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs
index 10556ca9fc..6880ed7f6d 100644
--- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs
+++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs
@@ -121,6 +121,9 @@ namespace Tgstation.Server.Host.Components.Session
///
public FifoSemaphore TopicSendSemaphore { get; }
+ ///
+ public long MemoryUsage => process.MemoryUsage;
+
///
/// The for the .
///
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs
index 96c6c9ccf9..2220c56932 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs
@@ -24,6 +24,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
///
WatchdogStatus Status { get; }
+ ///
+ /// Gets the memory usage of the game server in bytes.
+ ///
+ long? MemoryUsage { get; }
+
///
/// If the alpha server is the active server.
///
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs
index 28156db6e5..5d9326ce7d 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs
@@ -50,6 +50,9 @@ namespace Tgstation.Server.Host.Components.Watchdog
}
}
+ ///
+ public long? MemoryUsage => GetActiveController()?.MemoryUsage;
+
///
public abstract bool AlphaIsActive { get; }
diff --git a/src/Tgstation.Server.Host/Configuration/SessionConfiguration.cs b/src/Tgstation.Server.Host/Configuration/SessionConfiguration.cs
index c543698b90..d7ee3e1c9d 100644
--- a/src/Tgstation.Server.Host/Configuration/SessionConfiguration.cs
+++ b/src/Tgstation.Server.Host/Configuration/SessionConfiguration.cs
@@ -29,5 +29,10 @@
/// If , deployments that fail will not be immediately cleaned up. They will be cleaned up the next time the instance is onlined.
///
public bool DelayCleaningFailedDeployments { get; set; }
+
+ ///
+ /// If set dd.exe will not be used on Windows systems in versions where it is present. Instead dreamdaemon.exe will always be used.
+ ///
+ public bool ForceUseDreamDaemonExe { get; set; }
}
}
diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs
index 2ccc4f4c8b..d4c1335df7 100644
--- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs
+++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -42,6 +43,11 @@ namespace Tgstation.Server.Host.Controllers
///
const string OctokitException = "Bad GitHub API response, check configuration!";
+ ///
+ /// The key for .
+ ///
+ static readonly object ReadCacheKey = new();
+
///
/// The for the .
///
@@ -77,6 +83,11 @@ namespace Tgstation.Server.Host.Controllers
///
readonly IFileTransferTicketProvider fileTransferService;
+ ///
+ /// The for the .
+ ///
+ readonly IMemoryCache cacheService;
+
///
/// The for the .
///
@@ -94,6 +105,7 @@ namespace Tgstation.Server.Host.Controllers
/// The value of .
/// The value of .
/// The value of .
+ /// The value of .
/// The for the .
/// The containing value of .
/// The for the .
@@ -107,6 +119,7 @@ namespace Tgstation.Server.Host.Controllers
IIOManager ioManager,
IPlatformIdentifier platformIdentifier,
IFileTransferTicketProvider fileTransferService,
+ IMemoryCache cacheService,
ILogger logger,
IOptions fileLoggingConfigurationOptions,
IApiHeadersProvider apiHeadersProvider)
@@ -124,12 +137,14 @@ namespace Tgstation.Server.Host.Controllers
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService));
+ this.cacheService = cacheService ?? throw new ArgumentNullException(nameof(cacheService));
fileLoggingConfiguration = fileLoggingConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(fileLoggingConfigurationOptions));
}
///
/// Get server information.
///
+ /// If , the cache should be bypassed.
/// The for the operation.
/// A resulting in the for the operation.
/// Retrieved data successfully.
@@ -140,39 +155,56 @@ namespace Tgstation.Server.Host.Controllers
[ProducesResponseType(typeof(AdministrationResponse), 200)]
[ProducesResponseType(typeof(ErrorMessageResponse), 424)]
[ProducesResponseType(typeof(ErrorMessageResponse), 429)]
- public async ValueTask Read(CancellationToken cancellationToken)
+ public async ValueTask Read([FromQuery] bool? fresh, CancellationToken cancellationToken)
{
try
{
- Version? greatestVersion = null;
- Uri? repoUrl = null;
- try
+ async Task CacheFactory()
{
- var gitHubService = gitHubServiceFactory.CreateService();
- var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(cancellationToken);
- var releases = await gitHubService.GetTgsReleases(cancellationToken);
-
- foreach (var kvp in releases)
+ Version? greatestVersion = null;
+ Uri? repoUrl = null;
+ try
{
- var version = kvp.Key;
- var release = kvp.Value;
- if (version.Major > 3 // Forward/backward compatible but not before TGS4
- && (greatestVersion == null || version > greatestVersion))
- greatestVersion = version;
+ var gitHubService = gitHubServiceFactory.CreateService();
+ var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(cancellationToken);
+ var releases = await gitHubService.GetTgsReleases(cancellationToken);
+
+ foreach (var kvp in releases)
+ {
+ var version = kvp.Key;
+ var release = kvp.Value;
+ if (version.Major > 3 // Forward/backward compatible but not before TGS4
+ && (greatestVersion == null || version > greatestVersion))
+ greatestVersion = version;
+ }
+
+ repoUrl = await repositoryUrlTask;
+ }
+ catch (NotFoundException e)
+ {
+ Logger.LogWarning(e, "Not found exception while retrieving upstream repository info!");
}
- repoUrl = await repositoryUrlTask;
- }
- catch (NotFoundException e)
- {
- Logger.LogWarning(e, "Not found exception while retrieving upstream repository info!");
+ return Json(new AdministrationResponse
+ {
+ LatestVersion = greatestVersion,
+ TrackedRepositoryUrl = repoUrl,
+ GeneratedAt = DateTimeOffset.UtcNow,
+ });
}
- return Json(new AdministrationResponse
+ var ttl = TimeSpan.FromMinutes(30);
+ Task task;
+ if (fresh == true || !cacheService.TryGetValue(ReadCacheKey, out var rawCacheObject))
{
- LatestVersion = greatestVersion,
- TrackedRepositoryUrl = repoUrl,
- });
+ using var entry = cacheService.CreateEntry(ReadCacheKey);
+ entry.AbsoluteExpirationRelativeToNow = ttl;
+ entry.Value = task = CacheFactory();
+ }
+ else
+ task = (Task)rawCacheObject!;
+
+ return await task;
}
catch (RateLimitExceededException e)
{
diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
index d60fda967d..79e03ebb84 100644
--- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
+++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
@@ -317,11 +317,12 @@ namespace Tgstation.Server.Host.Controllers
/// If there was a settings change made that forced a switch to .
/// The for the operation.
/// A resulting in the of the operation.
+#pragma warning disable CA1502 // TODO: Decomplexify
ValueTask ReadImpl(DreamDaemonSettings? settings, bool knownForcedReboot, CancellationToken cancellationToken)
+#pragma warning restore CA1502
=> WithComponentInstance(async instance =>
{
var dd = instance.Watchdog;
-
var metadata = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadMetadata) != 0;
var revision = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadRevision) != 0;
@@ -372,6 +373,7 @@ namespace Tgstation.Server.Host.Controllers
result.Visibility = settings.Visibility!.Value;
result.SoftRestart = rstate == RebootState.Restart;
result.SoftShutdown = rstate == RebootState.Shutdown;
+ result.ImmediateMemoryUsage = dd.MemoryUsage;
if (rstate == RebootState.Normal && knownForcedReboot)
result.SoftRestart = true;
diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs
index 5d4d686aaf..735b9efa83 100644
--- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs
+++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs
@@ -223,6 +223,42 @@ namespace Tgstation.Server.Host.Controllers
return Accepted(api);
}
+ ///
+ /// Delete the repository.
+ ///
+ /// The for the operation.
+ /// A resulting in the of the operation.
+ /// Job to delete the repository created successfully.
+ /// The database entity for the requested instance could not be retrieved. The instance was likely detached.
+ [HttpPatch]
+ [TgsAuthorize(RepositoryRights.Reclone)]
+ [ProducesResponseType(typeof(RepositoryResponse), 202)]
+ [ProducesResponseType(typeof(ErrorMessageResponse), 410)]
+ public async ValueTask Reclone(CancellationToken cancellationToken)
+ {
+ var currentModel = await DatabaseContext
+ .RepositorySettings
+ .AsQueryable()
+ .Where(x => x.InstanceId == Instance.Id)
+ .FirstOrDefaultAsync(cancellationToken);
+
+ if (currentModel == default)
+ return this.Gone();
+
+ Logger.LogInformation("Instance {instanceId} repository reclone initiated by user {userId}", Instance.Id, AuthenticationContext.User.Require(x => x.Id));
+
+ var repositoryUpdater = CreateRepositoryUpdateService(currentModel);
+
+ var job = Job.Create(JobCode.RepositoryReclone, AuthenticationContext.User, Instance);
+ var api = currentModel.ToApi();
+ await jobManager.RegisterOperation(
+ job,
+ (core, databaseContextFactory, paramJob, progressReporter, ct) => repositoryUpdater.RepositoryRecloneJob(core, databaseContextFactory, progressReporter, ct),
+ cancellationToken);
+ api.ActiveJob = job.ToApi();
+ return Accepted(api);
+ }
+
///
/// Get the repository's status.
///
@@ -443,17 +479,12 @@ namespace Tgstation.Server.Host.Controllers
var job = Job.Create(JobCode.RepositoryUpdate, AuthenticationContext.User, Instance, RepositoryRights.CancelPendingChanges);
job.Description = description;
- var repositoryUpdater = new RepositoryUpdateService(
- model,
- currentModel,
- AuthenticationContext.User,
- loggerFactory.CreateLogger(),
- Instance.Require(x => x.Id));
+ var repositoryUpdater = CreateRepositoryUpdateService(currentModel);
// Time to access git, do it in a job
await jobManager.RegisterOperation(
job,
- repositoryUpdater.RepositoryUpdateJob,
+ (instance, databaseContextFactory, _, progressReporter, jobToken) => repositoryUpdater.RepositoryUpdateJob(model, instance, databaseContextFactory, progressReporter, jobToken),
cancellationToken);
api.ActiveJob = job.ToApi();
@@ -494,5 +525,17 @@ namespace Tgstation.Server.Host.Controllers
cancellationToken);
return needsDbUpdate;
}
+
+ ///
+ /// Creates a .
+ ///
+ /// The current .
+ /// A new .
+ RepositoryUpdateService CreateRepositoryUpdateService(RepositorySettings currentModel)
+ => new(
+ currentModel,
+ AuthenticationContext.User,
+ loggerFactory.CreateLogger(),
+ Instance.Require(x => x.Id));
}
}
diff --git a/src/Tgstation.Server.Host/System/IProcessBase.cs b/src/Tgstation.Server.Host/System/IProcessBase.cs
index 92f6bdb159..f34d5e6597 100644
--- a/src/Tgstation.Server.Host/System/IProcessBase.cs
+++ b/src/Tgstation.Server.Host/System/IProcessBase.cs
@@ -13,6 +13,11 @@ namespace Tgstation.Server.Host.System
///
Task Lifetime { get; }
+ ///
+ /// Gets the process' memory usage in bytes.
+ ///
+ long MemoryUsage { get; }
+
///
/// Set's the owned to a non-normal value.
///
diff --git a/src/Tgstation.Server.Host/System/Process.cs b/src/Tgstation.Server.Host/System/Process.cs
index 896195944c..64c92bf89b 100644
--- a/src/Tgstation.Server.Host/System/Process.cs
+++ b/src/Tgstation.Server.Host/System/Process.cs
@@ -22,6 +22,9 @@ namespace Tgstation.Server.Host.System
///
public Task Lifetime { get; }
+ ///
+ public long MemoryUsage => handle.VirtualMemorySize64;
+
///
/// The for the .
///
diff --git a/src/Tgstation.Server.Host/appsettings.yml b/src/Tgstation.Server.Host/appsettings.yml
index 4868f519fd..526f38ab38 100644
--- a/src/Tgstation.Server.Host/appsettings.yml
+++ b/src/Tgstation.Server.Host/appsettings.yml
@@ -23,6 +23,7 @@ Session:
HighPriorityLiveDreamDaemon: false # If DreamDaemon instances should run as higher priority processes
LowPriorityDeploymentProcesses: true # If TGS Deployments should run as lower priority processes
DelayCleaningFailedDeployments: false # If true, deployments that fail will not be immediately cleaned up. They will be cleaned up the next time the instance is onlined
+ ForceUseDreamDaemonExe: false # If true, dd.exe will not be used on Windows systems in versions where it is present. Instead dreamdaemon.exe will always be used.
FileLogging:
Directory: # Directory in which log files are stored. Windows default: %PROGRAMDATA%/tgstation-server. Linux default: /var/log/tgstation-server
Disable: true # Disable file logging entirely
diff --git a/tests/Tgstation.Server.Tests/Live/AdministrationTest.cs b/tests/Tgstation.Server.Tests/Live/AdministrationTest.cs
index adfb009d69..afeabef6f5 100644
--- a/tests/Tgstation.Server.Tests/Live/AdministrationTest.cs
+++ b/tests/Tgstation.Server.Tests/Live/AdministrationTest.cs
@@ -1,10 +1,10 @@
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-using System;
+using System;
using System.IO;
-using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Client;
@@ -54,10 +54,24 @@ namespace Tgstation.Server.Tests.Live
async Task TestRead(CancellationToken cancellationToken)
{
- var model = await client.Read(cancellationToken);
+ var model = await client.Read(false, cancellationToken);
//we've released a few 5.x versions now, check the release checker is at least somewhat functional
Assert.IsTrue(4 < model.LatestVersion.Major);
+ Assert.IsNotNull(model.TrackedRepositoryUrl);
+ Assert.IsTrue(model.GeneratedAt.HasValue);
+ Assert.IsTrue(model.GeneratedAt.Value <= DateTimeOffset.UtcNow);
+
+ // test the cache
+ var newerModel = await client.Read(false, cancellationToken);
+ Assert.AreEqual(model.GeneratedAt, newerModel.GeneratedAt);
+
+ await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
+
+ var newestModel = await client.Read(true, cancellationToken);
+ Assert.AreNotEqual(model.GeneratedAt, newestModel.GeneratedAt);
+ Assert.IsNotNull(newestModel.GeneratedAt);
+ Assert.IsTrue(model.GeneratedAt < newestModel.GeneratedAt);
}
}
}
diff --git a/tests/Tgstation.Server.Tests/Live/Instance/JobsHubTests.cs b/tests/Tgstation.Server.Tests/Live/Instance/JobsHubTests.cs
index b3149556e5..70b7e9c030 100644
--- a/tests/Tgstation.Server.Tests/Live/Instance/JobsHubTests.cs
+++ b/tests/Tgstation.Server.Tests/Live/Instance/JobsHubTests.cs
@@ -262,7 +262,7 @@ namespace Tgstation.Server.Tests.Live.Instance
Assert.AreEqual(HubConnectionState.Disconnected, permlessConn.State);
// force token refreshs
- await Task.WhenAll(permedUser.Administration.Read(cancellationToken).AsTask(), permlessUser.Instances.List(null, cancellationToken).AsTask());
+ await Task.WhenAll(permedUser.Administration.Read(false, cancellationToken).AsTask(), permlessUser.Instances.List(null, cancellationToken).AsTask());
if (!permlessPsId.HasValue)
{
diff --git a/tests/Tgstation.Server.Tests/Live/Instance/RepositoryTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/RepositoryTest.cs
index 0d6ecce864..86fe4ac911 100644
--- a/tests/Tgstation.Server.Tests/Live/Instance/RepositoryTest.cs
+++ b/tests/Tgstation.Server.Tests/Live/Instance/RepositoryTest.cs
@@ -116,6 +116,8 @@ namespace Tgstation.Server.Tests.Live.Instance
await ApiAssert.ThrowsException(() => Checkout(new RepositoryUpdateRequest { Reference = "master", CheckoutSha = "286bb75" }, false, false, cancellationToken), ErrorCode.RepoMismatchShaAndReference);
var updated = await Checkout(new RepositoryUpdateRequest { CheckoutSha = "286bb75" }, false, false, cancellationToken);
+ await RecloneTest(cancellationToken);
+
// Fake SHA
updated = await Checkout(new RepositoryUpdateRequest { CheckoutSha = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }, true, false, cancellationToken);
@@ -142,6 +144,23 @@ namespace Tgstation.Server.Tests.Live.Instance
await TestMergeTests(updated, prNumber, cancellationToken);
}
+ async ValueTask RecloneTest(CancellationToken cancellationToken)
+ {
+ var initialState = await repositoryClient.Read(cancellationToken);
+ Assert.IsNotNull(initialState.Reference);
+ Assert.IsNotNull(initialState.RevisionInformation);
+ Assert.IsNotNull(initialState.RevisionInformation.CommitSha);
+ Assert.IsNotNull(initialState.RevisionInformation.OriginCommitSha);
+
+ var reclone = await repositoryClient.Reclone(cancellationToken);
+ await WaitForJob(reclone.ActiveJob, 70, false, null, cancellationToken);
+
+ var newState = await repositoryClient.Read(cancellationToken);
+ Assert.AreEqual(initialState.Reference, newState.Reference);
+ Assert.AreEqual(initialState.RevisionInformation.CommitSha, newState.RevisionInformation.CommitSha);
+ Assert.AreEqual(initialState.RevisionInformation.OriginCommitSha, newState.RevisionInformation.OriginCommitSha);
+ }
+
async ValueTask Checkout(RepositoryUpdateRequest updated, bool expectFailure, bool isRef, CancellationToken cancellationToken)
{
var newRef = isRef ? updated.Reference : updated.CheckoutSha;
diff --git a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs
index 26bba79305..08f436b26d 100644
--- a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs
+++ b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs
@@ -150,6 +150,7 @@ namespace Tgstation.Server.Tests.Live.Instance
}, cancellationToken);
Assert.AreEqual(47, updated.OpenDreamTopicPort);
+ Assert.IsFalse(updated.ImmediateMemoryUsage.HasValue);
}
catch (ConflictException ex) when (ex.ErrorCode == ErrorCode.PortNotAvailable)
{
@@ -721,6 +722,8 @@ namespace Tgstation.Server.Tests.Live.Instance
await CheckDDPriority();
Assert.AreEqual(false, daemonStatus.SoftRestart);
Assert.AreEqual(false, daemonStatus.SoftShutdown);
+ Assert.IsTrue(daemonStatus.ImmediateMemoryUsage.HasValue);
+ Assert.AreNotEqual(0, daemonStatus.ImmediateMemoryUsage.Value);
await GracefulWatchdogShutdown(cancellationToken);
@@ -1365,6 +1368,9 @@ namespace Tgstation.Server.Tests.Live.Instance
Assert.IsNotNull(daemonStatus.ActiveCompileJob);
ValidateSessionId(daemonStatus, true);
+ Assert.IsTrue(daemonStatus.ImmediateMemoryUsage.HasValue);
+ Assert.AreNotEqual(0, daemonStatus.ImmediateMemoryUsage.Value);
+
Assert.AreEqual(initialStatus.ActiveCompileJob.Id, daemonStatus.ActiveCompileJob.Id);
var newerCompileJob = daemonStatus.StagedCompileJob;
Assert.AreNotEqual(daemonStatus.ActiveCompileJob.EngineVersion, newerCompileJob.EngineVersion);
diff --git a/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs b/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs
index 3e20609e60..30e6b9e78e 100644
--- a/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs
+++ b/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs
@@ -216,7 +216,7 @@ namespace Tgstation.Server.Tests.Live
};
var badClient = clientFactory.CreateFromToken(serverClient.Url, newToken);
- await ApiAssert.ThrowsException(() => badClient.Administration.Read(cancellationToken));
+ await ApiAssert.ThrowsException(() => badClient.Administration.Read(false, cancellationToken));
await ApiAssert.ThrowsException(() => badClient.ServerInformation(cancellationToken));
}
diff --git a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs
index bfc4538c68..b34b14f066 100644
--- a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs
+++ b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs
@@ -670,7 +670,7 @@ namespace Tgstation.Server.Tests.Live
"asdfasdfasdfasdf");
await using var node1BadClient = clientFactory.CreateFromToken(node1.RootUrl, controllerUserClient.Token);
- await ApiAssert.ThrowsException(() => node1BadClient.Administration.Read(cancellationToken));
+ await ApiAssert.ThrowsException(() => node1BadClient.Administration.Read(false, cancellationToken));
// check instance info is not shared
var controllerInstance = await controllerClient.Instances.CreateOrAttach(
diff --git a/tests/Tgstation.Server.Tests/TestRepository.cs b/tests/Tgstation.Server.Tests/TestRepository.cs
index e73731de40..59b4783994 100644
--- a/tests/Tgstation.Server.Tests/TestRepository.cs
+++ b/tests/Tgstation.Server.Tests/TestRepository.cs
@@ -45,7 +45,7 @@ namespace Tgstation.Server.Tests
() => { });
const string StartSha = "af4da8beb9f9b374b04a3cc4d65acca662e8cc1a";
- await repo.CheckoutObject(StartSha, null, null, true, new JobProgressReporter(Mock.Of>(), null, (stage, progress) => { }), CancellationToken.None);
+ await repo.CheckoutObject(StartSha, null, null, true, false, new JobProgressReporter(Mock.Of>(), null, (stage, progress) => { }), CancellationToken.None);
Assert.AreEqual(Host.Components.Repository.Repository.NoReference, repo.Reference);