Compare commits
No commits in common. "91097940fcd0de1608905ba0aadddee9b6515c37" and "738e8308a89f625625468b528abeec2fd7cf6441" have entirely different histories.
91097940fc
...
738e8308a8
@ -8,7 +8,6 @@
|
|||||||
<entry key="Flawless.Client/Theme/ToggleSwitch.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
<entry key="Flawless.Client/Theme/ToggleSwitch.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||||
<entry key="Flawless.Client/Views/HelloSetup/LoginPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
<entry key="Flawless.Client/Views/HelloSetup/LoginPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||||
<entry key="Flawless.Client/Views/HelloSetup/RegisterPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
<entry key="Flawless.Client/Views/HelloSetup/RegisterPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||||
<entry key="Flawless.Client/Views/HelloSetup/ServerSetupPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
|
||||||
<entry key="Flawless.Client/Views/HelloView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
<entry key="Flawless.Client/Views/HelloView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||||
<entry key="Flawless.Client/Views/HelloWindowView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
<entry key="Flawless.Client/Views/HelloWindowView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||||
<entry key="Flawless.Client/Views/HomeView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
<entry key="Flawless.Client/Views/HomeView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||||
|
|||||||
@ -106,12 +106,6 @@ public static class WorkPath
|
|||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string FormatPathDirectorySeparator(string path)
|
|
||||||
{
|
|
||||||
return path.Replace(Path.DirectorySeparatorChar, DirectorySeparatorChar)
|
|
||||||
.Replace(Path.AltDirectorySeparatorChar, DirectorySeparatorChar);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Split work path into path vector.
|
/// Split work path into path vector.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -13,8 +13,6 @@ public static class AppDefaultValues
|
|||||||
|
|
||||||
public const string RepoLocalStorageDepotFolder = "depots";
|
public const string RepoLocalStorageDepotFolder = "depots";
|
||||||
|
|
||||||
public const string RepoLocalStorageTempFolder = "temp";
|
|
||||||
|
|
||||||
public static string ProgramDataDirectory { get; } =
|
public static string ProgramDataDirectory { get; } =
|
||||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Ca2dWorks", "FlawlessClient");
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Ca2dWorks", "FlawlessClient");
|
||||||
|
|
||||||
|
|||||||
11
Flawless.Client/ErrorGUIHandler.cs
Normal file
11
Flawless.Client/ErrorGUIHandler.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia.Controls.Notifications;
|
||||||
|
|
||||||
|
namespace Flawless.Client;
|
||||||
|
|
||||||
|
public static class ErrorGUIHandler
|
||||||
|
{
|
||||||
|
public static void OnError(Exception ex)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Flawless.Client.Service;
|
using Flawless.Client.Service;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using ReactiveUI.SourceGenerators;
|
using ReactiveUI.SourceGenerators;
|
||||||
|
|
||||||
namespace Flawless.Client.Models;
|
namespace Flawless.Client.Models;
|
||||||
@ -18,13 +18,8 @@ public partial class RepositoryLocalDatabaseModel : ReactiveModel
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public RepositoryFileTreeAccessor? RepoAccessor { get; set; }
|
public RepositoryFileTreeAccessor? RepoAccessor { get; set; }
|
||||||
|
|
||||||
[JsonProperty("currentCommit")]
|
|
||||||
[Reactive] private Guid? _currentCommit;
|
[Reactive] private Guid? _currentCommit;
|
||||||
|
|
||||||
[JsonProperty("commitMessage")]
|
|
||||||
[Reactive] private string? _commitMessage;
|
[Reactive] private string? _commitMessage;
|
||||||
|
|
||||||
[JsonProperty("lastOperationTime")]
|
|
||||||
[Reactive] private DateTime _lastOprationTime;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -99,7 +99,7 @@ public class Api : BaseService<Api>
|
|||||||
public bool RequireRefreshToken()
|
public bool RequireRefreshToken()
|
||||||
{
|
{
|
||||||
if (_token.Value == null) return true;
|
if (_token.Value == null) return true;
|
||||||
if (DateTime.UtcNow > _token.Value.Expiration!.Value.UtcDateTime.AddMinutes(-2)) return true;
|
if (DateTime.UtcNow.AddMinutes(1) > _token.Value.Expiration) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -44,25 +44,20 @@ public class LocalFileTreeAccessor
|
|||||||
|
|
||||||
private IReadOnlyDictionary<string, WorkspaceFile> _baseline;
|
private IReadOnlyDictionary<string, WorkspaceFile> _baseline;
|
||||||
|
|
||||||
private Dictionary<string, ChangeRecord> _changes = new();
|
private Dictionary<string, ChangeRecord> _difference;
|
||||||
|
|
||||||
private Dictionary<string, WorkspaceFile> _currentFiles = new();
|
|
||||||
|
|
||||||
private object _optLock = new();
|
private object _optLock = new();
|
||||||
|
|
||||||
public DateTime LastScanTimeUtc { get; private set; }
|
|
||||||
|
|
||||||
public string WorkingDirectory => _rootDirectory;
|
public string WorkingDirectory => _rootDirectory;
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, WorkspaceFile> BaselineFiles => _baseline;
|
public IReadOnlyDictionary<string, WorkspaceFile> BaselineFiles => _baseline;
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, ChangeRecord> Changes => _changes;
|
public IReadOnlyDictionary<string, ChangeRecord> Changes => _difference;
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, WorkspaceFile> CurrentFiles => _currentFiles;
|
|
||||||
|
|
||||||
public LocalFileTreeAccessor(RepositoryModel repo, IEnumerable<WorkspaceFile> baselines)
|
public LocalFileTreeAccessor(RepositoryModel repo, IEnumerable<WorkspaceFile> baselines)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
|
_difference = new Dictionary<string, ChangeRecord>();
|
||||||
_baseline = baselines.ToImmutableDictionary(b => b.WorkPath);
|
_baseline = baselines.ToImmutableDictionary(b => b.WorkPath);
|
||||||
_rootDirectory = PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name);
|
_rootDirectory = PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name);
|
||||||
}
|
}
|
||||||
@ -72,7 +67,7 @@ public class LocalFileTreeAccessor
|
|||||||
lock (_optLock)
|
lock (_optLock)
|
||||||
{
|
{
|
||||||
_baseline = baselines.ToImmutableDictionary(b => b.WorkPath);
|
_baseline = baselines.ToImmutableDictionary(b => b.WorkPath);
|
||||||
_changes.Clear();
|
_difference.Clear();
|
||||||
|
|
||||||
RefreshInternal();
|
RefreshInternal();
|
||||||
}
|
}
|
||||||
@ -82,7 +77,7 @@ public class LocalFileTreeAccessor
|
|||||||
{
|
{
|
||||||
lock (_optLock)
|
lock (_optLock)
|
||||||
{
|
{
|
||||||
_changes.Clear();
|
_difference.Clear();
|
||||||
|
|
||||||
RefreshInternal();
|
RefreshInternal();
|
||||||
}
|
}
|
||||||
@ -90,7 +85,7 @@ public class LocalFileTreeAccessor
|
|||||||
|
|
||||||
private void RefreshInternal()
|
private void RefreshInternal()
|
||||||
{
|
{
|
||||||
_currentFiles.Clear();
|
Dictionary<string, WorkspaceFile> currentFiles = new();
|
||||||
foreach (var f in Directory.GetFiles(_rootDirectory, "*", SearchOption.AllDirectories))
|
foreach (var f in Directory.GetFiles(_rootDirectory, "*", SearchOption.AllDirectories))
|
||||||
{
|
{
|
||||||
var workPath = WorkPath.FromPlatformPath(f, _rootDirectory);
|
var workPath = WorkPath.FromPlatformPath(f, _rootDirectory);
|
||||||
@ -98,18 +93,16 @@ public class LocalFileTreeAccessor
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
var modifyTime = File.GetLastWriteTimeUtc(f);
|
var modifyTime = File.GetLastWriteTimeUtc(f);
|
||||||
_currentFiles.Add(workPath, new WorkspaceFile { WorkPath = workPath, ModifyTime = modifyTime });
|
currentFiles.Add(workPath, new WorkspaceFile { WorkPath = workPath, ModifyTime = modifyTime });
|
||||||
}
|
}
|
||||||
|
|
||||||
LastScanTimeUtc = DateTime.UtcNow;
|
|
||||||
|
|
||||||
// Find those are changed
|
// Find those are changed
|
||||||
var changes = _currentFiles.Values.Where(v => _baseline.TryGetValue(v.WorkPath, out var fi) && fi.ModifyTime != v.ModifyTime);
|
var changes = currentFiles.Values.Where(v => _baseline.TryGetValue(v.WorkPath, out var fi) && fi.ModifyTime != v.ModifyTime);
|
||||||
var news = _currentFiles.Values.Where(v => !_baseline.ContainsKey(v.WorkPath));
|
var news = currentFiles.Values.Where(v => !_baseline.ContainsKey(v.WorkPath));
|
||||||
var removed = _baseline.Values.Where(v => !_currentFiles.ContainsKey(v.WorkPath));
|
var removed = _baseline.Values.Where(v => !currentFiles.ContainsKey(v.WorkPath));
|
||||||
|
|
||||||
foreach (var f in changes) _changes.Add(f.WorkPath, new ChangeRecord(ChangeType.Modify, f));
|
foreach (var f in changes) _difference.Add(f.WorkPath, new ChangeRecord(ChangeType.Modify, f));
|
||||||
foreach (var f in news) _changes.Add(f.WorkPath, new ChangeRecord(ChangeType.Add, f));
|
foreach (var f in news) _difference.Add(f.WorkPath, new ChangeRecord(ChangeType.Add, f));
|
||||||
foreach (var f in removed) _changes.Add(f.WorkPath, new ChangeRecord(ChangeType.Remove, f));
|
foreach (var f in removed) _difference.Add(f.WorkPath, new ChangeRecord(ChangeType.Remove, f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
using Refit;
|
using Refit;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@ -114,25 +113,25 @@ namespace Flawless.Client.Remote
|
|||||||
/// <returns>OK</returns>
|
/// <returns>OK</returns>
|
||||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||||
[Headers("Accept: text/plain, application/json, text/json")]
|
[Headers("Accept: text/plain, application/json, text/json")]
|
||||||
[Post("/api/repo/{userName}/{repositoryName}/fetch_manifest")]
|
[Get("/api/repo/{userName}/{repositoryName}/fetch_manifest")]
|
||||||
Task<CommitManifest> FetchManifest(string userName, string repositoryName, [Query] string commitId);
|
Task<FileResponse> FetchManifest(string userName, string repositoryName, [Query] string commitId);
|
||||||
|
|
||||||
/// <returns>OK</returns>
|
/// <returns>OK</returns>
|
||||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||||
[Headers("Accept: text/plain, application/json, text/json")]
|
[Headers("Accept: text/plain, application/json, text/json")]
|
||||||
[Post("/api/repo/{userName}/{repositoryName}/fetch_depot")]
|
[Get("/api/repo/{userName}/{repositoryName}/fetch_depot")]
|
||||||
Task<ApiResponse<Stream>> FetchDepot(string userName, string repositoryName, [Query] string depotId);
|
Task<FileResponse> FetchDepot(string userName, string repositoryName, [Query] string depotId);
|
||||||
|
|
||||||
/// <returns>OK</returns>
|
/// <returns>OK</returns>
|
||||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||||
[Headers("Accept: text/plain, application/json, text/json")]
|
[Headers("Accept: text/plain, application/json, text/json")]
|
||||||
[Post("/api/repo/{userName}/{repositoryName}/list_commit")]
|
[Get("/api/repo/{userName}/{repositoryName}/list_commit")]
|
||||||
Task<RepositoryCommitResponseListingResponse> ListCommit(string userName, string repositoryName);
|
Task<RepositoryCommitResponseListingResponse> ListCommit(string userName, string repositoryName);
|
||||||
|
|
||||||
/// <returns>OK</returns>
|
/// <returns>OK</returns>
|
||||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||||
[Headers("Accept: text/plain, application/json, text/json")]
|
[Headers("Accept: text/plain, application/json, text/json")]
|
||||||
[Post("/api/repo/{userName}/{repositoryName}/list_locked_files")]
|
[Get("/api/repo/{userName}/{repositoryName}/list_locked_files")]
|
||||||
Task<LockFileInfoListingResponse> ListLockedFiles(string userName, string repositoryName);
|
Task<LockFileInfoListingResponse> ListLockedFiles(string userName, string repositoryName);
|
||||||
|
|
||||||
/// <returns>OK</returns>
|
/// <returns>OK</returns>
|
||||||
@ -156,7 +155,7 @@ namespace Flawless.Client.Remote
|
|||||||
[Multipart]
|
[Multipart]
|
||||||
[Headers("Accept: text/plain, application/json, text/json")]
|
[Headers("Accept: text/plain, application/json, text/json")]
|
||||||
[Post("/api/repo/{userName}/{repositoryName}/create_commit")]
|
[Post("/api/repo/{userName}/{repositoryName}/create_commit")]
|
||||||
Task<CommitSuccessResponse> CreateCommit(string userName, string repositoryName, StreamPart depot, string message, IEnumerable<string> workspaceSnapshot, IEnumerable<string> requiredDepots, string mainDepotId);
|
Task<CommitSuccessResponse> CreateCommit(string userName, string repositoryName, StreamPart depot, string message, IEnumerable<WorkspaceFile> workspaceSnapshot, IEnumerable<string> requiredDepots, string mainDepotId);
|
||||||
|
|
||||||
/// <returns>OK</returns>
|
/// <returns>OK</returns>
|
||||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||||
@ -233,21 +232,6 @@ namespace Flawless.Client.Remote
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
|
|
||||||
public partial class CommitManifest
|
|
||||||
{
|
|
||||||
|
|
||||||
[JsonPropertyName("manifestId")]
|
|
||||||
public System.Guid ManifestId { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("depot")]
|
|
||||||
public ICollection<DepotLabel> Depot { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("filePaths")]
|
|
||||||
public ICollection<WorkspaceFile> FilePaths { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
|
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||||
public partial class CommitSuccessResponse
|
public partial class CommitSuccessResponse
|
||||||
{
|
{
|
||||||
@ -258,21 +242,6 @@ namespace Flawless.Client.Remote
|
|||||||
[JsonPropertyName("commitId")]
|
[JsonPropertyName("commitId")]
|
||||||
public System.Guid CommitId { get; set; }
|
public System.Guid CommitId { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("mainDepotId")]
|
|
||||||
public System.Guid MainDepotId { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
|
|
||||||
public partial class DepotLabel
|
|
||||||
{
|
|
||||||
|
|
||||||
[JsonPropertyName("id")]
|
|
||||||
public System.Guid Id { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("length")]
|
|
||||||
public long Length { get; set; }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
|
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||||
@ -566,12 +535,6 @@ namespace Flawless.Client.Remote
|
|||||||
public partial class WorkspaceFile
|
public partial class WorkspaceFile
|
||||||
{
|
{
|
||||||
|
|
||||||
[JsonPropertyName("modifyTime")]
|
|
||||||
public System.DateTimeOffset ModifyTime { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("workPath")]
|
|
||||||
public string WorkPath { get; set; }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[System.CodeDom.Compiler.GeneratedCode("NSwag", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
|
[System.CodeDom.Compiler.GeneratedCode("NSwag", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||||
|
|||||||
@ -1,16 +1,14 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Flawless.Core.BinaryDataFormat;
|
using Flawless.Core.BinaryDataFormat;
|
||||||
using Flawless.Core.Modal;
|
using Flawless.Core.Modal;
|
||||||
|
|
||||||
namespace Flawless.Client.Service;
|
namespace Flawless.Client.Service;
|
||||||
|
|
||||||
public class RepositoryFileTreeAccessor : IDisposable, IAsyncDisposable, IEnumerable<WorkspaceFile>
|
public class RepositoryFileTreeAccessor : IDisposable, IAsyncDisposable
|
||||||
{
|
{
|
||||||
|
|
||||||
private struct FileReadInfoCache
|
private struct FileReadInfoCache
|
||||||
@ -79,7 +77,6 @@ public class RepositoryFileTreeAccessor : IDisposable, IAsyncDisposable, IEnumer
|
|||||||
|
|
||||||
foreach (var (id, stream) in _mappings)
|
foreach (var (id, stream) in _mappings)
|
||||||
{
|
{
|
||||||
stream.Seek(0, SeekOrigin.Begin);
|
|
||||||
var header = DataTransformer.ExtractStandardDepotHeaderV1(stream);
|
var header = DataTransformer.ExtractStandardDepotHeaderV1(stream);
|
||||||
_headers.Add(id, header);
|
_headers.Add(id, header);
|
||||||
await foreach (var inf in DataTransformer.ExtractDepotFileInfoMapAsync(stream, header.FileMapSize))
|
await foreach (var inf in DataTransformer.ExtractDepotFileInfoMapAsync(stream, header.FileMapSize))
|
||||||
@ -112,7 +109,7 @@ public class RepositoryFileTreeAccessor : IDisposable, IAsyncDisposable, IEnumer
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryWriteDataIntoStream(string workPath, Stream stream, out DateTime modifyTime)
|
public bool TryWriteDataIntoStream(string workPath, Stream stream)
|
||||||
{
|
{
|
||||||
DisposeCheck();
|
DisposeCheck();
|
||||||
if (stream == null || !stream.CanWrite) throw new ArgumentException("Stream is not writable!");
|
if (stream == null || !stream.CanWrite) throw new ArgumentException("Stream is not writable!");
|
||||||
@ -121,24 +118,6 @@ public class RepositoryFileTreeAccessor : IDisposable, IAsyncDisposable, IEnumer
|
|||||||
{
|
{
|
||||||
var baseStream = DataTransformer.ExtractStandardDepotFile(_mappings[r.DepotId], r.FileInfo);
|
var baseStream = DataTransformer.ExtractStandardDepotFile(_mappings[r.DepotId], r.FileInfo);
|
||||||
baseStream.CopyTo(stream);
|
baseStream.CopyTo(stream);
|
||||||
modifyTime = r.FileInfo.ModifyTime;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
modifyTime = DateTime.MinValue;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryWriteDataIntoStream(string workPath, string destinationPath)
|
|
||||||
{
|
|
||||||
DisposeCheck();
|
|
||||||
|
|
||||||
if (_cached.TryGetValue(workPath, out var r))
|
|
||||||
{
|
|
||||||
using var ws = new FileStream(destinationPath, FileMode.OpenOrCreate, FileAccess.Write);
|
|
||||||
var baseStream = DataTransformer.ExtractStandardDepotFile(_mappings[r.DepotId], r.FileInfo);
|
|
||||||
baseStream.CopyTo(ws);
|
|
||||||
File.SetLastWriteTimeUtc(destinationPath, r.FileInfo.ModifyTime);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,14 +129,4 @@ public class RepositoryFileTreeAccessor : IDisposable, IAsyncDisposable, IEnumer
|
|||||||
if (_disposed) throw new ObjectDisposedException("Accessor has already been disposed");
|
if (_disposed) throw new ObjectDisposedException("Accessor has already been disposed");
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerator<WorkspaceFile> GetEnumerator()
|
|
||||||
{
|
|
||||||
return _cached.Values
|
|
||||||
.Select(cv => new WorkspaceFile(cv.FileInfo.ModifyTime, cv.FileInfo.Path)).GetEnumerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
{
|
|
||||||
return GetEnumerator();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -3,14 +3,12 @@ using System.Collections.Generic;
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Flawless.Abstraction;
|
using Flawless.Abstraction;
|
||||||
using Flawless.Client.Models;
|
using Flawless.Client.Models;
|
||||||
using Flawless.Core.BinaryDataFormat;
|
|
||||||
using Flawless.Core.Modal;
|
using Flawless.Core.Modal;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Refit;
|
using ValueTaskSupplement;
|
||||||
|
|
||||||
namespace Flawless.Client.Service;
|
namespace Flawless.Client.Service;
|
||||||
|
|
||||||
@ -44,16 +42,18 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
|
|
||||||
public bool SaveRepositoryLocalDatabaseChanges(RepositoryModel repo)
|
public bool SaveRepositoryLocalDatabaseChanges(RepositoryModel repo)
|
||||||
{
|
{
|
||||||
var localRepo = GetRepositoryLocalDatabase(repo);
|
|
||||||
localRepo.LastOprationTime = DateTime.Now;
|
|
||||||
|
|
||||||
var dbPath = PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name);
|
var dbPath = PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name);
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
|
if (File.Exists(dbPath)) return false;
|
||||||
|
|
||||||
using var writeFs = new StreamWriter(new FileStream(dbPath, FileMode.OpenOrCreate));
|
if (!_localRepoDbModel.TryGetValue(repo, out var localRepo))
|
||||||
JsonSerializer.CreateDefault().Serialize(writeFs, localRepo);
|
{
|
||||||
|
using var writeFs = new StreamWriter(new FileStream(dbPath, FileMode.Truncate));
|
||||||
return true;
|
JsonSerializer.CreateDefault().Serialize(writeFs, localRepo);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RepositoryLocalDatabaseModel GetRepositoryLocalDatabase(RepositoryModel repo)
|
public RepositoryLocalDatabaseModel GetRepositoryLocalDatabase(RepositoryModel repo)
|
||||||
@ -115,7 +115,6 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
UIHelper.NotifyError(e);
|
|
||||||
Console.WriteLine(e);
|
Console.WriteLine(e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -165,7 +164,6 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
UIHelper.NotifyError(e);
|
|
||||||
Console.WriteLine(e);
|
Console.WriteLine(e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -185,6 +183,16 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async ValueTask<bool> UpdateDownloadedStatusFromDiskAsync(RepositoryModel repo)
|
||||||
|
{
|
||||||
|
var isFolderExists = Directory.Exists(PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name));
|
||||||
|
var isDbFileExists = File.Exists(PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name));
|
||||||
|
|
||||||
|
repo.IsDownloaded = isFolderExists && isDbFileExists;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public async ValueTask<bool> UpdateMembersFromServerAsync(RepositoryModel repo)
|
public async ValueTask<bool> UpdateMembersFromServerAsync(RepositoryModel repo)
|
||||||
{
|
{
|
||||||
var api = Api.C;
|
var api = Api.C;
|
||||||
@ -228,7 +236,6 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
UIHelper.NotifyError(e);
|
|
||||||
Console.WriteLine(e);
|
Console.WriteLine(e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -260,22 +267,17 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
|
|
||||||
public async ValueTask<bool> OpenRepositoryOnStorageAsync(RepositoryModel repo)
|
public async ValueTask<bool> OpenRepositoryOnStorageAsync(RepositoryModel repo)
|
||||||
{
|
{
|
||||||
if (!await UpdateRepositoriesDownloadedStatusFromDiskAsync() || repo.IsDownloaded == false) return false;
|
if (!await UpdateDownloadedStatusFromDiskAsync(repo) || repo.IsDownloaded == false) return false;
|
||||||
if (!await UpdateCommitsFromServerAsync(repo)) return false;
|
if (!await UpdateCommitsFromServerAsync(repo)) return false;
|
||||||
var ls = GetRepositoryLocalDatabase(repo);
|
var ls = GetRepositoryLocalDatabase(repo);
|
||||||
|
|
||||||
if (ls.CurrentCommit != null)
|
if (ls.CurrentCommit != null)
|
||||||
{
|
{
|
||||||
var accessor = await DownloadDepotsAndUseLocalCachesToGenerateRepositoryFileTreeAccessorFromServerAsync(repo, ls.CurrentCommit.Value);
|
var accessor = await DownloadDepotsToGetCommitFileTreeFromServerAsync(repo, ls.CurrentCommit.Value);
|
||||||
if (accessor == null) return false;
|
if (accessor == null) return false;
|
||||||
ls.RepoAccessor = accessor;
|
ls.RepoAccessor = accessor;
|
||||||
|
|
||||||
// Remember to cache accessor everytime it will being used.
|
|
||||||
await accessor.CreateCacheAsync();
|
|
||||||
ls.LocalAccessor.SetBaseline(accessor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SaveRepositoryLocalDatabaseChanges(repo);
|
|
||||||
_openedRepos.Add(repo);
|
_openedRepos.Add(repo);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -285,7 +287,6 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
// Create basic structures.
|
// Create basic structures.
|
||||||
if (!TryCreateRepositoryBaseStorageStructure(repo)) return false;
|
if (!TryCreateRepositoryBaseStorageStructure(repo)) return false;
|
||||||
|
|
||||||
SaveRepositoryLocalDatabaseChanges(repo);
|
|
||||||
repo.IsDownloaded = true;
|
repo.IsDownloaded = true;
|
||||||
_openedRepos.Add(repo);
|
_openedRepos.Add(repo);
|
||||||
return true;
|
return true;
|
||||||
@ -300,21 +301,20 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
var peekCommit = repo.Commits.MaxBy(sl => sl.CommittedOn);
|
var peekCommit = repo.Commits.MaxBy(sl => sl.CommittedOn);
|
||||||
if (peekCommit == null) return false; // Should not use this function!
|
if (peekCommit == null) return false; // Should not use this function!
|
||||||
|
|
||||||
|
// Create basic structures.
|
||||||
|
if (!TryCreateRepositoryBaseStorageStructure(repo)) return false;
|
||||||
|
|
||||||
// Download base repo info
|
// Download base repo info
|
||||||
var accessor = await DownloadDepotsAndUseLocalCachesToGenerateRepositoryFileTreeAccessorFromServerAsync(repo, peekCommit.CommitId);
|
var accessor = await DownloadDepotsToGetCommitFileTreeFromServerAsync(repo, peekCommit.CommitId);
|
||||||
if (accessor == null)
|
if (accessor == null)
|
||||||
{
|
{
|
||||||
await DeleteFromDiskAsync(repo);
|
await DeleteFromDiskAsync(repo);
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remember to cache accessor everytime it will being used.
|
|
||||||
await accessor.CreateCacheAsync();
|
|
||||||
|
|
||||||
var ls = GetRepositoryLocalDatabase(repo);
|
var ls = GetRepositoryLocalDatabase(repo);
|
||||||
ls.CurrentCommit = peekCommit.CommitId;
|
ls.CurrentCommit = peekCommit.CommitId;
|
||||||
ls.RepoAccessor = accessor;
|
ls.RepoAccessor = accessor;
|
||||||
ls.LocalAccessor.SetBaseline(accessor);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -325,19 +325,17 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
|
|
||||||
// Write into fs
|
// Write into fs
|
||||||
if (directory != null) Directory.CreateDirectory(directory);
|
if (directory != null) Directory.CreateDirectory(directory);
|
||||||
if (!accessor.TryWriteDataIntoStream(f.WorkPath, pfs))
|
await using var write = File.Create(pfs);
|
||||||
throw new InvalidDataException($"Can not write {f.WorkPath} into repository.");
|
accessor.TryWriteDataIntoStream(f.WorkPath, write);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
UIHelper.NotifyError(e);
|
|
||||||
Console.WriteLine(e);
|
Console.WriteLine(e);
|
||||||
await DeleteFromDiskAsync(repo);
|
await DeleteFromDiskAsync(repo);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
SaveRepositoryLocalDatabaseChanges(repo);
|
|
||||||
repo.IsDownloaded = true;
|
repo.IsDownloaded = true;
|
||||||
_openedRepos.Add(repo);
|
_openedRepos.Add(repo);
|
||||||
return true;
|
return true;
|
||||||
@ -365,7 +363,6 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
UIHelper.NotifyError(e);
|
|
||||||
Console.WriteLine(e);
|
Console.WriteLine(e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -422,7 +419,6 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
UIHelper.NotifyError(e);
|
|
||||||
Console.WriteLine(e);
|
Console.WriteLine(e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -430,8 +426,7 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<RepositoryFileTreeAccessor?>
|
public async ValueTask<RepositoryFileTreeAccessor?> DownloadDepotsToGetCommitFileTreeFromServerAsync
|
||||||
DownloadDepotsAndUseLocalCachesToGenerateRepositoryFileTreeAccessorFromServerAsync
|
|
||||||
(RepositoryModel repo, Guid commit, bool storeDownloadedDepots = true)
|
(RepositoryModel repo, Guid commit, bool storeDownloadedDepots = true)
|
||||||
{
|
{
|
||||||
if (commit == Guid.Empty) return null;
|
if (commit == Guid.Empty) return null;
|
||||||
@ -441,8 +436,9 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
if (manifest == null) return null;
|
if (manifest == null) return null;
|
||||||
|
|
||||||
// Prepare folders
|
// Prepare folders
|
||||||
|
var path = PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name);
|
||||||
var depotsRoot = PathUtility.GetWorkspaceDepotCachePath(repo.OwnerName, repo.Name);
|
var depotsRoot = PathUtility.GetWorkspaceDepotCachePath(repo.OwnerName, repo.Name);
|
||||||
Directory.CreateDirectory(depotsRoot);
|
Directory.CreateDirectory(path);
|
||||||
|
|
||||||
// Generate download depots list
|
// Generate download depots list
|
||||||
var mainDepotLabel = manifest.Value.Depot;
|
var mainDepotLabel = manifest.Value.Depot;
|
||||||
@ -456,10 +452,12 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
|
|
||||||
// Download them
|
// Download them
|
||||||
var downloadedDepots = await DownloadDepotsAndCopyNetworkStreamIntoNewMemoryStreamFromServerAsync(repo, willDownload);
|
var downloadedDepots = await DownloadDepotsAndCopyNetworkStreamIntoNewMemoryStreamFromServerAsync(repo, willDownload);
|
||||||
if (downloadedDepots == null) return null;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Check if anyone download failed
|
||||||
|
if (downloadedDepots == null || downloadedDepots.Any(dl => dl.Item2 == null))
|
||||||
|
throw new Exception("Some depots are not able to be downloaded.");
|
||||||
|
|
||||||
if (storeDownloadedDepots)
|
if (storeDownloadedDepots)
|
||||||
{
|
{
|
||||||
@ -469,22 +467,18 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create mapping dictionary
|
// Create mapping dictionary
|
||||||
var streamMap = downloadedDepots.ToDictionary(i => i.Item1, i => i.Item2!);
|
var mappingDict = downloadedDepots.ToDictionary(i => i.Item1, i => i.Item2!);
|
||||||
foreach (var dl in mainDepotLabel)
|
foreach (var dl in mainDepotLabel)
|
||||||
{
|
{
|
||||||
// If this file is not being opened, open it from file system
|
if (mappingDict.ContainsKey(dl.Id)) continue;
|
||||||
if (!streamMap.ContainsKey(dl.Id))
|
var dst = Path.Combine(depotsRoot, dl.Id.ToString());
|
||||||
{
|
mappingDict.Add(dl.Id, new FileStream(dst, FileMode.Create));
|
||||||
var dst = Path.Combine(depotsRoot, dl.Id.ToString());
|
|
||||||
streamMap.Add(dl.Id, new FileStream(dst, FileMode.Open, FileAccess.Read, FileShare.Read));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RepositoryFileTreeAccessor(streamMap, manifest.Value);
|
return new RepositoryFileTreeAccessor(mappingDict, manifest.Value);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
UIHelper.NotifyError(e);
|
|
||||||
if (downloadedDepots != null)
|
if (downloadedDepots != null)
|
||||||
foreach (var t in downloadedDepots)
|
foreach (var t in downloadedDepots)
|
||||||
{
|
{
|
||||||
@ -500,22 +494,21 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask WriteDownloadedDepotsFromServerToStorageAsync
|
public async ValueTask WriteDownloadedDepotsFromServerToStorageAsync
|
||||||
(RepositoryModel repo, IEnumerable<(Guid id, Stream stream)> depots)
|
(RepositoryModel repo, IEnumerable<(Guid, Stream)> depots)
|
||||||
{
|
{
|
||||||
var depotsRoot = PathUtility.GetWorkspaceDepotCachePath(repo.OwnerName, repo.Name);
|
var depotsRoot = PathUtility.GetWorkspaceDepotCachePath(repo.OwnerName, repo.Name);
|
||||||
foreach (var d in depots)
|
var tasks = depots.Select(async d =>
|
||||||
{
|
{
|
||||||
var dst = Path.Combine(depotsRoot, d.id.ToString());
|
var dst = Path.Combine(depotsRoot, d.Item1.ToString());
|
||||||
await using var ws = new FileStream(dst, FileMode.Create);
|
await using var ws = new FileStream(dst, FileMode.Create);
|
||||||
|
await d.Item2.CopyToAsync(ws);
|
||||||
// Make sure always to be at begin.
|
d.Item2.Seek(0, SeekOrigin.Begin);
|
||||||
d.stream.Seek(0, SeekOrigin.Begin);
|
});
|
||||||
await d.stream.CopyToAsync(ws);
|
|
||||||
d.stream.Seek(0, SeekOrigin.Begin);
|
await Task.WhenAll(tasks);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<(Guid, Stream)[]?> DownloadDepotsAndCopyNetworkStreamIntoNewMemoryStreamFromServerAsync
|
public async ValueTask<(Guid, Stream?)[]?> DownloadDepotsAndCopyNetworkStreamIntoNewMemoryStreamFromServerAsync
|
||||||
(RepositoryModel repo, IEnumerable<DepotLabel> depotsId)
|
(RepositoryModel repo, IEnumerable<DepotLabel> depotsId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -527,29 +520,24 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = new List<(Guid, Stream)>();
|
return await ValueTaskEx.WhenAll(depotsId.Select(p => DownloadDepotInternalAsync(p.Id, p.Length)));
|
||||||
foreach (var dl in depotsId)
|
|
||||||
{
|
|
||||||
using var rsp = await Api.C.Gateway.FetchDepot(repo.OwnerName, repo.Name, dl.Id.ToString());
|
|
||||||
if (rsp.StatusCode != HttpStatusCode.OK)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Failed to fetch depot {dl.Id}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var memoryStream = new MemoryStream(new byte[dl.Length]);
|
|
||||||
await rsp.Content!.CopyToAsync(memoryStream);
|
|
||||||
result.Add((dl.Id, memoryStream));
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.ToArray();
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
UIHelper.NotifyError(e);
|
|
||||||
Console.WriteLine(e);
|
Console.WriteLine(e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async ValueTask<(Guid, Stream?)> DownloadDepotInternalAsync(Guid depotId, long length)
|
||||||
|
{
|
||||||
|
using var rsp = await Api.C.Gateway.FetchDepot(repo.OwnerName, repo.Name, depotId.ToString());
|
||||||
|
if (rsp.StatusCode != 200) return (depotId, null);
|
||||||
|
if (rsp.Stream.Length != length) return (depotId, null);
|
||||||
|
|
||||||
|
var memoryStream = new MemoryStream(new byte[rsp.Stream.Length]);
|
||||||
|
await rsp.Stream.CopyToAsync(memoryStream);
|
||||||
|
return (depotId, memoryStream);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<CommitManifest?> DownloadManifestFromServerAsync(RepositoryModel repo, Guid manifestId)
|
public async ValueTask<CommitManifest?> DownloadManifestFromServerAsync(RepositoryModel repo, Guid manifestId)
|
||||||
@ -563,15 +551,13 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var rsp = await api.Gateway.FetchManifest(repo.OwnerName, repo.Name, manifestId.ToString());
|
using var manifestResponse = await api.Gateway.FetchManifest(repo.OwnerName, repo.Name, manifestId.ToString());
|
||||||
return new(
|
if (manifestResponse.StatusCode != 200) return null;
|
||||||
rsp.ManifestId,
|
|
||||||
rsp.Depot.Select(x => new DepotLabel(x.Id, x.Length)).ToArray(),
|
return await System.Text.Json.JsonSerializer.DeserializeAsync<CommitManifest>(manifestResponse.Stream);
|
||||||
rsp.FilePaths.Select(x => new WorkspaceFile(x.ModifyTime.UtcDateTime, x.WorkPath)).ToArray());
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
UIHelper.NotifyError(e);
|
|
||||||
Console.WriteLine(e);
|
Console.WriteLine(e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -586,7 +572,6 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
UIHelper.NotifyError(e);
|
|
||||||
Console.WriteLine(e);
|
Console.WriteLine(e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -594,173 +579,4 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
repo.IsDownloaded = false;
|
repo.IsDownloaded = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<CommitManifest?> CommitWorkspaceAsBaselineAsync
|
|
||||||
(RepositoryModel repo, IEnumerable<LocalFileTreeAccessor.ChangeRecord> changes, string message)
|
|
||||||
{
|
|
||||||
var localDb = GetRepositoryLocalDatabase(repo);
|
|
||||||
var manifestList = CreateCommitManifestByCurrentBaselineAndChanges(localDb.LocalAccessor, changes);
|
|
||||||
var api = Api.C;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
|
|
||||||
// Renew for once.
|
|
||||||
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
|
||||||
{
|
|
||||||
api.ClearGateway();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate depot
|
|
||||||
var tempDepotPath = await CreateDepotIntoTempFileAsync(repo, manifestList);
|
|
||||||
if (tempDepotPath == null) return null;
|
|
||||||
|
|
||||||
// Upload and create commit
|
|
||||||
await using var str = File.OpenRead(tempDepotPath);
|
|
||||||
var snapshot = manifestList.Select(l => $"{l.ModifyTime.ToBinary()}${l.WorkPath}");
|
|
||||||
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
|
||||||
{
|
|
||||||
api.ClearGateway();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var rsp = await api.Gateway.CreateCommit(repo.OwnerName, repo.Name,
|
|
||||||
new StreamPart(str, Path.GetFileName(tempDepotPath)), message, snapshot, null!, null!);
|
|
||||||
|
|
||||||
// Move depot file to destination
|
|
||||||
var depotsPath = PathUtility.GetWorkspaceDepotCachePath(repo.OwnerName, repo.Name);
|
|
||||||
var finalPath = Path.Combine(depotsPath, rsp.MainDepotId.ToString());
|
|
||||||
Directory.CreateDirectory(depotsPath);
|
|
||||||
File.Move(tempDepotPath, finalPath, true);
|
|
||||||
|
|
||||||
// Fetch mapped manifest
|
|
||||||
var manifest = await DownloadManifestFromServerAsync(repo, rsp.CommitId);
|
|
||||||
if (manifest == null) return null;
|
|
||||||
|
|
||||||
var accessor = await DownloadDepotsAndUseLocalCachesToGenerateRepositoryFileTreeAccessorFromServerAsync(repo, rsp.CommitId);
|
|
||||||
if (accessor == null) return null; //todo this is a really fatal issue...
|
|
||||||
if (localDb.RepoAccessor != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await localDb.RepoAccessor.DisposeAsync();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
UIHelper.NotifyError(e);
|
|
||||||
Console.WriteLine(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Point to newest state.
|
|
||||||
localDb.RepoAccessor = accessor;
|
|
||||||
localDb.CurrentCommit = rsp.CommitId;
|
|
||||||
localDb.LocalAccessor.SetBaseline(accessor);
|
|
||||||
SaveRepositoryLocalDatabaseChanges(repo);
|
|
||||||
|
|
||||||
return manifest;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
UIHelper.NotifyError(e);
|
|
||||||
Console.WriteLine(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<WorkspaceFile> CreateCommitManifestByCurrentBaselineAndChanges
|
|
||||||
(LocalFileTreeAccessor accessor, IEnumerable<LocalFileTreeAccessor.ChangeRecord> changes, bool hard = false)
|
|
||||||
{
|
|
||||||
// Create a new depot file manifest.
|
|
||||||
var files = accessor.BaselineFiles.Values.ToList();
|
|
||||||
foreach (var c in changes)
|
|
||||||
{
|
|
||||||
switch (c.Type)
|
|
||||||
{
|
|
||||||
case LocalFileTreeAccessor.ChangeType.Folder:
|
|
||||||
{
|
|
||||||
if (hard) throw new InvalidProgramException(
|
|
||||||
$"Can not commit folder into version control: {c.File.WorkPath}");
|
|
||||||
|
|
||||||
Console.WriteLine($"Can not commit folder into version control...Ignored: {c.File.WorkPath}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
case LocalFileTreeAccessor.ChangeType.Add:
|
|
||||||
{
|
|
||||||
if (files.Any(f => f.WorkPath == c.File.WorkPath))
|
|
||||||
{
|
|
||||||
if (hard) throw new InvalidProgramException(
|
|
||||||
$"Can not create an existed record into version control: {c.File.WorkPath}");
|
|
||||||
|
|
||||||
Console.WriteLine($"Can not create an existed record into version control...Ignored: {c.File.WorkPath}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
files.Add(c.File);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case LocalFileTreeAccessor.ChangeType.Remove:
|
|
||||||
{
|
|
||||||
var idx = files.FindIndex(f => f.WorkPath == c.File.WorkPath);
|
|
||||||
if (idx < 0)
|
|
||||||
{
|
|
||||||
if (hard) throw new InvalidProgramException(
|
|
||||||
$"Can not delete a missed record into version control: {c.File.WorkPath}");
|
|
||||||
|
|
||||||
Console.WriteLine($"Can not delete a missed record into version control...Ignored: {c.File.WorkPath}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
files.RemoveAt(idx);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case LocalFileTreeAccessor.ChangeType.Modify:
|
|
||||||
{
|
|
||||||
var idx = files.FindIndex(f => f.WorkPath == c.File.WorkPath);
|
|
||||||
if (idx < 0)
|
|
||||||
{
|
|
||||||
if (hard) throw new InvalidProgramException(
|
|
||||||
$"Can not modify a missed record into version control: {c.File.WorkPath}");
|
|
||||||
|
|
||||||
Console.WriteLine($"Can not modify a missed record into version control...Ignored: {c.File.WorkPath}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
files[idx] = c.File;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask<string?> CreateDepotIntoTempFileAsync(RepositoryModel repo, IEnumerable<WorkspaceFile> depotFiles)
|
|
||||||
{
|
|
||||||
var repoWs = PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name);
|
|
||||||
var commitTempFolder = Directory.CreateTempSubdirectory("FlawlessDepot_");
|
|
||||||
var depotFile = Path.Combine(commitTempFolder.FullName, "depot.bin");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// No UI thread blocked
|
|
||||||
await Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await using var fs = new FileStream(depotFile, FileMode.Create);
|
|
||||||
DataTransformer.CreateAndInsertStandardDepotFile(fs, depotFiles,
|
|
||||||
wf => File.OpenRead(WorkPath.ToPlatformPath(wf.WorkPath, repoWs)));
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
UIHelper.NotifyError(e);
|
|
||||||
Directory.Delete(repoWs, true);
|
|
||||||
Console.WriteLine(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return depotFile;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -50,7 +50,6 @@ public partial class UserService : BaseService<UserService>
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
UIHelper.NotifyError(e);
|
|
||||||
Console.WriteLine(e);
|
Console.WriteLine(e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -81,7 +80,6 @@ public partial class UserService : BaseService<UserService>
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
UIHelper.NotifyError(e);
|
|
||||||
Console.WriteLine(e);
|
Console.WriteLine(e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,96 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
|
||||||
using Avalonia.Controls.Notifications;
|
|
||||||
using Flawless.Client.ViewModels.ModalBox;
|
|
||||||
using Flawless.Client.Views.ModalBox;
|
|
||||||
using Ursa.Controls;
|
|
||||||
using Notification = Ursa.Controls.Notification;
|
|
||||||
using WindowNotificationManager = Ursa.Controls.WindowNotificationManager;
|
|
||||||
|
|
||||||
namespace Flawless.Client;
|
|
||||||
|
|
||||||
public static class UIHelper
|
|
||||||
{
|
|
||||||
|
|
||||||
private static WindowNotificationManager _notificationManager = null!;
|
|
||||||
|
|
||||||
public static WindowNotificationManager Notify
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_notificationManager != null) return _notificationManager!;
|
|
||||||
|
|
||||||
var lf = ((IClassicDesktopStyleApplicationLifetime)App.Current.ApplicationLifetime);
|
|
||||||
if (!WindowNotificationManager.TryGetNotificationManager(lf.MainWindow!, out _notificationManager))
|
|
||||||
throw new Exception("Can not get notification manager");
|
|
||||||
|
|
||||||
_notificationManager!.Position = NotificationPosition.TopCenter;
|
|
||||||
return _notificationManager!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static void NotifyError(Exception ex)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var content = ex.ToString();
|
|
||||||
if (content.Length > 100) content = content.Substring(0, 100) + "...";
|
|
||||||
var nf = new Notification(ex.GetType().Name, content, NotificationType.Error);
|
|
||||||
Notify.Show(nf);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Can not notify error to users: " + e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void NotifyError(string title, string content)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var nf = new Notification(title, content, NotificationType.Error);
|
|
||||||
Notify.Show(nf);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Can not notify error to users: {title} - {content}, {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static Task<DialogResult> SimpleAskAsync(string content, DialogMode mode = DialogMode.None)
|
|
||||||
{
|
|
||||||
var opt = new OverlayDialogOptions
|
|
||||||
{
|
|
||||||
FullScreen = false,
|
|
||||||
Buttons = DialogButton.YesNo,
|
|
||||||
CanResize = false,
|
|
||||||
CanDragMove = false,
|
|
||||||
IsCloseButtonVisible = true,
|
|
||||||
CanLightDismiss = true,
|
|
||||||
Mode = mode
|
|
||||||
};
|
|
||||||
|
|
||||||
var vm = new SimpleMessageDialogViewModel(content);
|
|
||||||
return OverlayDialog.ShowModal<SimpleMessageDialogView, SimpleMessageDialogViewModel>(vm, AppDefaultValues.HostId, opt);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task SimpleAlert(string content, DialogMode mode = DialogMode.Error)
|
|
||||||
{
|
|
||||||
var opt = new OverlayDialogOptions
|
|
||||||
{
|
|
||||||
FullScreen = false,
|
|
||||||
Buttons = DialogButton.YesNo,
|
|
||||||
CanResize = false,
|
|
||||||
CanDragMove = false,
|
|
||||||
IsCloseButtonVisible = true,
|
|
||||||
CanLightDismiss = true,
|
|
||||||
Mode = mode
|
|
||||||
};
|
|
||||||
|
|
||||||
var vm = new SimpleMessageDialogViewModel(content);
|
|
||||||
return OverlayDialog.ShowModal<SimpleMessageDialogView, SimpleMessageDialogViewModel>(vm, AppDefaultValues.HostId, opt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -109,7 +109,7 @@ public partial class HomeViewModel : ViewModelBase, IRoutableViewModel
|
|||||||
if (mr == DialogResult.Yes)
|
if (mr == DialogResult.Yes)
|
||||||
{
|
{
|
||||||
if (await RepositoryService.C.DeleteFromDiskAsync(_selectedRepository))
|
if (await RepositoryService.C.DeleteFromDiskAsync(_selectedRepository))
|
||||||
await RepositoryService.C.UpdateRepositoriesDownloadedStatusFromDiskAsync();
|
await RepositoryService.C.UpdateDownloadedStatusFromDiskAsync(_selectedRepository);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,8 @@ public partial class LoginPageViewModel : ViewModelBase, IRoutableViewModel
|
|||||||
[Reactive] private string _username = "cardidi";
|
[Reactive] private string _username = "cardidi";
|
||||||
|
|
||||||
[Reactive] private string _password = "4453A2b33";
|
[Reactive] private string _password = "4453A2b33";
|
||||||
|
|
||||||
|
[Reactive(SetModifier = AccessModifier.Protected)] private string _issue = String.Empty;
|
||||||
|
|
||||||
public IObservable<bool> CanLogin;
|
public IObservable<bool> CanLogin;
|
||||||
|
|
||||||
@ -54,12 +56,12 @@ public partial class LoginPageViewModel : ViewModelBase, IRoutableViewModel
|
|||||||
catch (ApiException ex)
|
catch (ApiException ex)
|
||||||
{
|
{
|
||||||
await Console.Error.WriteLineAsync($"Login as '{Username}' Failed: {ex.Content}");
|
await Console.Error.WriteLineAsync($"Login as '{Username}' Failed: {ex.Content}");
|
||||||
UIHelper.NotifyError(ex);
|
Issue = ex.Content ?? String.Empty;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await Console.Error.WriteLineAsync($"Login as '{Username}' Failed: {ex}");
|
await Console.Error.WriteLineAsync($"Login as '{Username}' Failed: {ex}");
|
||||||
UIHelper.NotifyError(ex);
|
Issue = ex.Message;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine($"Login as '{Username}' success!");
|
Console.WriteLine($"Login as '{Username}' success!");
|
||||||
|
|||||||
@ -23,6 +23,8 @@ public partial class RegisterPageViewModel : ViewModelBase, IRoutableViewModel
|
|||||||
|
|
||||||
[Reactive] private string _password;
|
[Reactive] private string _password;
|
||||||
|
|
||||||
|
[Reactive(SetModifier = AccessModifier.Protected)] private string _issue;
|
||||||
|
|
||||||
public RegisterPageViewModel(IScreen hostScreen)
|
public RegisterPageViewModel(IScreen hostScreen)
|
||||||
{
|
{
|
||||||
HostScreen = hostScreen;
|
HostScreen = hostScreen;
|
||||||
@ -47,12 +49,12 @@ public partial class RegisterPageViewModel : ViewModelBase, IRoutableViewModel
|
|||||||
catch (ApiException ex)
|
catch (ApiException ex)
|
||||||
{
|
{
|
||||||
await Console.Error.WriteLineAsync($"Register as '{Username}' Failed: {ex.Content}");
|
await Console.Error.WriteLineAsync($"Register as '{Username}' Failed: {ex.Content}");
|
||||||
UIHelper.NotifyError(ex);
|
Issue = ex.Content ?? String.Empty;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await Console.Error.WriteLineAsync($"Register as '{Username}' Failed: {ex}");
|
await Console.Error.WriteLineAsync($"Register as '{Username}' Failed: {ex}");
|
||||||
UIHelper.NotifyError(ex);
|
Issue = ex.Message;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine($"Register as '{Username}' success!");
|
Console.WriteLine($"Register as '{Username}' success!");
|
||||||
|
|||||||
@ -4,16 +4,13 @@ using System.Collections.ObjectModel;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using System.Reactive.Threading.Tasks;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.Models.TreeDataGrid;
|
using Avalonia.Controls.Models.TreeDataGrid;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using DynamicData.Binding;
|
using DynamicData.Binding;
|
||||||
using Flawless.Abstraction;
|
|
||||||
using Flawless.Client.Models;
|
using Flawless.Client.Models;
|
||||||
using Flawless.Client.Service;
|
using Flawless.Client.Service;
|
||||||
using Flawless.Core.Modal;
|
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using ReactiveUI.SourceGenerators;
|
using ReactiveUI.SourceGenerators;
|
||||||
using ChangeType = Flawless.Client.Service.LocalFileTreeAccessor.ChangeType;
|
using ChangeType = Flawless.Client.Service.LocalFileTreeAccessor.ChangeType;
|
||||||
@ -67,38 +64,6 @@ public class LocalChangesNode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CommitTransitNode
|
|
||||||
{
|
|
||||||
public required string Guid { get; set; }
|
|
||||||
|
|
||||||
public required string Author { get; set; }
|
|
||||||
|
|
||||||
public required string Message { get; set; }
|
|
||||||
|
|
||||||
public required DateTime? CommitAt { get; set; }
|
|
||||||
|
|
||||||
public static CommitTransitNode FromCommit(RepositoryModel.Commit cm)
|
|
||||||
{
|
|
||||||
string msg;
|
|
||||||
if (cm.Message.Length > 28)
|
|
||||||
{
|
|
||||||
msg = cm.Message.Substring(0, 28) + "...";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
msg = cm.Message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CommitTransitNode
|
|
||||||
{
|
|
||||||
Guid = cm.CommitId.ToString(),
|
|
||||||
Author = cm.Author,
|
|
||||||
CommitAt = cm.CommittedOn.ToLocalTime(),
|
|
||||||
Message = msg,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class RepositoryViewModel : RoutableViewModelBase
|
public partial class RepositoryViewModel : RoutableViewModelBase
|
||||||
{
|
{
|
||||||
public RepositoryModel Repository { get; }
|
public RepositoryModel Repository { get; }
|
||||||
@ -107,14 +72,8 @@ public partial class RepositoryViewModel : RoutableViewModelBase
|
|||||||
|
|
||||||
public HierarchicalTreeDataGridSource<LocalChangesNode> LocalChange { get; }
|
public HierarchicalTreeDataGridSource<LocalChangesNode> LocalChange { get; }
|
||||||
|
|
||||||
public HierarchicalTreeDataGridSource<LocalChangesNode> FileTree { get; }
|
|
||||||
|
|
||||||
public FlatTreeDataGridSource<CommitTransitNode> Commits { get; }
|
|
||||||
|
|
||||||
public ObservableCollection<LocalChangesNode> LocalChangeSetRaw { get; } = new();
|
public ObservableCollection<LocalChangesNode> LocalChangeSetRaw { get; } = new();
|
||||||
|
|
||||||
public ObservableCollection<LocalChangesNode> CurrentCommitFileTreeRaw { get; } = new();
|
|
||||||
|
|
||||||
public UserModel User { get; }
|
public UserModel User { get; }
|
||||||
|
|
||||||
[Reactive] private bool _autoDetectChanges = true;
|
[Reactive] private bool _autoDetectChanges = true;
|
||||||
@ -132,6 +91,12 @@ public partial class RepositoryViewModel : RoutableViewModelBase
|
|||||||
Repository.Members.ObserveCollectionChanges().Subscribe(_ => RefreshRepositoryRoleInfo());
|
Repository.Members.ObserveCollectionChanges().Subscribe(_ => RefreshRepositoryRoleInfo());
|
||||||
|
|
||||||
// Setup local change set
|
// Setup local change set
|
||||||
|
LocalChangeSetRaw.Add(new LocalChangesNode
|
||||||
|
{
|
||||||
|
Type = "Add",
|
||||||
|
FullPath = "test.md",
|
||||||
|
ModifiedTime = DateTime.Now,
|
||||||
|
});
|
||||||
LocalChange = new HierarchicalTreeDataGridSource<LocalChangesNode>(LocalChangeSetRaw)
|
LocalChange = new HierarchicalTreeDataGridSource<LocalChangesNode>(LocalChangeSetRaw)
|
||||||
{
|
{
|
||||||
Columns =
|
Columns =
|
||||||
@ -139,15 +104,15 @@ public partial class RepositoryViewModel : RoutableViewModelBase
|
|||||||
new CheckBoxColumn<LocalChangesNode>(
|
new CheckBoxColumn<LocalChangesNode>(
|
||||||
string.Empty, n => n.Included, (n, v) => n.Included = v),
|
string.Empty, n => n.Included, (n, v) => n.Included = v),
|
||||||
|
|
||||||
|
new TextColumn<LocalChangesNode, string>(
|
||||||
|
"Change",
|
||||||
|
n => n.Contents != null ? String.Empty : n.Type),
|
||||||
|
|
||||||
new HierarchicalExpanderColumn<LocalChangesNode>(
|
new HierarchicalExpanderColumn<LocalChangesNode>(
|
||||||
new TextColumn<LocalChangesNode, string>(
|
new TextColumn<LocalChangesNode, string>(
|
||||||
"Name",
|
"Name",
|
||||||
n => Path.GetFileName(n.FullPath)),
|
n => Path.GetFileName(n.FullPath)),
|
||||||
n => n.Contents),
|
n => n.Contents),
|
||||||
|
|
||||||
new TextColumn<LocalChangesNode, string>(
|
|
||||||
"Change",
|
|
||||||
n => n.Contents != null ? String.Empty : n.Type.ToString()),
|
|
||||||
|
|
||||||
new TextColumn<LocalChangesNode, string>(
|
new TextColumn<LocalChangesNode, string>(
|
||||||
"File Type",
|
"File Type",
|
||||||
@ -161,153 +126,9 @@ public partial class RepositoryViewModel : RoutableViewModelBase
|
|||||||
"ModifiedTime", n => n.ModifiedTime.HasValue ? n.ModifiedTime.Value.ToLocalTime() : null),
|
"ModifiedTime", n => n.ModifiedTime.HasValue ? n.ModifiedTime.Value.ToLocalTime() : null),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Commits = new FlatTreeDataGridSource<CommitTransitNode>(Repository.Commits.Select(CommitTransitNode.FromCommit))
|
|
||||||
{
|
|
||||||
Columns =
|
|
||||||
{
|
|
||||||
new TextColumn<CommitTransitNode, string>(
|
|
||||||
string.Empty, n => n.Guid == LocalDatabase.CurrentCommit.ToString() ? "*" : String.Empty),
|
|
||||||
|
|
||||||
new TextColumn<CommitTransitNode, string>(
|
|
||||||
"Message", x => x.Message),
|
|
||||||
|
|
||||||
new TextColumn<CommitTransitNode, string>(
|
|
||||||
"Author", x => x.Author),
|
|
||||||
|
|
||||||
new TextColumn<CommitTransitNode, DateTime>(
|
|
||||||
"Time", x => x.CommitAt!.Value),
|
|
||||||
|
|
||||||
new TextColumn<CommitTransitNode, string>(
|
|
||||||
"Id", x => x.Guid.Substring(0, 13)),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
FileTree = new HierarchicalTreeDataGridSource<LocalChangesNode>(CurrentCommitFileTreeRaw)
|
|
||||||
{
|
|
||||||
Columns =
|
|
||||||
{
|
|
||||||
new HierarchicalExpanderColumn<LocalChangesNode>(
|
|
||||||
new TextColumn<LocalChangesNode, string>(
|
|
||||||
"Name",
|
|
||||||
n => Path.GetFileName(n.FullPath)),
|
|
||||||
n => n.Contents),
|
|
||||||
|
|
||||||
new TextColumn<LocalChangesNode, string>(
|
|
||||||
"File Type",
|
|
||||||
n => n.Contents != null ? "Folder" : Path.GetExtension(n.FullPath)),
|
|
||||||
|
|
||||||
new TextColumn<LocalChangesNode, ulong>(
|
|
||||||
"Size",
|
|
||||||
n => 0),
|
|
||||||
|
|
||||||
new TextColumn<LocalChangesNode, DateTime?>(
|
|
||||||
"ModifiedTime", n => n.ModifiedTime.HasValue ? n.ModifiedTime.Value.ToLocalTime() : null),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_ = StartupTasksAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task StartupTasksAsync()
|
|
||||||
{
|
|
||||||
await DetectLocalChangesAsyncCommand.Execute();
|
|
||||||
await RendererFileTreeAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ValueTask RendererFileTreeAsync()
|
|
||||||
{
|
|
||||||
if (LocalDatabase.RepoAccessor == null) return;
|
|
||||||
var accessor = LocalDatabase.RepoAccessor;
|
|
||||||
var nodes = await CalculateFileTreeOfChangesNodeAsync(accessor.Select(
|
|
||||||
f => new LocalFileTreeAccessor.ChangeRecord(ChangeType.Add, f)));
|
|
||||||
|
|
||||||
CurrentCommitFileTreeRaw.Clear();
|
// Do refresh when entered
|
||||||
CurrentCommitFileTreeRaw.AddRange(nodes);
|
// DetectLocalChangesAsyncCommand.Execute();
|
||||||
}
|
|
||||||
|
|
||||||
private void CollectChanges(List<LocalFileTreeAccessor.ChangeRecord> store, IEnumerable<LocalChangesNode> changesNode)
|
|
||||||
{
|
|
||||||
foreach (var n in changesNode)
|
|
||||||
{
|
|
||||||
if (n.Contents != null) CollectChanges(store, n.Contents);
|
|
||||||
else if (n.Included)
|
|
||||||
{
|
|
||||||
store.Add(new LocalFileTreeAccessor.ChangeRecord(Enum.Parse<ChangeType>(n.Type), new WorkspaceFile
|
|
||||||
{
|
|
||||||
WorkPath = n.FullPath,
|
|
||||||
ModifyTime = n.ModifiedTime!.Value
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task<List<LocalChangesNode>> CalculateFileTreeOfChangesNodeAsync(IEnumerable<LocalFileTreeAccessor.ChangeRecord> changesNode)
|
|
||||||
{
|
|
||||||
return Task.Run(() =>
|
|
||||||
{
|
|
||||||
// Generate a map of all folders
|
|
||||||
var folderMap = new Dictionary<string, LocalChangesNode>();
|
|
||||||
var nodes = new List<LocalChangesNode>();
|
|
||||||
|
|
||||||
foreach (var file in changesNode)
|
|
||||||
{
|
|
||||||
var n = LocalChangesNode.FromWorkspaceFile(file);
|
|
||||||
var parentNode = AddParentToMap(file.File.WorkPath);
|
|
||||||
if (parentNode == null) nodes.Add(n);
|
|
||||||
else parentNode.Contents!.Add(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodes;
|
|
||||||
|
|
||||||
LocalChangesNode? AddParentToMap(string path)
|
|
||||||
{
|
|
||||||
path = WorkPath.FormatPathDirectorySeparator(Path.GetDirectoryName(path) ?? string.Empty);
|
|
||||||
|
|
||||||
// 如果为空,则其直接文件夹就是根目录
|
|
||||||
if (string.IsNullOrEmpty(path)) return null;
|
|
||||||
|
|
||||||
// 如果直接文件夹已经存在,则不再生成,直接返回即可
|
|
||||||
if (folderMap.TryGetValue(path, out var node)) return node;
|
|
||||||
|
|
||||||
// 生成当前文件夹,并先找到这个文件夹的直接文件夹
|
|
||||||
node = LocalChangesNode.FromFolder(path);
|
|
||||||
var parent = AddParentToMap(path);
|
|
||||||
folderMap.Add(path, node);
|
|
||||||
if (parent != null) parent.Contents!.Add(node);
|
|
||||||
else nodes.Add(node);
|
|
||||||
|
|
||||||
// 将新建的文件夹告知调用方
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[ReactiveCommand]
|
|
||||||
private async Task CommitSelectedChangesAsync()
|
|
||||||
{
|
|
||||||
var changes = new List<LocalFileTreeAccessor.ChangeRecord>();
|
|
||||||
CollectChanges(changes, LocalChangeSetRaw);
|
|
||||||
|
|
||||||
if (changes.Count == 0)
|
|
||||||
{
|
|
||||||
await UIHelper.SimpleAlert("You haven't choose any changes yet!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(LocalDatabase.CommitMessage))
|
|
||||||
{
|
|
||||||
await UIHelper.SimpleAlert("Commit message can not be empty!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var manifest = await RepositoryService.C.CommitWorkspaceAsBaselineAsync(Repository, changes, LocalDatabase.CommitMessage!);
|
|
||||||
if (manifest == null) return;
|
|
||||||
|
|
||||||
LocalDatabase.LocalAccessor.SetBaseline(manifest.Value.FilePaths);
|
|
||||||
LocalDatabase.CommitMessage = string.Empty;
|
|
||||||
await DetectLocalChangesAsyncCommand.Execute();
|
|
||||||
await RendererFileTreeAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[ReactiveCommand]
|
[ReactiveCommand]
|
||||||
@ -334,28 +155,37 @@ public partial class RepositoryViewModel : RoutableViewModelBase
|
|||||||
[ReactiveCommand]
|
[ReactiveCommand]
|
||||||
private async ValueTask DetectLocalChangesAsync()
|
private async ValueTask DetectLocalChangesAsync()
|
||||||
{
|
{
|
||||||
var ns = await Task.Run(async () =>
|
var ns = await Task.Run(() =>
|
||||||
{
|
{
|
||||||
LocalDatabase.LocalAccessor.Refresh();
|
LocalDatabase.LocalAccessor.Refresh();
|
||||||
return await CalculateFileTreeOfChangesNodeAsync(LocalDatabase.LocalAccessor.Changes.Values);
|
|
||||||
|
// Generate a map of all folders
|
||||||
|
var folderMap = new Dictionary<string, LocalChangesNode>();
|
||||||
|
foreach (var k in LocalDatabase.LocalAccessor.Changes.Keys)
|
||||||
|
AddParentToMap(k);
|
||||||
|
|
||||||
|
var nodes = new List<LocalChangesNode>();
|
||||||
|
foreach (var file in LocalDatabase.LocalAccessor.Changes.Values)
|
||||||
|
{
|
||||||
|
var directory = Path.GetDirectoryName(file.File.WorkPath);
|
||||||
|
var n = LocalChangesNode.FromWorkspaceFile(file);
|
||||||
|
if (string.IsNullOrEmpty(directory)) nodes.Add(n);
|
||||||
|
else folderMap[directory].Contents!.Add(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.AddRange(folderMap.Values);
|
||||||
|
return nodes;
|
||||||
|
|
||||||
|
void AddParentToMap(string path)
|
||||||
|
{
|
||||||
|
var parent = Path.GetDirectoryName(path);
|
||||||
|
if (string.IsNullOrEmpty(parent) || folderMap.ContainsKey(parent)) return;
|
||||||
|
|
||||||
|
folderMap.Add(parent, LocalChangesNode.FromFolder(parent));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
LocalChangeSetRaw.Clear();
|
LocalChangeSetRaw.Clear();
|
||||||
LocalChangeSetRaw.AddRange(ns);
|
LocalChangeSetRaw.AddRange(ns);
|
||||||
}
|
}
|
||||||
|
|
||||||
[ReactiveCommand]
|
|
||||||
private void SelectAllChanges()
|
|
||||||
{
|
|
||||||
foreach (var n in LocalChangeSetRaw)
|
|
||||||
n.Included = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[ReactiveCommand]
|
|
||||||
private void DeselectAllChanges()
|
|
||||||
{
|
|
||||||
foreach (var n in LocalChangeSetRaw)
|
|
||||||
n.Included = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -14,6 +14,8 @@ public partial class ServerSetupPageViewModel : RoutableViewModelBase
|
|||||||
|
|
||||||
[Reactive] private string _host = "http://localhost:5256/";
|
[Reactive] private string _host = "http://localhost:5256/";
|
||||||
|
|
||||||
|
[Reactive(SetModifier = AccessModifier.Protected)] private string? _issue;
|
||||||
|
|
||||||
public IObservable<bool> CanSetHost { get; }
|
public IObservable<bool> CanSetHost { get; }
|
||||||
|
|
||||||
public ServerSetupPageViewModel(IScreen hostScreen) : base(hostScreen)
|
public ServerSetupPageViewModel(IScreen hostScreen) : base(hostScreen)
|
||||||
@ -31,18 +33,19 @@ public partial class ServerSetupPageViewModel : RoutableViewModelBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
Issue = string.Empty;
|
||||||
await Api.C.SetGatewayAsync(Host);
|
await Api.C.SetGatewayAsync(Host);
|
||||||
HostScreen.Router.Navigate.Execute(new LoginPageViewModel(HostScreen));
|
HostScreen.Router.Navigate.Execute(new LoginPageViewModel(HostScreen));
|
||||||
}
|
}
|
||||||
catch (ApiException ex)
|
catch (ApiException ex)
|
||||||
{
|
{
|
||||||
await Console.Error.WriteLineAsync("Can not connect to server: " + ex.ToString());
|
await Console.Error.WriteLineAsync("Can not connect to server: " + ex.ToString());
|
||||||
UIHelper.NotifyError(ex);
|
Issue = ex.Content;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await Console.Error.WriteLineAsync("Can not connect to server: " + ex.ToString());
|
await Console.Error.WriteLineAsync("Can not connect to server: " + ex.ToString());
|
||||||
UIHelper.NotifyError(ex);
|
Issue = ex.Message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,5 +20,6 @@
|
|||||||
<Button Content="Login" Command="{Binding LoginCommand}"/>
|
<Button Content="Login" Command="{Binding LoginCommand}"/>
|
||||||
<Button Content="Register" Command="{Binding RegisterCommand}"/>
|
<Button Content="Register" Command="{Binding RegisterCommand}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
<Label Content="{Binding Issue}" Foreground="Red"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@ -20,5 +20,6 @@
|
|||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="{DynamicResource Semi}">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="{DynamicResource Semi}">
|
||||||
<Button Content="Register" Command="{Binding RegisterCommand}"/>
|
<Button Content="Register" Command="{Binding RegisterCommand}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
<Label Content="{Binding Issue}" Foreground="Red"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -16,5 +16,6 @@
|
|||||||
<StackPanel Spacing="10" HorizontalAlignment="Stretch" VerticalAlignment="Center">
|
<StackPanel Spacing="10" HorizontalAlignment="Stretch" VerticalAlignment="Center">
|
||||||
<TextBox Watermark="Host" Text="{Binding Host, Mode=TwoWay}"/>
|
<TextBox Watermark="Host" Text="{Binding Host, Mode=TwoWay}"/>
|
||||||
<Button Content="Connect" Command="{Binding SetHostCommand}"/>
|
<Button Content="Connect" Command="{Binding SetHostCommand}"/>
|
||||||
|
<Label Content="{Binding Issue}" Foreground="Red"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@ -21,6 +21,5 @@
|
|||||||
<Panel>
|
<Panel>
|
||||||
<rxui:RoutedViewHost Router="{Binding Router}"/>
|
<rxui:RoutedViewHost Router="{Binding Router}"/>
|
||||||
<ursa:OverlayDialogHost HostId="Overlay"/>
|
<ursa:OverlayDialogHost HostId="Overlay"/>
|
||||||
<ursa:WindowNotificationManager/>
|
|
||||||
</Panel>
|
</Panel>
|
||||||
</ursa:UrsaWindow>
|
</ursa:UrsaWindow>
|
||||||
@ -7,7 +7,8 @@
|
|||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Flawless.Client.Views.RepositoryPage.RepoCommitPageView">
|
x:Class="Flawless.Client.Views.RepositoryPage.RepoCommitPageView">
|
||||||
<Grid ColumnDefinitions="2*, *">
|
<Grid ColumnDefinitions="2*, *">
|
||||||
<TreeDataGrid Grid.Column="0" Source="{Binding Commits}"/>
|
<TreeDataGrid Grid.Column="0">
|
||||||
|
</TreeDataGrid>
|
||||||
<Border Grid.Column="1" Classes="Shadow" Theme="{StaticResource CardBorder}">
|
<Border Grid.Column="1" Classes="Shadow" Theme="{StaticResource CardBorder}">
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
|
|||||||
@ -10,9 +10,15 @@
|
|||||||
|
|
||||||
<Grid ColumnDefinitions="2*, *">
|
<Grid ColumnDefinitions="2*, *">
|
||||||
<Border Grid.Column="0" Classes="Shadow" Theme="{StaticResource CardBorder}">
|
<Border Grid.Column="0" Classes="Shadow" Theme="{StaticResource CardBorder}">
|
||||||
<ScrollViewer Grid.Row="1">
|
<Grid RowDefinitions="Auto, *">
|
||||||
<SelectableTextBlock Text="{Binding Repository.Description}"/>
|
<StackPanel Orientation="Horizontal" Spacing="8"
|
||||||
</ScrollViewer>
|
IsVisible="{Binding IsOwnerRole}">
|
||||||
|
<u:IconButton Icon="{StaticResource SemiIconEdit}" Content="Edit"/>
|
||||||
|
</StackPanel>
|
||||||
|
<ScrollViewer Grid.Row="1">
|
||||||
|
<SelectableTextBlock Text="{Binding Repository.Description}"/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
<ScrollViewer Grid.Column="1" Margin="36 20">
|
<ScrollViewer Grid.Column="1" Margin="36 20">
|
||||||
<StackPanel Orientation="Vertical">
|
<StackPanel Orientation="Vertical">
|
||||||
|
|||||||
@ -7,14 +7,14 @@
|
|||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Flawless.Client.Views.RepositoryPage.RepoFileTreePageView">
|
x:Class="Flawless.Client.Views.RepositoryPage.RepoFileTreePageView">
|
||||||
<Grid ColumnDefinitions="2*, *">
|
<Grid ColumnDefinitions="2*, *">
|
||||||
<TreeDataGrid Grid.Column="0" Grid.ColumnSpan="2" Source="{Binding FileTree}">
|
<TreeDataGrid Grid.Column="0">
|
||||||
</TreeDataGrid>
|
</TreeDataGrid>
|
||||||
<!-- <Border Grid.Column="1" Classes="Shadow" Theme="{StaticResource CardBorder}"> -->
|
<Border Grid.Column="1" Classes="Shadow" Theme="{StaticResource CardBorder}">
|
||||||
<!-- <ScrollViewer> -->
|
<ScrollViewer>
|
||||||
<!-- <StackPanel Spacing="4"> -->
|
<StackPanel Spacing="4">
|
||||||
<!-- <Label Content="File History"/> -->
|
<Label Content="File History"/>
|
||||||
<!-- </StackPanel> -->
|
</StackPanel>
|
||||||
<!-- </ScrollViewer> -->
|
</ScrollViewer>
|
||||||
<!-- </Border> -->
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@ -6,26 +6,7 @@
|
|||||||
x:DataType="vm:RepositoryViewModel"
|
x:DataType="vm:RepositoryViewModel"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Flawless.Client.Views.RepositoryPage.RepoSettingPageView">
|
x:Class="Flawless.Client.Views.RepositoryPage.RepoSettingPageView">
|
||||||
<TabControl TabStripPlacement="Left">
|
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||||
<TabItem Header="Members">
|
<Label Content="Sit down and wait patience."></Label>
|
||||||
<StackPanel Width="400" HorizontalAlignment="Stretch">
|
</StackPanel>
|
||||||
|
|
||||||
</StackPanel>
|
|
||||||
</TabItem>
|
|
||||||
<TabItem Header="Statics" IsVisible="{Binding IsDeveloperRole}">
|
|
||||||
<StackPanel Width="400" HorizontalAlignment="Stretch">
|
|
||||||
|
|
||||||
</StackPanel>
|
|
||||||
</TabItem>
|
|
||||||
<TabItem Header="Admin Area" IsVisible="{Binding IsOwnerRole}">
|
|
||||||
<StackPanel Width="400" HorizontalAlignment="Stretch">
|
|
||||||
|
|
||||||
</StackPanel>
|
|
||||||
</TabItem>
|
|
||||||
<TabItem Header="Hooks" IsVisible="{Binding IsOwnerRole}">
|
|
||||||
<StackPanel Width="400" HorizontalAlignment="Stretch">
|
|
||||||
|
|
||||||
</StackPanel>
|
|
||||||
</TabItem>
|
|
||||||
</TabControl>
|
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@ -12,9 +12,7 @@
|
|||||||
<u:IconButton Icon="{StaticResource SemiIconRefresh}" Content="Detect"
|
<u:IconButton Icon="{StaticResource SemiIconRefresh}" Content="Detect"
|
||||||
Command="{Binding DetectLocalChangesAsyncCommand}"/>
|
Command="{Binding DetectLocalChangesAsyncCommand}"/>
|
||||||
<u:IconButton Icon="{StaticResource SemiIconDownload}" Content="Pull"/>
|
<u:IconButton Icon="{StaticResource SemiIconDownload}" Content="Pull"/>
|
||||||
<u:IconButton Icon="{StaticResource SemiIconCheckList}" Content="Select"/>
|
<ToggleButton Content="Auto Refresh" IsChecked="{Binding AutoDetectChanges}"/>
|
||||||
<u:IconButton Icon="{StaticResource SemiIconList}" Content="Deselect"/>
|
|
||||||
<!-- <ToggleButton Content="Auto Refresh" IsChecked="{Binding AutoDetectChanges}"/> -->
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TreeDataGrid Grid.Row="1" Grid.Column="0" Source="{Binding LocalChange}" CanUserSortColumns="True"/>
|
<TreeDataGrid Grid.Row="1" Grid.Column="0" Source="{Binding LocalChange}" CanUserSortColumns="True"/>
|
||||||
<Border Grid.Row="1" Grid.Column="2" Classes="Shadow" Theme="{StaticResource CardBorder}">
|
<Border Grid.Row="1" Grid.Column="2" Classes="Shadow" Theme="{StaticResource CardBorder}">
|
||||||
@ -22,10 +20,10 @@
|
|||||||
<u:Form HorizontalAlignment="Stretch">
|
<u:Form HorizontalAlignment="Stretch">
|
||||||
<TextBox Text="{Binding LocalDatabase.CommitMessage}"
|
<TextBox Text="{Binding LocalDatabase.CommitMessage}"
|
||||||
Classes="TextArea" MaxHeight="300" Watermark="Description of this commit"/>
|
Classes="TextArea" MaxHeight="300" Watermark="Description of this commit"/>
|
||||||
<SplitButton HorizontalAlignment="Stretch" Content="Commit" Command="{Binding CommitSelectedChangesCommand}">
|
<SplitButton HorizontalAlignment="Stretch" Content="Commit">
|
||||||
<SplitButton.Flyout>
|
<SplitButton.Flyout>
|
||||||
<MenuFlyout Placement="TopEdgeAlignedRight">
|
<MenuFlyout Placement="TopEdgeAlignedRight">
|
||||||
<MenuItem Header="Force Commit as Baseline" Command="{Binding CommitSelectedChangesCommand}"/>
|
<MenuItem Header="Force Commit as Baseline"/>
|
||||||
</MenuFlyout>
|
</MenuFlyout>
|
||||||
</SplitButton.Flyout>
|
</SplitButton.Flyout>
|
||||||
</SplitButton>
|
</SplitButton>
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
<TabControl TabStripPlacement="Top" Margin="0 20 0 0">
|
<TabControl TabStripPlacement="Top" Margin="0 20 0 0">
|
||||||
<TabItem>
|
<TabItem>
|
||||||
<TabItem.Header>
|
<TabItem.Header>
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
||||||
<PathIcon MaxHeight="14" MaxWidth="14" Data="{StaticResource SemiIconBox}"/>
|
<PathIcon MaxHeight="14" MaxWidth="14" Data="{StaticResource SemiIconBox}"/>
|
||||||
<Label Content="Dashboard"/>
|
<Label Content="Dashboard"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@ -8,9 +8,9 @@ public class CommitRequest
|
|||||||
|
|
||||||
public required string Message { get; set; }
|
public required string Message { get; set; }
|
||||||
|
|
||||||
public required List<string> WorkspaceSnapshot { get; set; }
|
public required WorkspaceFile[] WorkspaceSnapshot { get; set; }
|
||||||
|
|
||||||
public List<string>? RequiredDepots { get; set; }
|
public string[]? RequiredDepots { get; set; }
|
||||||
|
|
||||||
public string? MainDepotId { get; set; } // If commit is not modify files, but changes workspace files (Delete) We will not require a main depot id.
|
public string? MainDepotId { get; set; } // If commit is not modify files, but changes workspace files (Delete) We will not require a main depot id.
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
namespace Flawless.Communication.Response;
|
namespace Flawless.Communication.Response;
|
||||||
|
|
||||||
public record CommitSuccessResponse(DateTime CommittedOn, Guid CommitId, Guid MainDepotId);
|
public record CommitSuccessResponse(DateTime CommittedOn, Guid CommitId);
|
||||||
@ -16,133 +16,7 @@ public static class DataTransformer
|
|||||||
|
|
||||||
return (byte)val;
|
return (byte)val;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void CreateAndInsertStandardDepotFile
|
|
||||||
(Stream depotStream, IEnumerable<WorkspaceFile> workfiles, Func<WorkspaceFile, Stream> payloadLocator)
|
|
||||||
{
|
|
||||||
if (!depotStream.CanWrite || !depotStream.CanRead) throw new IOException("Depot stream is not writable/readable.");
|
|
||||||
if (payloadLocator == null) throw new ArgumentNullException(nameof(payloadLocator));
|
|
||||||
|
|
||||||
long headerStart = depotStream.Position;
|
|
||||||
long payloadStart = 0;
|
|
||||||
long payloadEnd = 0;
|
|
||||||
long fileMapStart = 0;
|
|
||||||
long fileMapEnd = 0;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var writer = new BinaryWriter(depotStream, Encoding.ASCII, true);
|
|
||||||
|
|
||||||
writer.Write(StandardDepotHeaderV1.FormatMagicNumber);
|
|
||||||
writer.Write((uint) 0);
|
|
||||||
writer.Write((byte) 1); // Crc of header - later
|
|
||||||
writer.Write((byte) CompressType.Raw);
|
|
||||||
writer.Write((ushort) 0); // Preserved
|
|
||||||
writer.Write((uint) 0); // Preserved
|
|
||||||
writer.Write((ulong) 0); // Hash of this file - later
|
|
||||||
writer.Write((ulong) 0);
|
|
||||||
writer.Write(DateTime.UtcNow.ToBinary());
|
|
||||||
writer.Write((ulong) 0); // Filemap Size - later
|
|
||||||
writer.Write((ulong) 0); // Payload Size - later
|
|
||||||
writer.Write((ulong) 0); // Preserved
|
|
||||||
|
|
||||||
writer.Flush();
|
|
||||||
}
|
|
||||||
catch (EndOfStreamException e)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException("Stream is too small! Maybe file is broken.", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Write files into binary
|
|
||||||
List<DepotFileInfo> fileinfos = new List<DepotFileInfo>();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Payload start at here
|
|
||||||
payloadStart = depotStream.Position;
|
|
||||||
|
|
||||||
foreach (var wf in workfiles)
|
|
||||||
{
|
|
||||||
var startPos = depotStream.Position;
|
|
||||||
using var rs = payloadLocator(wf);
|
|
||||||
|
|
||||||
rs.CopyTo(depotStream);
|
|
||||||
fileinfos.Add(new DepotFileInfo(
|
|
||||||
(ulong)(startPos - payloadStart),
|
|
||||||
(ulong)(depotStream.Position - startPos),
|
|
||||||
wf.ModifyTime, wf.WorkPath));
|
|
||||||
|
|
||||||
depotStream.Flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
payloadEnd = depotStream.Position;
|
|
||||||
|
|
||||||
// Filemap start at here
|
|
||||||
var splitor = '$';
|
|
||||||
fileMapStart = depotStream.Position;
|
|
||||||
|
|
||||||
using (var writer = new StreamWriter(depotStream, Encoding.UTF8, 1024, true))
|
|
||||||
{
|
|
||||||
// Format: {Path}${Size}${ModifyTime}${Offset}$
|
|
||||||
foreach (var df in fileinfos)
|
|
||||||
{
|
|
||||||
writer.Write(df.Path);
|
|
||||||
writer.Write(splitor);
|
|
||||||
|
|
||||||
writer.Write(df.Size.ToString());
|
|
||||||
writer.Write(splitor);
|
|
||||||
|
|
||||||
writer.Write(df.ModifyTime.ToBinary().ToString());
|
|
||||||
writer.Write(splitor);
|
|
||||||
|
|
||||||
writer.Write(df.Offset.ToString());
|
|
||||||
writer.Write(splitor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
depotStream.Flush();
|
|
||||||
|
|
||||||
fileMapEnd = depotStream.Position;
|
|
||||||
|
|
||||||
}
|
|
||||||
catch (EndOfStreamException e)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException("Stream is too small! Maybe file is broken.", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write rest part of header and calculate crc
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var writer = new BinaryWriter(depotStream, Encoding.ASCII, true);
|
|
||||||
|
|
||||||
ulong payloadSize = (ulong)(payloadEnd - payloadStart);
|
|
||||||
ulong filemapSize = (ulong)(fileMapEnd - fileMapStart);
|
|
||||||
|
|
||||||
// Write fs size
|
|
||||||
depotStream.Seek(headerStart + 40, SeekOrigin.Begin);
|
|
||||||
writer.Write(filemapSize);
|
|
||||||
writer.Write(payloadSize);
|
|
||||||
|
|
||||||
writer.Flush();
|
|
||||||
|
|
||||||
// Calculate CRC
|
|
||||||
depotStream.Seek(headerStart + 8, SeekOrigin.Begin);
|
|
||||||
Span<byte> crcArea = stackalloc byte[64 - 8];
|
|
||||||
if (depotStream.Read(crcArea) != 64 - 8) throw new InvalidDataException("Stream is too short!");
|
|
||||||
var crc = Crc32.HashToUInt32(crcArea);
|
|
||||||
|
|
||||||
// Write CRC
|
|
||||||
depotStream.Seek(headerStart + 4, SeekOrigin.Begin);
|
|
||||||
writer.Write(crc);
|
|
||||||
|
|
||||||
writer.Flush();
|
|
||||||
}
|
|
||||||
catch (EndOfStreamException e)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException("Stream is too small! Maybe file is broken.", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static StandardDepotHeaderV1 ExtractStandardDepotHeaderV1(Stream depotStream)
|
public static StandardDepotHeaderV1 ExtractStandardDepotHeaderV1(Stream depotStream)
|
||||||
{
|
{
|
||||||
@ -202,7 +76,7 @@ public static class DataTransformer
|
|||||||
public static async IAsyncEnumerable<DepotFileInfo> ExtractDepotFileInfoMapAsync(Stream depotStream, ulong fileMapSize)
|
public static async IAsyncEnumerable<DepotFileInfo> ExtractDepotFileInfoMapAsync(Stream depotStream, ulong fileMapSize)
|
||||||
{
|
{
|
||||||
var splitor = '$';
|
var splitor = '$';
|
||||||
depotStream.Seek(-(long)fileMapSize, SeekOrigin.End);
|
depotStream.Seek((long) fileMapSize, SeekOrigin.End);
|
||||||
|
|
||||||
var state = -1;
|
var state = -1;
|
||||||
var buffer = new char[64];
|
var buffer = new char[64];
|
||||||
@ -217,45 +91,41 @@ public static class DataTransformer
|
|||||||
// Read loop
|
// Read loop
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
int length = 0;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
length = await reader.ReadBlockAsync(buffer, 0, 64);
|
var length = await reader.ReadBlockAsync(buffer, 0, 64);
|
||||||
if (length == 0) break;
|
if (length == 0) break;
|
||||||
|
for (int i = 0; i < length; i++)
|
||||||
|
{
|
||||||
|
var c = buffer[i];
|
||||||
|
if (c != splitor) builder.Append(c);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
switch ((state = (state + 1) % 4))
|
||||||
|
{
|
||||||
|
case 0: path = builder.ToString(); break;
|
||||||
|
case 1: size = ulong.Parse(builder.ToString()); break;
|
||||||
|
case 2: modifyTime = DateTime.FromBinary(long.Parse(builder.ToString())); break;
|
||||||
|
case 3: offset = ulong.Parse(builder.ToString()); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (EndOfStreamException e)
|
catch (EndOfStreamException e)
|
||||||
{
|
{
|
||||||
throw new InvalidDataException("Stream is too small! Maybe file is broken.", e);
|
throw new InvalidDataException("Stream is too small! Maybe file is broken.", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < length; i++)
|
|
||||||
{
|
// Do output at here.
|
||||||
var c = buffer[i];
|
yield return new DepotFileInfo(offset, size, modifyTime, path);
|
||||||
if (c != splitor) builder.Append(c);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
switch ((state = (state + 1) % 4))
|
|
||||||
{
|
|
||||||
case 0: path = builder.ToString(); break;
|
|
||||||
case 1: size = ulong.Parse(builder.ToString()); break;
|
|
||||||
case 2: modifyTime = DateTime.FromBinary(long.Parse(builder.ToString())); break;
|
|
||||||
case 3:
|
|
||||||
{
|
|
||||||
offset = ulong.Parse(builder.ToString());
|
|
||||||
yield return new DepotFileInfo(offset, size, modifyTime, path);
|
|
||||||
} break;
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check is this the real ending
|
// Check is this the real ending
|
||||||
if (builder.Length > 0 || (state > 0 && state < 3))
|
if (builder.Length > 0 || state != 0)
|
||||||
throw new InvalidDataException("Stream is too small! Maybe file is broken.");
|
throw new InvalidDataException("Stream is too small! Maybe file is broken.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -33,7 +33,7 @@ namespace Flawless.Core.BinaryDataFormat;
|
|||||||
* 14 : (Preserve)
|
* 14 : (Preserve)
|
||||||
* 15 : (Preserve)
|
* 15 : (Preserve)
|
||||||
* ------------------------------------------------------------
|
* ------------------------------------------------------------
|
||||||
* 16 : Depot MD5 Checksum (From 64 to end, uncompressed)
|
* 16 : Depot MD5 Checksum (From 48 to end, uncompressed)
|
||||||
* 17 : ~
|
* 17 : ~
|
||||||
* 18 : ~
|
* 18 : ~
|
||||||
* 19 : ~
|
* 19 : ~
|
||||||
|
|||||||
@ -1,18 +1,11 @@
|
|||||||
namespace Flawless.Core.Modal;
|
namespace Flawless.Core.Modal;
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public struct WorkspaceFile : IEquatable<WorkspaceFile>
|
public struct WorkspaceFile : IEquatable<WorkspaceFile>
|
||||||
{
|
{
|
||||||
public DateTime ModifyTime { get; set; }
|
public required DateTime ModifyTime;
|
||||||
|
|
||||||
public string WorkPath { get; set; }
|
public required string WorkPath;
|
||||||
|
|
||||||
public WorkspaceFile(DateTime modifyTime, string workPath)
|
|
||||||
{
|
|
||||||
ModifyTime = modifyTime;
|
|
||||||
WorkPath = workPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(WorkspaceFile other)
|
public bool Equals(WorkspaceFile other)
|
||||||
{
|
{
|
||||||
return ModifyTime.Equals(other.ModifyTime) && WorkPath == other.WorkPath;
|
return ModifyTime.Equals(other.ModifyTime) && WorkPath == other.WorkPath;
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
using System.Text.Json;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Flawless.Communication.Request;
|
using Flawless.Communication.Request;
|
||||||
using Flawless.Communication.Response;
|
using Flawless.Communication.Response;
|
||||||
using Flawless.Communication.Shared;
|
using Flawless.Communication.Shared;
|
||||||
@ -7,13 +9,15 @@ using Flawless.Core.Modal;
|
|||||||
using Flawless.Server.Models;
|
using Flawless.Server.Models;
|
||||||
using Flawless.Server.Services;
|
using Flawless.Server.Services;
|
||||||
using Flawless.Server.Utility;
|
using Flawless.Server.Utility;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.OpenApi.Validations.Rules;
|
||||||
|
|
||||||
namespace Flawless.Server.Controllers;
|
namespace Flawless.Server.Controllers;
|
||||||
|
|
||||||
[ApiController, Microsoft.AspNetCore.Authorization.Authorize, Route("api/repo/{userName}/{repositoryName}")]
|
[ApiController, Authorize, Route("api/repo/{userName}/{repositoryName}")]
|
||||||
public class RepositoryInnieController(
|
public class RepositoryInnieController(
|
||||||
UserManager<AppUser> userManager,
|
UserManager<AppUser> userManager,
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
@ -219,30 +223,30 @@ public class RepositoryInnieController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpPost("fetch_manifest")]
|
[HttpGet("fetch_manifest")]
|
||||||
public async Task<ActionResult<CommitManifest>> DownloadManifestAsync(string userName, string repositoryName, [FromQuery] string commitId)
|
[ProducesResponseType<FileStreamResult>(200)]
|
||||||
|
public async Task<IActionResult> DownloadManifestAsync(string userName, string repositoryName, [FromQuery] string commitId)
|
||||||
{
|
{
|
||||||
if (!Guid.TryParse(commitId, out var commitGuid)) return BadRequest(new FailedResponse("Invalid commit id"));
|
if (!Guid.TryParse(commitId, out var commitGuid)) return BadRequest(new FailedResponse("Invalid commit id"));
|
||||||
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
||||||
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Guest);
|
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Guest);
|
||||||
if (grantIssue is not Repository rp) return (ActionResult) grantIssue;
|
if (grantIssue is not Repository rp) return (IActionResult) grantIssue;
|
||||||
|
|
||||||
// Start checkout file.
|
// Start checkout file.
|
||||||
var target = transformer.GetCommitManifestPath(rp.Id, commitGuid);
|
var target = transformer.GetCommitManifestPath(rp.Id, commitGuid);
|
||||||
if (!System.IO.File.Exists(target)) return NotFound(new FailedResponse($"Could not find commit manifest {target}"));
|
if (System.IO.File.Exists(target)) return File(System.IO.File.OpenRead(target), "application/octet-stream");
|
||||||
|
return NotFound(new FailedResponse($"Could not find commit manifest {target}"));
|
||||||
await using var manifest = System.IO.File.OpenRead(target);
|
|
||||||
return await JsonSerializer.DeserializeAsync<CommitManifest>(manifest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpPost("fetch_depot")]
|
[HttpGet("fetch_depot")]
|
||||||
public async Task<ActionResult<Stream>> DownloadDepotAsync(string userName, string repositoryName, [FromQuery] string depotId)
|
[ProducesResponseType<FileStreamResult>(200)]
|
||||||
|
public async Task<IActionResult> DownloadDepotAsync(string userName, string repositoryName, [FromQuery] string depotId)
|
||||||
{
|
{
|
||||||
if (!Guid.TryParse(depotId, out var depotGuid)) return BadRequest(new FailedResponse("Invalid depot id"));
|
if (!Guid.TryParse(depotId, out var depotGuid)) return BadRequest(new FailedResponse("Invalid depot id"));
|
||||||
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
||||||
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Guest);
|
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Guest);
|
||||||
if (grantIssue is not Repository rp) return (ActionResult) grantIssue;
|
if (grantIssue is not Repository rp) return (IActionResult) grantIssue;
|
||||||
|
|
||||||
// Start checkout file.
|
// Start checkout file.
|
||||||
var target = transformer.GetDepotPath(rp.Id, depotGuid);
|
var target = transformer.GetDepotPath(rp.Id, depotGuid);
|
||||||
@ -251,7 +255,7 @@ public class RepositoryInnieController(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("list_commit")]
|
[HttpGet("list_commit")]
|
||||||
public async Task<ActionResult<ListingResponse<RepositoryCommitResponse>>> ListCommitsAsync(string userName, string repositoryName)
|
public async Task<ActionResult<ListingResponse<RepositoryCommitResponse>>> ListCommitsAsync(string userName, string repositoryName)
|
||||||
{
|
{
|
||||||
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
||||||
@ -273,7 +277,7 @@ public class RepositoryInnieController(
|
|||||||
return Ok(new ListingResponse<RepositoryCommitResponse>(r.ToArray()));
|
return Ok(new ListingResponse<RepositoryCommitResponse>(r.ToArray()));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("list_locked_files")]
|
[HttpGet("list_locked_files")]
|
||||||
public async Task<ActionResult<ListingResponse<LockFileInfo>>> ListLocksAsync(string userName, string repositoryName)
|
public async Task<ActionResult<ListingResponse<LockFileInfo>>> ListLocksAsync(string userName, string repositoryName)
|
||||||
{
|
{
|
||||||
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
||||||
@ -382,7 +386,6 @@ public class RepositoryInnieController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("create_commit")]
|
[HttpPost("create_commit")]
|
||||||
[RequestSizeLimit(long.MaxValue)]
|
|
||||||
[ProducesResponseType<CommitSuccessResponse>(200)]
|
[ProducesResponseType<CommitSuccessResponse>(200)]
|
||||||
public async Task<IActionResult> CommitAsync(string userName, string repositoryName, [FromForm] FormCommitRequest req)
|
public async Task<IActionResult> CommitAsync(string userName, string repositoryName, [FromForm] FormCommitRequest req)
|
||||||
{
|
{
|
||||||
@ -390,7 +393,6 @@ public class RepositoryInnieController(
|
|||||||
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Developer);
|
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Developer);
|
||||||
if (grantIssue is not Repository rp) return (IActionResult) grantIssue;
|
if (grantIssue is not Repository rp) return (IActionResult) grantIssue;
|
||||||
|
|
||||||
// Appoint or upload a commit - two in one choice.
|
|
||||||
if (req.Depot != null ^ req.MainDepotId != null) return await CommitInternalAsync(rp, user, req);
|
if (req.Depot != null ^ req.MainDepotId != null) return await CommitInternalAsync(rp, user, req);
|
||||||
|
|
||||||
// Not valid response
|
// Not valid response
|
||||||
@ -400,17 +402,7 @@ public class RepositoryInnieController(
|
|||||||
|
|
||||||
private async Task<IActionResult> CommitInternalAsync(Repository rp, AppUser user, FormCommitRequest req)
|
private async Task<IActionResult> CommitInternalAsync(Repository rp, AppUser user, FormCommitRequest req)
|
||||||
{
|
{
|
||||||
var actualFs = req.WorkspaceSnapshot.Select(s =>
|
var test = new HashSet<WorkspaceFile>(req.WorkspaceSnapshot);
|
||||||
{
|
|
||||||
var idx = s.IndexOf('$');
|
|
||||||
if (idx < 0) throw new InvalidDataException($"WorkPath '{s}' is invalid!");
|
|
||||||
var dateTimeStr = s.Substring(0, idx);
|
|
||||||
var pathStr = s.Substring(idx + 1);
|
|
||||||
|
|
||||||
return new WorkspaceFile(DateTime.FromBinary(long.Parse(dateTimeStr)), pathStr);
|
|
||||||
}).ToArray();
|
|
||||||
|
|
||||||
var test = new HashSet<WorkspaceFile>(actualFs);
|
|
||||||
var createNewDepot = false;
|
var createNewDepot = false;
|
||||||
RepositoryDepot mainDepot;
|
RepositoryDepot mainDepot;
|
||||||
List<DepotLabel> depotLabels = new();
|
List<DepotLabel> depotLabels = new();
|
||||||
@ -489,13 +481,8 @@ public class RepositoryInnieController(
|
|||||||
.Where(cm => actualRequiredDepots.Contains(cm.DepotId))
|
.Where(cm => actualRequiredDepots.Contains(cm.DepotId))
|
||||||
.ToArrayAsync());
|
.ToArrayAsync());
|
||||||
|
|
||||||
// Create commit and let it alloc a main key
|
|
||||||
rp.Depots.Add(mainDepot);
|
|
||||||
await dbContext.SaveChangesAsync();
|
|
||||||
|
|
||||||
// Then write depot into disk
|
// Then write depot into disk
|
||||||
var depotPath = transformer.GetDepotPath(rp.Id, mainDepot.DepotId);
|
var depotPath = transformer.GetDepotPath(rp.Id, mainDepot.DepotId);
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(depotPath)!);
|
|
||||||
await using (var depotStream = System.IO.File.Create(depotPath))
|
await using (var depotStream = System.IO.File.Create(depotPath))
|
||||||
{
|
{
|
||||||
cacheStream.Seek(0, SeekOrigin.Begin);
|
cacheStream.Seek(0, SeekOrigin.Begin);
|
||||||
@ -505,51 +492,56 @@ public class RepositoryInnieController(
|
|||||||
// Everything alright, so make response
|
// Everything alright, so make response
|
||||||
depotLabels.Add(new DepotLabel(mainDepot.DepotId, mainDepot.Length));
|
depotLabels.Add(new DepotLabel(mainDepot.DepotId, mainDepot.Length));
|
||||||
depotLabels.AddRange(mainDepot.Dependencies.Select(d => new DepotLabel(d.DepotId, d.Length)));
|
depotLabels.AddRange(mainDepot.Dependencies.Select(d => new DepotLabel(d.DepotId, d.Length)));
|
||||||
|
rp.Depots.Add(mainDepot);
|
||||||
|
|
||||||
|
createNewDepot = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create manifest file and write to disk
|
||||||
|
var commitId = Guid.NewGuid();
|
||||||
|
var manifestPath = transformer.GetCommitManifestPath(rp.Id, commitId);
|
||||||
|
var manifest = new CommitManifest
|
||||||
|
{
|
||||||
|
ManifestId = commitId,
|
||||||
|
Depot = depotLabels.ToArray(),
|
||||||
|
FilePaths = req.WorkspaceSnapshot
|
||||||
|
};
|
||||||
|
|
||||||
|
await using (var manifestStream = System.IO.File.Create(manifestPath))
|
||||||
|
await JsonSerializer.SerializeAsync(manifestStream, manifest);
|
||||||
|
|
||||||
// Create commit info
|
// Create commit info
|
||||||
var commit = new RepositoryCommit
|
var commit = new RepositoryCommit
|
||||||
{
|
{
|
||||||
|
Id = commitId,
|
||||||
Author = user,
|
Author = user,
|
||||||
CommittedOn = DateTime.UtcNow,
|
CommittedOn = DateTime.UtcNow,
|
||||||
MainDepot = mainDepot,
|
MainDepot = mainDepot,
|
||||||
Message = req.Message,
|
Message = manifestPath
|
||||||
};
|
};
|
||||||
|
|
||||||
|
rp.Commits.Add(commit);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Write changes into db.
|
// Write changes into db.
|
||||||
rp.Commits.Add(commit);
|
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
// Revert depot create operation
|
// Revert manifest create operation
|
||||||
System.IO.File.Delete(transformer.GetDepotPath(rp.Id, mainDepot.DepotId));
|
if (createNewDepot) System.IO.File.Delete(transformer.GetDepotPath(rp.Id, mainDepot.DepotId));
|
||||||
|
System.IO.File.Delete(manifestPath);
|
||||||
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create manifest file and write to disk
|
return Ok(new CommitSuccessResponse(commit.CommittedOn, commit.Id));
|
||||||
var manifestPath = transformer.GetCommitManifestPath(rp.Id, commit.Id);
|
|
||||||
var manifest = new CommitManifest
|
|
||||||
{
|
|
||||||
ManifestId = commit.Id,
|
|
||||||
Depot = depotLabels.ToArray(),
|
|
||||||
FilePaths = actualFs
|
|
||||||
};
|
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(manifestPath)!);
|
|
||||||
await using (var manifestStream = System.IO.File.Create(manifestPath))
|
|
||||||
await JsonSerializer.SerializeAsync(manifestStream, manifest);
|
|
||||||
|
|
||||||
return Ok(new CommitSuccessResponse(commit.CommittedOn, commit.Id, mainDepot.DepotId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async ValueTask StencilWorkspaceSnapshotAsync(Stream depotStream, HashSet<WorkspaceFile> unresolved)
|
private static async ValueTask StencilWorkspaceSnapshotAsync(Stream depotStream, HashSet<WorkspaceFile> unresolved)
|
||||||
{
|
{
|
||||||
// Get version
|
// Get version
|
||||||
depotStream.Seek(0, SeekOrigin.Begin); // Fix: Get an invalid version code due to offset is bad.
|
|
||||||
var version = DataTransformer.GuessStandardDepotHeaderVersion(depotStream);
|
var version = DataTransformer.GuessStandardDepotHeaderVersion(depotStream);
|
||||||
if (version != 1) throw new InvalidDataException($"Unable to get depot header version, feedback is {version}.");
|
if (version != 1) throw new InvalidDataException($"Unable to get depot header version, feedback is {version}.");
|
||||||
|
|
||||||
|
|||||||
@ -38,8 +38,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="data\development\ff9864da-eac3-41f5-9d0f-99d51e608743\Depots\" />
|
|
||||||
<Folder Include="data\development\ff9864da-eac3-41f5-9d0f-99d51e608743\Manifests\" />
|
|
||||||
<Folder Include="Exceptions\" />
|
<Folder Include="Exceptions\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ public class Repository
|
|||||||
{
|
{
|
||||||
[Key]
|
[Key]
|
||||||
[Required]
|
[Required]
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public required AppUser Owner { get; set; }
|
public required AppUser Owner { get; set; }
|
||||||
|
|||||||
@ -5,17 +5,17 @@ namespace Flawless.Server.Models;
|
|||||||
public class RepositoryCommit
|
public class RepositoryCommit
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
public Guid Id { get; set; }
|
public required Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public AppUser Author { get; set; }
|
public required AppUser Author { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public DateTime CommittedOn { get; set; }
|
public required DateTime CommittedOn { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public string Message { get; set; } = String.Empty;
|
public required string Message { get; set; } = String.Empty;
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public RepositoryDepot MainDepot { get; set; }
|
public required RepositoryDepot MainDepot { get; set; }
|
||||||
}
|
}
|
||||||
@ -6,7 +6,7 @@ public class RepositoryDepot
|
|||||||
{
|
{
|
||||||
[Key]
|
[Key]
|
||||||
[Required]
|
[Required]
|
||||||
public Guid DepotId { get; set; }
|
public Guid DepotId { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public long Length { get; set; }
|
public long Length { get; set; }
|
||||||
|
|||||||
@ -5,8 +5,10 @@ namespace Flawless.Server.Models;
|
|||||||
|
|
||||||
public class RepositoryMember
|
public class RepositoryMember
|
||||||
{
|
{
|
||||||
[Key] [Required] public Guid Id { get; set; }
|
[Key]
|
||||||
|
[Required]
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public required AppUser User { get; set; }
|
public required AppUser User { get; set; }
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,6 @@ using Flawless.Server.Services;
|
|||||||
using Flawless.Server.Utility;
|
using Flawless.Server.Utility;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
@ -35,20 +34,6 @@ public static class Program
|
|||||||
|
|
||||||
private static void ConfigAppService(WebApplicationBuilder builder)
|
private static void ConfigAppService(WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
// Set size limit
|
|
||||||
builder.WebHost.ConfigureKestrel(opt =>
|
|
||||||
{
|
|
||||||
opt.Limits.MaxRequestBodySize = long.MaxValue; // As big as possible...
|
|
||||||
opt.Limits.MaxRequestHeaderCount = int.MaxValue; // As big as possible...
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.Configure<FormOptions>(opt =>
|
|
||||||
{
|
|
||||||
opt.MultipartBodyLengthLimit = long.MaxValue;
|
|
||||||
opt.ValueLengthLimit = int.MaxValue;
|
|
||||||
opt.MultipartHeadersLengthLimit = int.MaxValue;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Api related
|
// Api related
|
||||||
builder.Services.AddSingleton<PathTransformer>();
|
builder.Services.AddSingleton<PathTransformer>();
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user