diff --git a/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml b/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml
index 0c9d2dd..f4aa09b 100644
--- a/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml
+++ b/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml
@@ -16,6 +16,8 @@
+
+
diff --git a/Flawless-Version-Control.sln.DotSettings.user b/Flawless-Version-Control.sln.DotSettings.user
index 958b690..4ad8656 100644
--- a/Flawless-Version-Control.sln.DotSettings.user
+++ b/Flawless-Version-Control.sln.DotSettings.user
@@ -33,13 +33,13 @@
<Assembly Path="C:\Users\Cardi\.nuget\packages\irihi.ursa\1.10.0\lib\net8.0\Ursa.dll" />
<Assembly Path="C:\Users\Cardi\.nuget\packages\irihi.ursa.themes.semi\1.10.0\lib\netstandard2.0\Ursa.Themes.Semi.dll" />
</AssemblyExplorer>
- <SessionState ContinuousTestingMode="0" IsActive="True" IsLocked="True" Name="PathValidationTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
- <TestAncestor>
- <TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit</TestId>
- </TestAncestor>
+ <SessionState ContinuousTestingMode="0" IsActive="True" IsLocked="True" Name="PathValidationTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
+ <TestAncestor>
+ <TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit</TestId>
+ </TestAncestor>
</SessionState>
- <SessionState ContinuousTestingMode="0" Name="PathValidationTest #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
- <TestAncestor>
- <TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit.PathValidationTest</TestId>
- </TestAncestor>
+ <SessionState ContinuousTestingMode="0" Name="PathValidationTest #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
+ <TestAncestor>
+ <TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit.PathValidationTest</TestId>
+ </TestAncestor>
</SessionState>
\ No newline at end of file
diff --git a/Flawless.Client/AppDefaultValues.cs b/Flawless.Client/AppDefaultValues.cs
index 1e6362b..273c5bc 100644
--- a/Flawless.Client/AppDefaultValues.cs
+++ b/Flawless.Client/AppDefaultValues.cs
@@ -6,6 +6,12 @@ namespace Flawless.Client;
public static class AppDefaultValues
{
public const string HostId = "Overlay";
+
+ public const string RepoLocalStorageManagerFolder = ".flawless";
+
+ public const string RepoLocalStorageDatabaseFile = "db.json";
+
+ public const string RepoLocalStorageDepotFolder = "depots";
public static string ProgramDataDirectory { get; } =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Ca2dWorks", "FlawlessClient");
diff --git a/Flawless.Client/ErrorGUIHandler.cs b/Flawless.Client/ErrorGUIHandler.cs
new file mode 100644
index 0000000..63125e6
--- /dev/null
+++ b/Flawless.Client/ErrorGUIHandler.cs
@@ -0,0 +1,11 @@
+using System;
+using Avalonia.Controls.Notifications;
+
+namespace Flawless.Client;
+
+public static class ErrorGUIHandler
+{
+ public static void OnError(Exception ex)
+ {
+ }
+}
\ No newline at end of file
diff --git a/Flawless.Client/Flawless.Client.csproj b/Flawless.Client/Flawless.Client.csproj
index 744b21e..8b03c2c 100644
--- a/Flawless.Client/Flawless.Client.csproj
+++ b/Flawless.Client/Flawless.Client.csproj
@@ -36,6 +36,7 @@
+
@@ -55,5 +56,14 @@
ServerSetupPageView.axaml
Code
+
+ SimpleMessageDialogView.axaml
+ Code
+
+
+
+
+
+
diff --git a/Flawless.Client/Models/RepositoryLocalDatabaseModel.cs b/Flawless.Client/Models/RepositoryLocalDatabaseModel.cs
new file mode 100644
index 0000000..3349125
--- /dev/null
+++ b/Flawless.Client/Models/RepositoryLocalDatabaseModel.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.ObjectModel;
+using Flawless.Client.Service;
+using ReactiveUI.SourceGenerators;
+
+namespace Flawless.Client.Models;
+
+[Serializable]
+public partial class RepositoryLocalDatabaseModel : ReactiveModel
+{
+ public required RepositoryModel RootModal { get; init; }
+
+ public required LocalFileTreeAccessor LocalAccessor { get; init; }
+
+ public RepositoryFileTreeAccessor? RepoAccessor { get; set; }
+
+ public ObservableCollection CurrentLockedFiles { get; } = new();
+
+ [NonSerialized]
+ [Reactive] private Guid? _currentCommit;
+}
\ No newline at end of file
diff --git a/Flawless.Client/ObserverHelper.cs b/Flawless.Client/ObserverHelper.cs
index 0a36445..2c70c77 100644
--- a/Flawless.Client/ObserverHelper.cs
+++ b/Flawless.Client/ObserverHelper.cs
@@ -1,4 +1,7 @@
using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Threading.Tasks;
using Avalonia.Threading;
namespace Flawless.Client;
@@ -10,4 +13,13 @@ public static class ObserverHelper
var dispatcher = Dispatcher.UIThread;
observer.Subscribe(val => dispatcher.Invoke(() => onNext(val)));
}
+
+ public static void Sort(this ObservableCollection collection, Comparison comparison)
+ {
+ var sortableList = new List(collection);
+ sortableList.Sort(comparison);
+
+ for (int i = 0; i < sortableList.Count; i++) collection.Move(collection.IndexOf(sortableList[i]), i);
+ }
+
}
\ No newline at end of file
diff --git a/Flawless.Client/PathUtility.cs b/Flawless.Client/PathUtility.cs
new file mode 100644
index 0000000..a5aefab
--- /dev/null
+++ b/Flawless.Client/PathUtility.cs
@@ -0,0 +1,24 @@
+using System.IO;
+using Flawless.Client.Service;
+
+namespace Flawless.Client;
+
+public static class PathUtility
+{
+ public static string GetWorkspacePath(string owner, string repo)
+ => Path.Combine(SettingService.C.AppSetting.RepositoryPath, owner, repo);
+
+
+ public static string GetWorkspaceManagerPath(string owner, string repo)
+ => Path.Combine(SettingService.C.AppSetting.RepositoryPath, owner, repo, AppDefaultValues.RepoLocalStorageManagerFolder);
+
+
+ public static string GetWorkspaceDbPath(string owner, string repo)
+ => Path.Combine(SettingService.C.AppSetting.RepositoryPath, owner, repo,
+ AppDefaultValues.RepoLocalStorageManagerFolder, AppDefaultValues.RepoLocalStorageDatabaseFile);
+
+ public static string GetWorkspaceDepotCachePath(string owner, string repo)
+ => Path.Combine(SettingService.C.AppSetting.RepositoryPath, owner, repo,
+ AppDefaultValues.RepoLocalStorageManagerFolder, AppDefaultValues.RepoLocalStorageDepotFolder);
+
+}
\ No newline at end of file
diff --git a/Flawless.Client/Service/LocalFileTreeAccessor.cs b/Flawless.Client/Service/LocalFileTreeAccessor.cs
new file mode 100644
index 0000000..b283141
--- /dev/null
+++ b/Flawless.Client/Service/LocalFileTreeAccessor.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using Flawless.Abstraction;
+using Flawless.Client.Models;
+using Flawless.Core.Modal;
+
+namespace Flawless.Client.Service;
+
+public class LocalFileTreeAccessor
+{
+ private static readonly string[] IgnoredDirectories =
+ {
+ AppDefaultValues.RepoLocalStorageManagerFolder
+ };
+
+ private readonly RepositoryModel _repo;
+
+ private readonly string _rootDirectory;
+
+ private IReadOnlyDictionary _baseline;
+
+ private readonly Dictionary _newFiles = new();
+
+ private readonly Dictionary _deleteFiles = new();
+
+ private readonly Dictionary _modifyFiles = new();
+
+ private object _optLock = new();
+
+ public string WorkingDirectory => _rootDirectory;
+
+ public IReadOnlyDictionary BaselineFiles => _baseline;
+
+ public IReadOnlyDictionary NewFiles => _newFiles;
+
+ public IReadOnlyDictionary DeletedFiles => _deleteFiles;
+
+ public IReadOnlyDictionary ModifiedFiles => _modifyFiles;
+
+
+ public LocalFileTreeAccessor(RepositoryModel repo, IEnumerable baselines)
+ {
+ _repo = repo;
+ _baseline = baselines.ToImmutableDictionary(b => b.WorkPath);
+ _rootDirectory = PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name);
+ }
+
+ public void SetBaseline(IEnumerable baselines)
+ {
+ lock (_optLock)
+ {
+ _baseline = baselines.ToImmutableDictionary(b => b.WorkPath);
+ _newFiles.Clear();
+ _deleteFiles.Clear();
+ _modifyFiles.Clear();
+
+ RefreshInternal();
+ }
+ }
+
+ public void Refresh()
+ {
+ lock (_optLock)
+ {
+ _newFiles.Clear();
+ _deleteFiles.Clear();
+ _modifyFiles.Clear();
+
+ RefreshInternal();
+ }
+ }
+
+ private void RefreshInternal()
+ {
+ Dictionary currentFiles = new();
+ foreach (var f in Directory.GetFiles(_rootDirectory, "*", SearchOption.AllDirectories))
+ {
+ var workPath = WorkPath.FromPlatformPath(f, _rootDirectory);
+ if (!WorkPath.IsPathValid(workPath) || IgnoredDirectories.Any(d => workPath.StartsWith(d)))
+ continue;
+
+ var modifyTime = File.GetLastWriteTimeUtc(f);
+ currentFiles.Add(workPath, new WorkspaceFile { WorkPath = workPath, ModifyTime = modifyTime });
+ }
+
+ // Find those are changed
+ 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 removed = _baseline.Values.Where(v => !currentFiles.ContainsKey(v.WorkPath));
+
+ foreach (var f in changes) _modifyFiles.Add(f.WorkPath, f);
+ foreach (var f in news) _newFiles.Add(f.WorkPath, f);
+ foreach (var f in removed) _deleteFiles.Add(f.WorkPath, f);
+ }
+}
\ No newline at end of file
diff --git a/Flawless.Client/Service/Remote_Generated.cs b/Flawless.Client/Service/Remote_Generated.cs
index 3c714c3..5db99a8 100644
--- a/Flawless.Client/Service/Remote_Generated.cs
+++ b/Flawless.Client/Service/Remote_Generated.cs
@@ -167,7 +167,7 @@ namespace Flawless.Client.Remote
/// Thrown when the request returns a non-success status code.
[Headers("Accept: text/plain, application/json, text/json")]
[Post("/api/repo_create")]
- Task RepoCreate([Query] string repositoryName);
+ Task RepoCreate([Query] string repositoryName, [Query] string description);
/// A that completes when the request is finished.
/// Thrown when the request returns a non-success status code.
diff --git a/Flawless.Client/Service/RepositoryFileTreeAccessor.cs b/Flawless.Client/Service/RepositoryFileTreeAccessor.cs
new file mode 100644
index 0000000..7e76b9b
--- /dev/null
+++ b/Flawless.Client/Service/RepositoryFileTreeAccessor.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.IO;
+using System.Threading.Tasks;
+using Flawless.Core.BinaryDataFormat;
+using Flawless.Core.Modal;
+
+namespace Flawless.Client.Service;
+
+public class RepositoryFileTreeAccessor : IDisposable, IAsyncDisposable
+{
+
+ private struct FileReadInfoCache
+ {
+ public readonly Guid DepotId { get; init; }
+
+ public readonly DepotFileInfo FileInfo { get; init; }
+ }
+
+ private readonly Dictionary _mappings;
+
+ private readonly CommitManifest _manifest;
+
+ private readonly Dictionary _headers;
+
+ private readonly Dictionary _cached;
+
+ private object _readonlyLock = new(); // todo support async method.
+
+ private bool _disposed;
+
+ public CommitManifest Manifest => _manifest;
+
+ public bool IsCached { get; private set; }
+
+ public RepositoryFileTreeAccessor(Dictionary currentReadFs, CommitManifest manifest)
+ {
+ IsCached = false;
+ _mappings = currentReadFs;
+ _manifest = manifest;
+ _headers = new Dictionary();
+ _cached = new Dictionary();
+ _disposed = false;
+ }
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ foreach (var mv in _mappings.Values)
+ {
+ try { mv.Dispose(); }
+ catch (Exception e) { Console.WriteLine(e); }
+ }
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ foreach (var mv in _mappings.Values)
+ {
+ try { await mv.DisposeAsync(); }
+ catch (Exception e) { Console.WriteLine(e); }
+ }
+ }
+
+ public async Task CreateCacheAsync()
+ {
+ DisposeCheck();
+ lock (_readonlyLock) if (IsCached) return;
+
+ var ms = _manifest.FilePaths.ToImmutableSortedDictionary(x => x.WorkPath, x => x.ModifyTime);
+
+ foreach (var (id, stream) in _mappings)
+ {
+ var header = DataTransformer.ExtractStandardDepotHeaderV1(stream);
+ _headers.Add(id, header);
+ await foreach (var inf in DataTransformer.ExtractDepotFileInfoMapAsync(stream, header.FileMapSize))
+ {
+ if (ms.TryGetValue(inf.Path, out var targetTime) && targetTime == inf.ModifyTime)
+ {
+ _cached.Add(inf.Path, new FileReadInfoCache { DepotId = id, FileInfo = inf });
+ }
+ }
+ }
+
+ if (ms.Count != _cached.Count)
+ throw new FileNotFoundException("Some of those files are not able to be fount in any depots!");
+
+ lock (_readonlyLock) IsCached = true;
+ }
+
+ public bool TryGetFileInfo(string workPath, out DepotFileInfo info)
+ {
+ DisposeCheck();
+ lock (_readonlyLock) if (!IsCached) throw new InvalidOperationException("Not cached!");
+
+ if (_cached.TryGetValue(workPath, out var r))
+ {
+ info = r.FileInfo;
+ return true;
+ }
+
+ info = default;
+ return false;
+ }
+
+ public bool TryWriteDataIntoStream(string workPath, Stream stream)
+ {
+ DisposeCheck();
+ if (stream == null || !stream.CanWrite) throw new ArgumentException("Stream is not writable!");
+
+ if (_cached.TryGetValue(workPath, out var r))
+ {
+ var baseStream = DataTransformer.ExtractStandardDepotFile(_mappings[r.DepotId], r.FileInfo);
+ baseStream.CopyTo(stream);
+ return true;
+ }
+
+ return false;
+ }
+
+ private void DisposeCheck()
+ {
+ if (_disposed) throw new ObjectDisposedException("Accessor has already been disposed");
+ }
+
+}
\ No newline at end of file
diff --git a/Flawless.Client/Service/RepositoryService.cs b/Flawless.Client/Service/RepositoryService.cs
index f8f57ed..0af8911 100644
--- a/Flawless.Client/Service/RepositoryService.cs
+++ b/Flawless.Client/Service/RepositoryService.cs
@@ -4,8 +4,11 @@ using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
+using Flawless.Abstraction;
using Flawless.Client.Models;
-using Flawless.Client.Remote;
+using Flawless.Core.Modal;
+using Newtonsoft.Json;
+using ValueTaskSupplement;
namespace Flawless.Client.Service;
@@ -13,40 +16,107 @@ public class RepositoryService : BaseService
{
public ObservableCollection Repositories => _repositories;
-
private readonly ObservableCollection _repositories = new();
- public async ValueTask CreateRepositoryOnServerAsync(string repositoryName)
+ private readonly Dictionary _localRepoDbModel = new();
+
+ private readonly HashSet _openedRepos = new();
+
+ private bool TryCreateRepositoryBaseStorageStructure(RepositoryModel repo)
{
+ var dbPath = PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name);
+ if (File.Exists(dbPath)) return false;
+
+ // Get directories
+ var localRepoDb = GetRepositoryLocalDatabase(repo);
+ var folderPath = PathUtility.GetWorkspaceManagerPath(repo.OwnerName, repo.Name);
+
+ // Create initial data.
+ Directory.CreateDirectory(folderPath);
+ using var writeFs = new StreamWriter(new FileStream(dbPath, FileMode.Create));
+ JsonSerializer.CreateDefault().Serialize(writeFs, localRepoDb);
+
+ return true;
+ }
+
+
+ public bool SaveRepositoryLocalDatabaseChanges(RepositoryModel repo)
+ {
+ var dbPath = PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name);
+ if (File.Exists(dbPath)) return false;
+ if (!_localRepoDbModel.TryGetValue(repo, out var localRepo))
+ {
+ using var writeFs = new StreamWriter(new FileStream(dbPath, FileMode.Truncate));
+ JsonSerializer.CreateDefault().Serialize(writeFs, localRepo);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public RepositoryLocalDatabaseModel GetRepositoryLocalDatabase(RepositoryModel repo)
+ {
+ if (!_localRepoDbModel.TryGetValue(repo, out var localRepo))
+ {
+ var dbPath = PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name);
+ if (File.Exists(dbPath))
+ {
+ // Use existed target
+ using var readFs = new StreamReader(new FileStream(dbPath, FileMode.Open));
+ localRepo = JsonSerializer.CreateDefault().Deserialize(readFs, typeof(RepositoryLocalDatabaseModel))
+ as RepositoryLocalDatabaseModel; // todo add broken test.
+ }
+ else
+ {
+ // Create new one.
+ localRepo = new RepositoryLocalDatabaseModel
+ {
+ RootModal = repo,
+ LocalAccessor = new LocalFileTreeAccessor(repo, [])
+ };
+ }
+
+ _localRepoDbModel.Add(repo, localRepo);
+ }
+
+ return localRepo;
+ }
+
+ public async ValueTask CreateRepositoryOnServerAsync(string repositoryName, string description)
+ {
+ RepositoryModel repo;
var api = Api.C;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
- return false;
+ return null;
}
- var r = await api.Gateway.RepoCreate(repositoryName);
+ var r = await api.Gateway.RepoCreate(repositoryName, description);
- var repo = new RepositoryModel();
- repo.Name = r.RepositoryName;
- repo.OwnerName = r.OwnerUsername;
- repo.StandaloneName = RepositoryModel.GetStandaloneName(r.RepositoryName, r.OwnerUsername);
- repo.Description = r.Description;
- repo.Archived = r.IsArchived;
- repo.OwnByCurrentUser = (int) r.Role == (int) RepositoryModel.RepositoryRole.Owner;
-
+ repo = new RepositoryModel
+ {
+ Name = r.RepositoryName,
+ OwnerName = r.OwnerUsername,
+ StandaloneName = RepositoryModel.GetStandaloneName(r.RepositoryName, r.OwnerUsername),
+ Description = r.Description,
+ Archived = r.IsArchived,
+ OwnByCurrentUser = (int) r.Role == (int) RepositoryModel.RepositoryRole.Owner
+ };
+
Repositories.Insert(0, repo);
}
catch (Exception e)
{
Console.WriteLine(e);
- return false;
+ return null;
}
- return true;
+ return repo;
}
public async ValueTask UpdateRepositoriesFromServerAsync()
@@ -98,18 +168,27 @@ public class RepositoryService : BaseService
return true;
}
- public async ValueTask UpdateRepositoriesDownloadedFromDiskAsync()
+ public async ValueTask UpdateRepositoriesDownloadedStatusFromDiskAsync()
{
foreach (var repo in _repositories)
{
- var fsPath = Path.Combine(SettingService.C.AppSetting.RepositoryPath, repo.OwnerName, repo.Name);
- repo.IsDownloaded = Directory.Exists(fsPath);
+ 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 UpdateDownloadedStatusFromDiskAsync(RepositoryModel repo)
+ {
+ var isFolderExists = Directory.Exists(PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name));
+ var isDbFileExists = File.Exists(PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name));
+
+ return isFolderExists && isDbFileExists;
+ }
- public async ValueTask UpdateRepositoryMembersFromServerAsync(RepositoryModel repo)
+ public async ValueTask UpdateMembersFromServerAsync(RepositoryModel repo)
{
var api = Api.C;
try
@@ -158,4 +237,344 @@ public class RepositoryService : BaseService
return true;
}
+
+ public bool IsRepositoryOpened(RepositoryModel repo)
+ {
+ return _openedRepos.Any(r => r == repo);
+ }
+
+ public async ValueTask CloseRepositoryAsync(RepositoryModel repo)
+ {
+ if (_openedRepos.Contains(repo))
+ {
+ var ls = GetRepositoryLocalDatabase(repo);
+ if (ls.RepoAccessor != null) await ls.RepoAccessor!.DisposeAsync();
+ ls.RepoAccessor = null;
+
+ if (!SaveRepositoryLocalDatabaseChanges(repo)) return false;
+
+ _openedRepos.Remove(repo);
+ return true;
+ }
+
+ return false;
+ }
+
+ public async ValueTask OpenRepositoryOnStorageAsync(RepositoryModel repo)
+ {
+ if (!await UpdateDownloadedStatusFromDiskAsync(repo) || repo.IsDownloaded == false) return false;
+ if (!await UpdateCommitsFromServerAsync(repo)) return false;
+ var ls = GetRepositoryLocalDatabase(repo);
+
+ if (ls.CurrentCommit != null)
+ {
+ var accessor = await DownloadDepotsToGetCommitFileTreeFromServerAsync(repo, ls.CurrentCommit.Value);
+ if (accessor == null) return false;
+ ls.RepoAccessor = accessor;
+ }
+
+ _openedRepos.Add(repo);
+ return true;
+ }
+
+ public async ValueTask CreateNewRepositoryOnStorageAsync(RepositoryModel repo)
+ {
+ // Create basic structures.
+ if (!TryCreateRepositoryBaseStorageStructure(repo)) return false;
+
+ if (!await UpdateCommitsFromServerAsync(repo)) return false;
+ var peekCommit = repo.Commits.MaxBy(sl => sl.CommittedOn);
+ if (peekCommit == null) return false; // Should not use this function!
+
+ repo.IsDownloaded = true;
+ _openedRepos.Add(repo);
+ return true;
+ }
+
+ public async ValueTask CloneRepositoryFromRemoteAsync(RepositoryModel repo)
+ {
+ // Create basic structures.
+ if (!TryCreateRepositoryBaseStorageStructure(repo)) return false;
+
+ if (!await UpdateCommitsFromServerAsync(repo)) return false;
+ var peekCommit = repo.Commits.MaxBy(sl => sl.CommittedOn);
+ if (peekCommit == null) return false; // Should not use this function!
+
+ // Create basic structures.
+ if (!TryCreateRepositoryBaseStorageStructure(repo)) return false;
+
+ // Download base repo info
+ var accessor = await DownloadDepotsToGetCommitFileTreeFromServerAsync(repo, peekCommit.CommitId);
+ if (accessor == null)
+ {
+ await DeleteFromDiskAsync(repo);
+ return false;
+ };
+
+ var ls = GetRepositoryLocalDatabase(repo);
+ ls.CurrentCommit = peekCommit.CommitId;
+ ls.RepoAccessor = accessor;
+
+ try
+ {
+ foreach (var f in accessor.Manifest.FilePaths)
+ {
+ var pfs = WorkPath.ToPlatformPath(f.WorkPath, ls.LocalAccessor.WorkingDirectory);
+ var directory = Path.GetDirectoryName(pfs);
+
+ // Write into fs
+ if (directory != null) Directory.CreateDirectory(directory);
+ await using var write = File.Create(pfs);
+ accessor.TryWriteDataIntoStream(f.WorkPath, write);
+ }
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ await DeleteFromDiskAsync(repo);
+ return false;
+ }
+
+ repo.IsDownloaded = true;
+ _openedRepos.Add(repo);
+ return true;
+ }
+
+ public async ValueTask ShouldUpdateLocalCommitsCacheFromServerAsync(RepositoryModel repo)
+ {
+ var api = Api.C;
+ try
+ {
+ if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
+ {
+ api.ClearGateway();
+ return false;
+ }
+
+ var rsp = await api.Gateway.PeekCommit(repo.Name, repo.OwnerName);
+ var emptyRepo = repo.Commits.Count == 0;
+
+ // If they both empty
+ if ((rsp.Result == Guid.Empty) == emptyRepo) return false;
+
+ if (emptyRepo) return true;
+ return rsp.Result == repo.Commits.MaxBy(cm => cm.CommittedOn)!.CommitId;
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ return false;
+ }
+ }
+
+ public async ValueTask UpdateCommitsFromServerAsync(RepositoryModel repo)
+ {
+ var api = Api.C;
+ try
+ {
+ if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
+ {
+ api.ClearGateway();
+ return false;
+ }
+
+ var rsp = await api.Gateway.ListCommit(repo.Name, repo.OwnerName);
+
+ // Update existed
+ var dict = rsp.Result.ToDictionary(m => m.Id);
+ for (var i = 0; i < repo.Commits.Count; i++)
+ {
+ var ele = repo.Commits[i];
+ if (!dict.Remove(ele.CommitId, out var cm))
+ {
+ repo.Members.RemoveAt(i);
+ i -= 1;
+ continue;
+ }
+
+ ele.Message = cm.Message;
+ ele.DepotId = cm.MainDepotId;
+ ele.CommittedOn = cm.CommitedOn.UtcDateTime;
+ ele.Author = cm.Author;
+ }
+
+ // Add missing
+ foreach (var cm in dict.Values)
+ {
+ var r = new RepositoryModel.Commit
+ {
+ CommitId = cm.Id,
+ Message = cm.Message,
+ DepotId = cm.MainDepotId,
+ CommittedOn = cm.CommitedOn.UtcDateTime,
+ Author = cm.Author,
+ };
+
+ repo.Commits.Add(r);
+ }
+
+ // Resort them again
+ repo.Commits.Sort((l, r) => r.CommittedOn.CompareTo(l.CommittedOn));
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ return false;
+ }
+
+ return true;
+ }
+
+ public async ValueTask DownloadDepotsToGetCommitFileTreeFromServerAsync
+ (RepositoryModel repo, Guid commit, bool storeDownloadedDepots = true)
+ {
+ if (commit == Guid.Empty) return null;
+
+ // Get depots list and manifest
+ var manifest = await DownloadManifestFromServerAsync(repo, commit);
+ if (manifest == null) return null;
+
+ // Prepare folders
+ var path = PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name);
+ var depotsRoot = PathUtility.GetWorkspaceDepotCachePath(repo.OwnerName, repo.Name);
+ Directory.CreateDirectory(path);
+
+ // Generate download depots list
+ var mainDepotLabel = manifest.Value.Depot;
+ var willDownload = mainDepotLabel.Where(label =>
+ {
+ var dpPath = Path.Combine(depotsRoot, label.Id.ToString());
+ if (!File.Exists(dpPath)) return true;
+ // todo Needs a way to check if that valid.
+ return false;
+ });
+
+ // Download them
+ var downloadedDepots = await DownloadDepotsAndCopyNetworkStreamIntoNewMemoryStreamFromServerAsync(repo, willDownload);
+
+ 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)
+ {
+ // Write new downloaded depots into disk
+ var transform = downloadedDepots.Select(dl => (dl.Item1, dl.Item2!));
+ await WriteDownloadedDepotsFromServerToStorageAsync(repo, transform);
+ }
+
+ // Create mapping dictionary
+ var mappingDict = downloadedDepots.ToDictionary(i => i.Item1, i => i.Item2!);
+ foreach (var dl in mainDepotLabel)
+ {
+ if (mappingDict.ContainsKey(dl.Id)) continue;
+ var dst = Path.Combine(depotsRoot, dl.Id.ToString());
+ mappingDict.Add(dl.Id, new FileStream(dst, FileMode.Create));
+ }
+
+ return new RepositoryFileTreeAccessor(mappingDict, manifest.Value);
+ }
+ catch (Exception e)
+ {
+ if (downloadedDepots != null)
+ foreach (var t in downloadedDepots)
+ {
+ if (t.Item2 == null) continue;
+ try { await t.Item2.DisposeAsync(); }
+ catch (Exception ex) { Console.WriteLine(ex); }
+ }
+
+ Console.WriteLine(e);
+ return null;
+ }
+
+ }
+
+ public async ValueTask WriteDownloadedDepotsFromServerToStorageAsync
+ (RepositoryModel repo, IEnumerable<(Guid, Stream)> depots)
+ {
+ var depotsRoot = PathUtility.GetWorkspaceDepotCachePath(repo.OwnerName, repo.Name);
+ var tasks = depots.Select(async d =>
+ {
+ var dst = Path.Combine(depotsRoot, d.Item1.ToString());
+ await using var ws = new FileStream(dst, FileMode.Create);
+ await d.Item2.CopyToAsync(ws);
+ d.Item2.Seek(0, SeekOrigin.Begin);
+ });
+
+ await Task.WhenAll(tasks);
+ }
+
+ public async ValueTask<(Guid, Stream?)[]?> DownloadDepotsAndCopyNetworkStreamIntoNewMemoryStreamFromServerAsync
+ (RepositoryModel repo, IEnumerable depotsId)
+ {
+ try
+ {
+ var api = Api.C;
+ if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
+ {
+ api.ClearGateway();
+ return null;
+ }
+
+ return await ValueTaskEx.WhenAll(depotsId.Select(p => DownloadDepotInternalAsync(p.Id, p.Length)));
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ 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 DownloadManifestFromServerAsync(RepositoryModel repo, Guid manifestId)
+ {
+ try
+ {
+ var api = Api.C;
+ if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
+ {
+ api.ClearGateway();
+ return null;
+ }
+
+ using var manifestResponse = await api.Gateway.FetchManifest(repo.OwnerName, repo.Name, manifestId.ToString());
+ if (manifestResponse.StatusCode != 200) return null;
+
+ return await System.Text.Json.JsonSerializer.DeserializeAsync(manifestResponse.Stream);
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ return null;
+ }
+ }
+
+ public async ValueTask DeleteFromDiskAsync(RepositoryModel repo)
+ {
+ try
+ {
+ var path = PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name);
+ if (Directory.Exists(path)) Directory.Delete(path, true);
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ return false;
+ }
+
+ return true;
+ }
}
\ No newline at end of file
diff --git a/Flawless.Client/ViewModels/HomeViewModel.cs b/Flawless.Client/ViewModels/HomeViewModel.cs
index 1d7ecb9..b036636 100644
--- a/Flawless.Client/ViewModels/HomeViewModel.cs
+++ b/Flawless.Client/ViewModels/HomeViewModel.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.ObjectModel;
-using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.ReactiveUI;
@@ -22,9 +21,9 @@ public partial class HomeViewModel : ViewModelBase, IRoutableViewModel
[Reactive] private RepositoryModel? _selectedRepository;
- [Reactive] private string _serverFriendlyName;
+ [Reactive] private string? _serverFriendlyName;
- public RepositoryService RepoSrc => RepositoryService.C;
+ public ObservableCollection Repos => RepositoryService.C.Repositories;
public HomeViewModel(IScreen hostScreen)
{
@@ -36,10 +35,8 @@ public partial class HomeViewModel : ViewModelBase, IRoutableViewModel
[ReactiveCommand]
private async Task RefreshRepositoriesAsync()
{
- if (await RepoSrc.UpdateRepositoriesFromServerAsync())
- {
-
- }
+ await RepositoryService.C.UpdateRepositoriesFromServerAsync();
+ await RepositoryService.C.UpdateRepositoriesDownloadedStatusFromDiskAsync();
}
[ReactiveCommand]
@@ -62,34 +59,64 @@ public partial class HomeViewModel : ViewModelBase, IRoutableViewModel
if (mr == DialogResult.OK)
{
- await RepoSrc.CreateRepositoryOnServerAsync(form.RepositoryName);
+ var repo = await RepositoryService.C.CreateRepositoryOnServerAsync(form.RepositoryName, form.Description);
+ if (repo == null) return;
+ if (!await RepositoryService.C.CreateNewRepositoryOnStorageAsync(repo)) return;
+
+ await HostScreen.Router.Navigate.Execute(new RepositoryViewModel(repo, HostScreen));
}
}
[ReactiveCommand]
private async Task OpenRepositoryAsync()
{
+ if (_selectedRepository == null) return;
+ if (!await RepositoryService.C.OpenRepositoryOnStorageAsync(_selectedRepository)) return;
+ await HostScreen.Router.Navigate.Execute(new RepositoryViewModel(_selectedRepository, HostScreen));
}
[ReactiveCommand]
private async Task DownloadRepositoryAsync()
{
+ if (_selectedRepository == null) return;
+ if (!await RepositoryService.C.CloneRepositoryFromRemoteAsync(_selectedRepository)) return;
+ await HostScreen.Router.Navigate.Execute(new RepositoryViewModel(_selectedRepository, HostScreen));
}
[ReactiveCommand]
private async Task DeleteRepositoryAsync()
{
+ if (_selectedRepository == null) return;
+ if (await RepositoryService.C.DeleteFromDiskAsync(_selectedRepository))
+ {
+ await RepositoryService.C.UpdateDownloadedStatusFromDiskAsync(_selectedRepository);
+ }
}
[ReactiveCommand]
private async Task QuitLoginAsync()
{
- Api.C.ClearGateway();
+ var opt = new OverlayDialogOptions
+ {
+ FullScreen = false,
+ Buttons = DialogButton.YesNo,
+ CanResize = false,
+ CanDragMove = false,
+ IsCloseButtonVisible = true,
+ CanLightDismiss = true,
+ Mode = DialogMode.Question
+ };
+
+ var vm = new SimpleMessageDialogViewModel("Do you really want to logout?");
+ var mr = await OverlayDialog
+ .ShowModal(vm, AppDefaultValues.HostId, opt);
+
+ if (mr == DialogResult.Yes) Api.C.ClearGateway();
}
[ReactiveCommand]
- private void OpenGlobalSettingAsync()
+ private async Task OpenGlobalSettingAsync()
{
- HostScreen.Router.Navigate.Execute(new SettingViewModel(HostScreen));
+ await HostScreen.Router.Navigate.Execute(new SettingViewModel(HostScreen));
}
}
\ No newline at end of file
diff --git a/Flawless.Client/ViewModels/ModalBox/CreateRepositoryDialogViewModel.cs b/Flawless.Client/ViewModels/ModalBox/CreateRepositoryDialogViewModel.cs
index 411fdcb..00b26da 100644
--- a/Flawless.Client/ViewModels/ModalBox/CreateRepositoryDialogViewModel.cs
+++ b/Flawless.Client/ViewModels/ModalBox/CreateRepositoryDialogViewModel.cs
@@ -5,4 +5,6 @@ namespace Flawless.Client.ViewModels.ModalBox;
public partial class CreateRepositoryDialogViewModel : ViewModelBase
{
[Reactive] private string _repositoryName;
+
+ [Reactive] private string _description;
}
\ No newline at end of file
diff --git a/Flawless.Client/ViewModels/ModalBox/SimpleMessageDialogViewModel.cs b/Flawless.Client/ViewModels/ModalBox/SimpleMessageDialogViewModel.cs
new file mode 100644
index 0000000..20b03b9
--- /dev/null
+++ b/Flawless.Client/ViewModels/ModalBox/SimpleMessageDialogViewModel.cs
@@ -0,0 +1,13 @@
+using ReactiveUI.SourceGenerators;
+
+namespace Flawless.Client.ViewModels.ModalBox;
+
+public partial class SimpleMessageDialogViewModel : ViewModelBase
+{
+ [Reactive] private string _message;
+
+ public SimpleMessageDialogViewModel(string message)
+ {
+ _message = message;
+ }
+}
\ No newline at end of file
diff --git a/Flawless.Client/ViewModels/RepositoryViewModel.cs b/Flawless.Client/ViewModels/RepositoryViewModel.cs
index 0078e17..c2a244f 100644
--- a/Flawless.Client/ViewModels/RepositoryViewModel.cs
+++ b/Flawless.Client/ViewModels/RepositoryViewModel.cs
@@ -1,18 +1,25 @@
-using Flawless.Client.Models;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using Flawless.Client.Models;
using Flawless.Client.Service;
using ReactiveUI;
+using ReactiveUI.SourceGenerators;
namespace Flawless.Client.ViewModels;
-public class RepositoryViewModel : RoutableViewModelBase
+public partial class RepositoryViewModel : RoutableViewModelBase
{
public RepositoryModel Repository { get; }
-
- public RepositoryService RepoSrv { get; }
+
+ [ReactiveCommand]
+ private async Task GoBackAsync()
+ {
+ await RepositoryService.C.CloseRepositoryAsync(Repository);
+ await HostScreen.Router.NavigateBack.Execute();
+ }
public RepositoryViewModel(RepositoryModel repo, IScreen hostScreen) : base(hostScreen)
{
Repository = repo;
- RepoSrv = RepositoryService.C;
}
}
\ No newline at end of file
diff --git a/Flawless.Client/Views/HomeView.axaml b/Flawless.Client/Views/HomeView.axaml
index 31b7692..9403ff7 100644
--- a/Flawless.Client/Views/HomeView.axaml
+++ b/Flawless.Client/Views/HomeView.axaml
@@ -18,7 +18,7 @@
+ Command="{Binding OpenGlobalSettingCommand}"/>
@@ -35,7 +35,7 @@
Command="{Binding OpenRepositoryCommand}"/>
+ Command="{Binding DownloadRepositoryCommand}"/>
@@ -44,14 +44,14 @@
-
-
+
-
diff --git a/Flawless.Client/Views/ModalBox/CreateRepositoryDialogView.axaml b/Flawless.Client/Views/ModalBox/CreateRepositoryDialogView.axaml
index 5d0863b..132d5bb 100644
--- a/Flawless.Client/Views/ModalBox/CreateRepositoryDialogView.axaml
+++ b/Flawless.Client/Views/ModalBox/CreateRepositoryDialogView.axaml
@@ -12,11 +12,14 @@
-
+
-
+
+
+
+
diff --git a/Flawless.Client/Views/ModalBox/SimpleMessageDialogView.axaml b/Flawless.Client/Views/ModalBox/SimpleMessageDialogView.axaml
new file mode 100644
index 0000000..e49afab
--- /dev/null
+++ b/Flawless.Client/Views/ModalBox/SimpleMessageDialogView.axaml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flawless.Client/Views/ModalBox/SimpleMessageDialogView.axaml.cs b/Flawless.Client/Views/ModalBox/SimpleMessageDialogView.axaml.cs
new file mode 100644
index 0000000..724683e
--- /dev/null
+++ b/Flawless.Client/Views/ModalBox/SimpleMessageDialogView.axaml.cs
@@ -0,0 +1,15 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Flawless.Client.ViewModels.ModalBox;
+using Ursa.ReactiveUIExtension;
+
+namespace Flawless.Client.Views.ModalBox;
+
+public partial class SimpleMessageDialogView : ReactiveUrsaView
+{
+ public SimpleMessageDialogView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/Flawless.Client/Views/RepositoryView.axaml b/Flawless.Client/Views/RepositoryView.axaml
index f8cc8e3..d22d9a9 100644
--- a/Flawless.Client/Views/RepositoryView.axaml
+++ b/Flawless.Client/Views/RepositoryView.axaml
@@ -6,12 +6,14 @@
xmlns:semi="https://irihi.tech/semi"
xmlns:vm="using:Flawless.Client.ViewModels"
xmlns:page="using:Flawless.Client.Views.RepositoryPage"
+ x:DataType="vm:RepositoryViewModel"
mc:Ignorable="d" d:DesignWidth="1280" d:DesignHeight="768"
x:Class="Flawless.Client.Views.RepositoryView">
-
+
diff --git a/Flawless.Communication/Request/CommitRequest.cs b/Flawless.Communication/Request/CommitRequest.cs
index 4d21065..03d238f 100644
--- a/Flawless.Communication/Request/CommitRequest.cs
+++ b/Flawless.Communication/Request/CommitRequest.cs
@@ -1,3 +1,4 @@
+using Flawless.Communication.Shared;
using Flawless.Core.Modal;
namespace Flawless.Communication.Request;
diff --git a/Flawless.Core/Modal/CommitManifest.cs b/Flawless.Core/Modal/CommitManifest.cs
index ea0db22..277375d 100644
--- a/Flawless.Core/Modal/CommitManifest.cs
+++ b/Flawless.Core/Modal/CommitManifest.cs
@@ -1,4 +1,4 @@
namespace Flawless.Core.Modal;
[Serializable]
-public record struct CommitManifest(Guid ManifestId, DepotLabel Depot, WorkspaceFile[] FilePaths);
\ No newline at end of file
+public record struct CommitManifest(Guid ManifestId, DepotLabel[] Depot, WorkspaceFile[] FilePaths);
\ No newline at end of file
diff --git a/Flawless.Core/Modal/DepotLabel.cs b/Flawless.Core/Modal/DepotLabel.cs
index d4a94f4..891f807 100644
--- a/Flawless.Core/Modal/DepotLabel.cs
+++ b/Flawless.Core/Modal/DepotLabel.cs
@@ -3,4 +3,4 @@
namespace Flawless.Core.Modal;
[Serializable]
-public record struct DepotLabel(Guid Id, long Length, Guid[] Dependency);
\ No newline at end of file
+public record struct DepotLabel(Guid Id, long Length);
\ No newline at end of file
diff --git a/Flawless.Server/Controllers/RepositoryInnieController.cs b/Flawless.Server/Controllers/RepositoryInnieController.cs
index 7864d85..7fb9ac1 100644
--- a/Flawless.Server/Controllers/RepositoryInnieController.cs
+++ b/Flawless.Server/Controllers/RepositoryInnieController.cs
@@ -405,11 +405,12 @@ public class RepositoryInnieController(
var test = new HashSet(req.WorkspaceSnapshot);
var createNewDepot = false;
RepositoryDepot mainDepot;
- DepotLabel depotLabel;
+ List depotLabels = new();
// Judge if receive new depot or use old one...
if (req.Depot == null)
{
+ // Use existed depots
var mainDepotId = Guid.Parse(req.MainDepotId!);
var preDepot = await StencilWorkspaceSnapshotViaDatabaseAsync(rp.Id, mainDepotId, test);
@@ -422,11 +423,13 @@ public class RepositoryInnieController(
// Set this depot label...
mainDepot = preDepot;
- var deps = mainDepot.Dependencies.Select(d => d.DepotId).ToArray();
- depotLabel = new DepotLabel(mainDepot.DepotId, mainDepot.Length, deps);
+ depotLabels.Add(new DepotLabel(mainDepot.DepotId, mainDepot.Length));
+ depotLabels.AddRange(mainDepot.Dependencies.Select(d => new DepotLabel(d.DepotId, d.Length)));
}
else
{
+ // Get a new depot from request - this will flat to direct depot reference to avoid deep search that
+ // raise memory issue.
var actualRequiredDepots = new HashSet();
// Read and create a new depot
@@ -477,9 +480,6 @@ public class RepositoryInnieController(
.SelectMany(r => r.Depots)
.Where(cm => actualRequiredDepots.Contains(cm.DepotId))
.ToArrayAsync());
-
- depotLabel = new DepotLabel(mainDepot.DepotId, mainDepot.Length, actualRequiredDepots.ToArray());
- rp.Depots.Add(mainDepot);
// Then write depot into disk
var depotPath = transformer.GetDepotPath(rp.Id, mainDepot.DepotId);
@@ -489,6 +489,11 @@ public class RepositoryInnieController(
await cacheStream.CopyToAsync(depotStream, HttpContext.RequestAborted);
}
+ // Everything alright, so make response
+ depotLabels.Add(new DepotLabel(mainDepot.DepotId, mainDepot.Length));
+ depotLabels.AddRange(mainDepot.Dependencies.Select(d => new DepotLabel(d.DepotId, d.Length)));
+ rp.Depots.Add(mainDepot);
+
createNewDepot = true;
}
@@ -498,7 +503,7 @@ public class RepositoryInnieController(
var manifest = new CommitManifest
{
ManifestId = commitId,
- Depot = depotLabel,
+ Depot = depotLabels.ToArray(),
FilePaths = req.WorkspaceSnapshot
};
diff --git a/Flawless.Server/Controllers/RepositoryOutieController.cs b/Flawless.Server/Controllers/RepositoryOutieController.cs
index 9355631..ed2e777 100644
--- a/Flawless.Server/Controllers/RepositoryOutieController.cs
+++ b/Flawless.Server/Controllers/RepositoryOutieController.cs
@@ -39,7 +39,7 @@ public class RepositoryOutieController(AppDbContext dbContext, UserManager> CreateRepositoryAsync([FromQuery] string repositoryName)
+ public async Task> CreateRepositoryAsync([FromQuery] string repositoryName,[FromQuery] string description)
{
if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3)
return BadRequest(new FailedResponse("Repository name is empty or too short!"));
@@ -51,6 +51,7 @@ public class RepositoryOutieController(AppDbContext dbContext, UserManager