1
0

feat: Add revert ability with lots of bug fix

This commit is contained in:
Ca2didi 2025-04-11 01:07:24 +08:00
parent ca8c3bcad3
commit 62ced75815
9 changed files with 399 additions and 97 deletions

View File

@ -4,6 +4,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AArgumentOutOfRangeException_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003F82_003F5d81019e_003FArgumentOutOfRangeException_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAssert_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fea501b1a950043b99f3df638f1824d6143a18_003Fb8_003Fb16d6a68_003FAssert_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAsyncValueTaskMethodBuilder_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003Fa5_003Ff3a8130e_003FAsyncValueTaskMethodBuilder_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAsyncValueTaskMethodBuilder_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F4c8540adf3cd4f6ab5d99f290234ba1ad19c00_003Fd1_003Ff1626b2e_003FAsyncValueTaskMethodBuilder_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticatorTokenProvider_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fd56cb0a089b14dab96ad3ee133819f966d938_003Feb_003Fa2d5eee1_003FAuthenticatorTokenProvider_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACheckBoxColumnOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F25ac83f0b30c483ab65ac482108a36294cc00_003F_005Fe6928_003FCheckBoxColumnOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACheckBoxColumn_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F2142c5e8387b5a71988ab7c9ece92b77aa8cea9b3f6f76a861547f959780d5_003FCheckBoxColumn_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
@ -12,11 +13,13 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AColumnBase_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F7d901ebbf99b7e87c21edb35dc72355e0e287a2559c8ee26452dc86792b2a_003FColumnBase_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AControllerBase_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fdffdaf205cf54e098aa7d66ba76b38621de920_003F53_003F6f15feba_003FControllerBase_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADbContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcb0587797ea44bd6915ede69888c6766291038_003Fbc_003F2b4c89d0_003FDbContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFileInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F4c8540adf3cd4f6ab5d99f290234ba1ad19c00_003Ffe_003F5a5023e2_003FFileInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityDbContext_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F3eeae7a548684642a53a9ceddc825b7a1a930_003Fcf_003F6a374370_003FIdentityDbContext_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityUserToken_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Facfead36ed0138084f13ee724545d2dcb853f354ec658443b72fc26eff58781_003FIdentityUserToken_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityUser_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fc405819100144b0483c14b61d32c5aa215930_003F90_003F4d8e1a86_003FIdentityUser_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityUser_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F7d382df578ec93391918cfaa4ce7f4b8f35c9aed1241d6556dc9be26df13c_003FIdentityUser_002Ecs_002Fz_003A2_002D1/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityUser_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fd991417b721d4ddab50a2b715d0ad696b138_003Fa2_003Fdb3874bc_003FIdentityUser_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIReactiveObjectExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F404d064a80dc4960b93f90c9bd69770750810_003F5a_003F1516290d_003FIReactiveObjectExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJwtBearerHandler_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F61447741f88e235f7cd1a276ef5abe648b2dee4b210873893d178b861c9d0_003FJwtBearerHandler_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APath_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003F28_003F6a41ec86_003FPath_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AReactiveObject_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F404d064a80dc4960b93f90c9bd69770750810_003F5c_003F228dd86b_003FReactiveObject_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>

View File

@ -0,0 +1,29 @@
namespace Flawless.Client.Models;
public enum RepositoryResetMethod
{
/// <summary>
/// Tracked files will being reset, changes will being keep.
/// </summary>
Keep,
/// <summary>
/// Tracked files will being reset, changes will being reset.
/// </summary>
Soft,
/// <summary>
/// All files will being reset, changes will being reset, changes list will being cleaned.
/// </summary>
Hard,
/// <summary>
/// Tracked files will being reset, changes will being merged.
/// </summary>
Merge,
/// <summary>
/// All files will being reset, changes will being merged.
/// </summary>
HardMerge
}

View File

@ -21,4 +21,22 @@ public static class PathUtility
=> Path.Combine(SettingService.C.AppSetting.RepositoryPath, login, owner, repo,
AppDefaultValues.RepoLocalStorageManagerFolder, AppDefaultValues.RepoLocalStorageDepotFolder);
public static string ConvertBytesToBestDisplay(ulong bytes)
{
string[] units = { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" };
int unitIndex = 0;
double size = bytes;
if (bytes <= 0) return "0B"; // 处理零值和负值[6,7](@ref)
// 单位递进计算[6](@ref)
while (size >= 1024 && unitIndex < units.Length - 1)
{
size /= 1024.0;
unitIndex++;
}
// 智能格式化输出[6,7](@ref)
return $"{size:0.#}{units[unitIndex]}";
}
}

View File

@ -9,30 +9,57 @@ using Flawless.Core.Modal;
namespace Flawless.Client.Service;
public enum ChangeInfoType
{
Folder = 0,
Add,
Remove,
Modify
}
public struct ChangeInfo : IEquatable<ChangeInfo>
{
public ChangeInfoType Type { get; }
public WorkspaceFile File { get; }
public ChangeInfo(ChangeInfoType type, WorkspaceFile file)
{
Type = type;
File = file;
}
public bool Equals(ChangeInfo other)
{
return Type == other.Type && File.Equals(other.File);
}
public override bool Equals(object? obj)
{
return obj is ChangeInfo other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine((int)Type, File);
}
public static bool operator ==(ChangeInfo left, ChangeInfo right)
{
return left.Equals(right);
}
public static bool operator !=(ChangeInfo left, ChangeInfo right)
{
return !left.Equals(right);
}
}
public class LocalFileTreeAccessor
{
public enum ChangeType
{
Folder = 0,
Add,
Remove,
Modify
}
public struct ChangeRecord
{
public ChangeType Type { get; }
public WorkspaceFile File { get; }
public ChangeRecord(ChangeType type, WorkspaceFile file)
{
Type = type;
File = file;
}
}
private static readonly string[] IgnoredDirectories =
{
AppDefaultValues.RepoLocalStorageManagerFolder
@ -44,7 +71,7 @@ public class LocalFileTreeAccessor
private IReadOnlyDictionary<string, WorkspaceFile> _baseline;
private Dictionary<string, ChangeRecord> _changes = new();
private Dictionary<string, ChangeInfo> _changes = new();
private Dictionary<string, WorkspaceFile> _currentFiles = new();
@ -56,7 +83,7 @@ public class LocalFileTreeAccessor
public IReadOnlyDictionary<string, WorkspaceFile> BaselineFiles => _baseline;
public IReadOnlyDictionary<string, ChangeRecord> Changes => _changes;
public IReadOnlyDictionary<string, ChangeInfo> Changes => _changes;
public IReadOnlyDictionary<string, WorkspaceFile> CurrentFiles => _currentFiles;
@ -108,8 +135,8 @@ public class LocalFileTreeAccessor
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) _changes.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 removed) _changes.Add(f.WorkPath, new ChangeRecord(ChangeType.Remove, f));
foreach (var f in changes) _changes.Add(f.WorkPath, new ChangeInfo(ChangeInfoType.Modify, f));
foreach (var f in news) _changes.Add(f.WorkPath, new ChangeInfo(ChangeInfoType.Add, f));
foreach (var f in removed) _changes.Add(f.WorkPath, new ChangeInfo(ChangeInfoType.Remove, f));
}
}

View File

@ -440,9 +440,58 @@ public class RepositoryService : BaseService<RepositoryService>
_openedRepos.Add(repo);
return true;
}
public bool RevertChangesToBaseline(RepositoryModel repo, ICollection<ChangeInfo> changes)
{
var needBaseline = changes.Any(static x => x.Type is ChangeInfoType.Modify or ChangeInfoType.Remove);
var ls = GetRepositoryLocalDatabase(repo);
if (needBaseline && ls.RepoAccessor == null)
{
var e = new InvalidDataException(
"Remove and modify will not able comes with changes when it is the first commit.");
UIHelper.NotifyError(e);
Console.WriteLine(e);
return false;
}
foreach (var ci in changes)
{
try
{
string wfs;
switch (ci.Type)
{
case ChangeInfoType.Add:
wfs = WorkPath.ToPlatformPath(ci.File.WorkPath, ls.LocalAccessor.WorkingDirectory);
File.Delete(wfs);
break;
case ChangeInfoType.Remove:
case ChangeInfoType.Modify:
wfs = WorkPath.ToPlatformPath(ci.File.WorkPath, ls.LocalAccessor.WorkingDirectory);
ls.RepoAccessor!.TryWriteDataIntoStream(ci.File.WorkPath, wfs);
break;
case ChangeInfoType.Folder:
default:
break;
}
}
catch (Exception e)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
}
}
return true;
}
public async ValueTask<bool> ResetCommitPointerToTargetAndMergeDepotsIntoRepositoryFromRemoteAsync
(RepositoryModel repo, Guid commitId)
public async ValueTask<bool> SetPointerAndMergeDepotsWithLocalFromRemoteAsync
(RepositoryModel repo, Guid commitId, IReadOnlyDictionary<string, WorkspaceFile> localChangesTable,
RepositoryResetMethod method)
{
// Try Download base repo info
var accessor = await DownloadDepotsAndUseLocalCachesToGenerateRepositoryFileTreeAccessorFromServerAsync(repo, commitId);
@ -456,25 +505,56 @@ public class RepositoryService : BaseService<RepositoryService>
await accessor.CreateCacheAsync();
var ls = GetRepositoryLocalDatabase(repo);
var oldAcceesor = ls.RepoAccessor;
var oldAccessor = ls.RepoAccessor;
ls.CurrentCommit = commitId;
ls.RepoAccessor = accessor;
ls.LocalAccessor.SetBaseline(accessor);
try
{
var mergeChanges = method is RepositoryResetMethod.Merge or RepositoryResetMethod.HardMerge;
var resetChanges = method is RepositoryResetMethod.Soft or RepositoryResetMethod.Hard;
var hardMode = method is RepositoryResetMethod.Hard or RepositoryResetMethod.HardMerge;
List<WorkspaceFile> unmerged = new();
// Apply files excepts local changed.
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);
// todo Check if we need merge at here and add logic to handle that...
if (!accessor.TryWriteDataIntoStream(f.WorkPath, pfs))
throw new InvalidDataException($"Can not write {f.WorkPath} into repository.");
var isChanged = localChangesTable.ContainsKey(f.WorkPath);
if (isChanged)
{
if (mergeChanges)
unmerged.Add(f);
else if (resetChanges && !accessor.TryWriteDataIntoStream(f.WorkPath, pfs))
throw new InvalidDataException($"Can not write {f.WorkPath} into repository. (Reset changes)");
}
else
{
if (!accessor.TryWriteDataIntoStream(f.WorkPath, pfs))
throw new InvalidDataException($"Can not write {f.WorkPath} into repository.");
}
}
// Try remove files not being tracked
if (hardMode)
{
var tester = accessor.Manifest.FilePaths.Select(static x => x.WorkPath).ToHashSet();
foreach (var f in Directory.GetFiles(ls.LocalAccessor.WorkingDirectory, "*", SearchOption.AllDirectories))
{
var wfs = WorkPath.FromPlatformPath(f, ls.LocalAccessor.WorkingDirectory);
if (!tester.Contains(wfs)) File.Delete(f);
}
}
// Handle merge one by one.
foreach (var f in unmerged)
{
throw new Exception("No merge tools has been detected! Merge failed.");
}
}
catch (Exception e)
@ -482,8 +562,8 @@ public class RepositoryService : BaseService<RepositoryService>
// Revert baseline
try
{
ls.RepoAccessor = oldAcceesor;
if (oldAcceesor != null) ls.LocalAccessor.SetBaseline(oldAcceesor);
ls.RepoAccessor = oldAccessor;
if (oldAccessor != null) ls.LocalAccessor.SetBaseline(oldAccessor);
else ls.LocalAccessor.SetBaseline([]);
}
catch (Exception exception) { Console.WriteLine(exception); }
@ -496,7 +576,7 @@ public class RepositoryService : BaseService<RepositoryService>
}
// Dispose old RepoAccessor
try { if (oldAcceesor != null) await oldAcceesor.DisposeAsync(); }
try { if (oldAccessor != null) await oldAccessor.DisposeAsync(); }
catch (Exception exception) { Console.WriteLine(exception); }
SaveRepositoryLocalDatabaseChanges(repo);
@ -505,7 +585,7 @@ public class RepositoryService : BaseService<RepositoryService>
return true;
}
public async ValueTask<bool?> IsCurrentPointedToCommitIsNotPeekResultFromServerAsync(RepositoryModel repo)
public async ValueTask<bool?> IsPointedToCommitNotPeekResultFromServerAsync(RepositoryModel repo)
{
var api = Api.C;
try
@ -759,11 +839,11 @@ public class RepositoryService : BaseService<RepositoryService>
}
public async ValueTask<CommitManifest?> CommitWorkspaceAsBaselineAsync
(RepositoryModel repo, IEnumerable<LocalFileTreeAccessor.ChangeRecord> changes, string message)
(RepositoryModel repo, IEnumerable<ChangeInfo> changes, string message)
{
// Check if current version is the latest
var api = Api.C;
var requireUpdate = await IsCurrentPointedToCommitIsNotPeekResultFromServerAsync(repo);
var requireUpdate = await IsPointedToCommitNotPeekResultFromServerAsync(repo);
if (!requireUpdate.HasValue) return null;
if (requireUpdate.Value)
@ -844,7 +924,7 @@ public class RepositoryService : BaseService<RepositoryService>
}
public List<WorkspaceFile> CreateCommitManifestByCurrentBaselineAndChanges
(LocalFileTreeAccessor accessor, IEnumerable<LocalFileTreeAccessor.ChangeRecord> changes, bool hard = false)
(LocalFileTreeAccessor accessor, IEnumerable<ChangeInfo> changes, bool hard = false)
{
// Create a new depot file manifest.
var files = accessor.BaselineFiles.Values.ToList();
@ -852,7 +932,7 @@ public class RepositoryService : BaseService<RepositoryService>
{
switch (c.Type)
{
case LocalFileTreeAccessor.ChangeType.Folder:
case ChangeInfoType.Folder:
{
if (hard) throw new InvalidProgramException(
$"Can not commit folder into version control: {c.File.WorkPath}");
@ -860,7 +940,7 @@ public class RepositoryService : BaseService<RepositoryService>
Console.WriteLine($"Can not commit folder into version control...Ignored: {c.File.WorkPath}");
continue;
}
case LocalFileTreeAccessor.ChangeType.Add:
case ChangeInfoType.Add:
{
if (files.Any(f => f.WorkPath == c.File.WorkPath))
{
@ -874,7 +954,7 @@ public class RepositoryService : BaseService<RepositoryService>
files.Add(c.File);
break;
}
case LocalFileTreeAccessor.ChangeType.Remove:
case ChangeInfoType.Remove:
{
var idx = files.FindIndex(f => f.WorkPath == c.File.WorkPath);
if (idx < 0)
@ -889,7 +969,7 @@ public class RepositoryService : BaseService<RepositoryService>
files.RemoveAt(idx);
break;
}
case LocalFileTreeAccessor.ChangeType.Modify:
case ChangeInfoType.Modify:
{
var idx = files.FindIndex(f => f.WorkPath == c.File.WorkPath);
if (idx < 0)

View File

@ -1,11 +1,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Models.TreeDataGrid;
@ -20,7 +19,6 @@ using Flawless.Core.Modal;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using Ursa.Controls;
using ChangeType = Flawless.Client.Service.LocalFileTreeAccessor.ChangeType;
namespace Flawless.Client.ViewModels;
@ -64,7 +62,7 @@ public partial class LocalChangesNode : ReactiveModel
};
}
public static LocalChangesNode FromWorkspaceFile(LocalChangesNode? parent, LocalFileTreeAccessor.ChangeRecord file)
public static LocalChangesNode FromWorkspaceFile(LocalChangesNode? parent, ChangeInfo file)
{
return new LocalChangesNode
{
@ -130,21 +128,26 @@ public partial class RepositoryViewModel : RoutableViewModelBase
public FlatTreeDataGridSource<CommitTransitNode> Commits { get; }
public ObservableCollection<LocalChangesNode> LocalChangeSetRaw { get; } = new();
public ObservableCollectionExtended<LocalChangesNode> LocalChangeSetRaw { get; } = new();
public ObservableCollection<LocalChangesNode> CurrentCommitFileTreeRaw { get; } = new();
public ObservableCollectionExtended<LocalChangesNode> CurrentCommitFileTreeRaw { get; } = new();
public ObservableCollectionExtended<CommitTransitNode> CommitsRaw { get; } = new();
public UserModel User { get; }
[Reactive] private bool _autoDetectChanges = true;
[Reactive] private bool _isOwnerRole, _isDeveloperRole, _isReporterRole, _isGuestRole;
private string _wsRoot;
public RepositoryViewModel(RepositoryModel repo, IScreen hostScreen) : base(hostScreen)
{
Repository = repo;
LocalDatabase = RepositoryService.C.GetRepositoryLocalDatabase(repo);
User = UserService.C.GetUserInfoAsync(Api.C.Username.Value!)!;
_wsRoot = PathUtility.GetWorkspacePath(Api.C.Username.Value!, Repository.OwnerName, Repository.Name);
// Setup local change set
LocalChange = new HierarchicalTreeDataGridSource<LocalChangesNode>(LocalChangeSetRaw)
@ -173,16 +176,16 @@ public partial class RepositoryViewModel : RoutableViewModelBase
"File Type",
n => n.Contents != null ? "Folder" : Path.GetExtension(n.FullPath)),
new TextColumn<LocalChangesNode, ulong>(
new TextColumn<LocalChangesNode, string>(
"Size",
n => 0),
n => PathUtility.ConvertBytesToBestDisplay(GetFileSize(n))),
new TextColumn<LocalChangesNode, DateTime?>(
"ModifiedTime", n => n.ModifiedTime.HasValue ? n.ModifiedTime.Value.ToLocalTime() : null),
}
};
Commits = new FlatTreeDataGridSource<CommitTransitNode>(Repository.Commits.Select(CommitTransitNode.FromCommit))
Commits = new FlatTreeDataGridSource<CommitTransitNode>(CommitsRaw)
{
Columns =
{
@ -229,11 +232,19 @@ public partial class RepositoryViewModel : RoutableViewModelBase
_ = StartupTasksAsync();
}
private ulong GetFileSize(LocalChangesNode node)
{
if (!File.Exists(node.FullPath)) return 0;
var path = Path.Combine(_wsRoot, node.FullPath);
return (ulong)new FileInfo(path).Length;
}
private async Task StartupTasksAsync()
{
await RefreshRepositoryRoleInfoAsyncCommand.Execute();
await DetectLocalChangesAsyncCommand.Execute();
await RendererFileTreeAsync();
await RefreshRepositoryRoleInfoAsyncCommand.Execute();
SyncCommitsFromRepository();
}
private async ValueTask RendererFileTreeAsync()
@ -241,20 +252,26 @@ public partial class RepositoryViewModel : RoutableViewModelBase
if (LocalDatabase.RepoAccessor == null) return;
var accessor = LocalDatabase.RepoAccessor;
var nodes = await CalculateFileTreeOfChangesNodeAsync(accessor.Select(
f => new LocalFileTreeAccessor.ChangeRecord(ChangeType.Add, f)));
f => new ChangeInfo(ChangeInfoType.Add, f)));
CurrentCommitFileTreeRaw.Clear();
CurrentCommitFileTreeRaw.AddRange(nodes);
}
private void SyncCommitsFromRepository()
{
CommitsRaw.Clear();
CommitsRaw.AddRange(Repository.Commits.Select(CommitTransitNode.FromCommit));
}
private void CollectChanges(List<LocalFileTreeAccessor.ChangeRecord> store, IEnumerable<LocalChangesNode> changesNode)
private void CollectChanges(List<ChangeInfo> 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
store.Add(new ChangeInfo(Enum.Parse<ChangeInfoType>(n.Type), new WorkspaceFile
{
WorkPath = n.FullPath,
ModifyTime = n.ModifiedTime!.Value
@ -263,7 +280,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase
}
}
private Task<List<LocalChangesNode>> CalculateFileTreeOfChangesNodeAsync(IEnumerable<LocalFileTreeAccessor.ChangeRecord> changesNode)
private Task<List<LocalChangesNode>> CalculateFileTreeOfChangesNodeAsync(IEnumerable<ChangeInfo> changesNode)
{
return Task.Run(() =>
{
@ -316,12 +333,127 @@ public partial class RepositoryViewModel : RoutableViewModelBase
if (role >= RepositoryModel.RepositoryRole.Reporter) IsReporterRole = true;
if (role >= RepositoryModel.RepositoryRole.Guest) IsGuestRole = true;
}
[ReactiveCommand]
private async Task RevertFileTreeToSelectedCommitKeepAsync()
{
var sel = Commits.RowSelection?.SelectedItem;
if (sel != null)
{
using var l = UIHelper.MakeLoading($"Reset (Keep) to Commit {sel.CommitId}");
var changes = new List<ChangeInfo>();
CollectChanges(changes, LocalChangeSetRaw);
var kid = Repository.Commits.First(x => x.CommitId.ToString() == sel.CommitId).CommitId;
var changeDict = changes.ToImmutableDictionary(x => x.File.WorkPath, x => x.File);
await RepositoryService.C
.SetPointerAndMergeDepotsWithLocalFromRemoteAsync(
Repository, kid, changeDict, RepositoryResetMethod.Keep);
await DetectLocalChangesAsyncCommand.Execute();
await RendererFileTreeAsync();
SyncCommitsFromRepository();
}
}
[ReactiveCommand]
private async Task RevertFileTreeToSelectedCommitSoftAsync()
{
var sel = Commits.RowSelection?.SelectedItem;
if (sel != null)
{
using var l = UIHelper.MakeLoading($"Reset (Soft) to Commit {sel.CommitId}");
var changes = new List<ChangeInfo>();
CollectChanges(changes, LocalChangeSetRaw);
if (changes.Count != 0)
{
var result = await UIHelper.SimpleAskAsync(
"There are commits that has not been committed. Do you wish discard them at all?",
DialogMode.Warning);
if (result == DialogResult.No) return;
}
var kid = Repository.Commits.First(x => x.CommitId.ToString() == sel.CommitId).CommitId;
var changeDict = changes.ToImmutableDictionary(x => x.File.WorkPath, x => x.File);
await RepositoryService.C
.SetPointerAndMergeDepotsWithLocalFromRemoteAsync(
Repository, kid, changeDict, RepositoryResetMethod.Soft);
await DetectLocalChangesAsyncCommand.Execute();
await RendererFileTreeAsync();
SyncCommitsFromRepository();
}
}
[ReactiveCommand]
private async Task RevertFileTreeToSelectedCommitHardAsync()
{
var sel = Commits.RowSelection?.SelectedItem;
if (sel != null)
{
using var l = UIHelper.MakeLoading($"Reset (Hard) to Commit {sel.CommitId}");
if (!await RepositoryService.C.UpdateCommitsHistoryFromServerAsync(Repository)) return;
var changes = new List<ChangeInfo>();
CollectChanges(changes, LocalChangeSetRaw);
var result = await UIHelper.SimpleAskAsync(
"All files will being matched with this commit. Do you wish to execute it?",
DialogMode.Warning);
if (result == DialogResult.No) return;
var kid = Repository.Commits.First(x => x.CommitId.ToString() == sel.CommitId).CommitId;
var changeDict = changes.ToImmutableDictionary(x => x.File.WorkPath, x => x.File);
await RepositoryService.C
.SetPointerAndMergeDepotsWithLocalFromRemoteAsync(
Repository, kid, changeDict, RepositoryResetMethod.Hard);
await DetectLocalChangesAsyncCommand.Execute();
await RendererFileTreeAsync();
SyncCommitsFromRepository();
}
}
[ReactiveCommand]
private async Task PullLatestRepositoryAsync()
{
using var l = UIHelper.MakeLoading("Pulling latest changes...");
var mayUpdate = await RepositoryService.C.IsPointedToCommitNotPeekResultFromServerAsync(Repository);
if (!mayUpdate.HasValue) return;
if (mayUpdate.Value == false)
{
await UIHelper.SimpleAskAsync("Everything is new, no needs to pull.");
return;
}
if (!await RepositoryService.C.UpdateCommitsHistoryFromServerAsync(Repository)) return;
if (!await RepositoryService.C.UpdateCommitsHistoryFromServerAsync(Repository)) return;
var changes = new List<ChangeInfo>();
CollectChanges(changes, LocalChangeSetRaw);
var kid = Repository.Commits.MaxBy(k => k.CommittedOn)!.CommitId;
var changeDict = changes.ToImmutableDictionary(x => x.File.WorkPath, x => x.File);
await RepositoryService.C.SetPointerAndMergeDepotsWithLocalFromRemoteAsync(
Repository, kid, changeDict, RepositoryResetMethod.Merge);
await DetectLocalChangesAsyncCommand.Execute();
await RendererFileTreeAsync();
SyncCommitsFromRepository();
}
[ReactiveCommand]
private async Task CommitSelectedChangesAsync()
{
var changes = new List<LocalFileTreeAccessor.ChangeRecord>();
var changes = new List<ChangeInfo>();
CollectChanges(changes, LocalChangeSetRaw);
if (changes.Count == 0)
@ -335,7 +467,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase
await UIHelper.SimpleAlert("Commit message can not be empty!");
return;
}
using var l = UIHelper.MakeLoading("Committing changes...");
var manifest = await RepositoryService.C.CommitWorkspaceAsBaselineAsync(Repository, changes, LocalDatabase.CommitMessage!);
if (manifest == null) return;
@ -344,6 +476,26 @@ public partial class RepositoryViewModel : RoutableViewModelBase
LocalDatabase.CommitMessage = string.Empty;
await DetectLocalChangesAsyncCommand.Execute();
await RendererFileTreeAsync();
SyncCommitsFromRepository();
}
[ReactiveCommand]
private async Task RevertSelectedChangesAsync()
{
var changes = new List<ChangeInfo>();
CollectChanges(changes, LocalChangeSetRaw);
if (changes.Count == 0)
{
await UIHelper.SimpleAlert("You haven't choose any changes yet!");
return;
}
using var l = UIHelper.MakeLoading("Reverting changes...");
RepositoryService.C.RevertChangesToBaseline(Repository, changes);
await DetectLocalChangesAsyncCommand.Execute();
await RendererFileTreeAsync();
SyncCommitsFromRepository();
}
[ReactiveCommand]
@ -363,23 +515,6 @@ public partial class RepositoryViewModel : RoutableViewModelBase
}
}
[ReactiveCommand]
private async Task PullLatestRepositoryAsync()
{
using var l = UIHelper.MakeLoading("Pulling latest changes...");
var mayUpdate = await RepositoryService.C.IsCurrentPointedToCommitIsNotPeekResultFromServerAsync(Repository);
if (!mayUpdate.HasValue) return;
if (mayUpdate.Value == false)
{
await UIHelper.SimpleAskAsync("Everything is new, no needs to pull.");
return;
}
if (!await RepositoryService.C.UpdateCommitsHistoryFromServerAsync(Repository)) return;
var kid = Repository.Commits.MaxBy(k => k.CommittedOn)!.CommitId;
await RepositoryService.C.ResetCommitPointerToTargetAndMergeDepotsIntoRepositoryFromRemoteAsync(Repository, kid);
}
[ReactiveCommand]
private async Task CloseRepositoryAsync()
{

View File

@ -7,15 +7,25 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Flawless.Client.Views.RepositoryPage.RepoCommitPageView">
<Grid ColumnDefinitions="2*, *">
<TreeDataGrid Grid.Column="0" Source="{Binding Commits}"/>
<TreeDataGrid Grid.Column="0" Source="{Binding Commits}">
<TreeDataGrid.ContextMenu>
<ContextMenu>
<MenuItem Header="View File Tree"/>
<MenuItem Header="Reset To">
<MenuItem Header="Keep" Command="{Binding RevertFileTreeToSelectedCommitKeepCommand}"/>
<MenuItem Header="Soft" Command="{Binding RevertFileTreeToSelectedCommitSoftCommand}"/>
<MenuItem Header="Hard" Command="{Binding RevertFileTreeToSelectedCommitHardCommand}"/>
</MenuItem>
</ContextMenu>
</TreeDataGrid.ContextMenu>
</TreeDataGrid>
<Border Grid.Column="1" Classes="Shadow" Theme="{StaticResource CardBorder}">
<ScrollViewer IsVisible="{Binding !!Commits.RowSelection.SelectedItem}">
<StackPanel Spacing="8">
<Label FontWeight="600" FontSize="18" Content="Commit Details"/>
<Label Content="{Binding Commits.RowSelection.SelectedItem.FullCommitId, FallbackValue='00000000-0000', StringFormat='Id: {0}'}"/>
<Label Content="{Binding Commits.RowSelection.SelectedItem.Author, FallbackValue='Author', StringFormat='Author: {0}'}"/>
<Label Content="{Binding Commits.RowSelection.SelectedItem.CommitAt, FallbackValue='At Time', StringFormat='Time: {0}'}"/>
<Label FontWeight="400" FontSize="14" Content="{Binding Commits.RowSelection.SelectedItem.FullMessage, FallbackValue='Commit messages.'}"/>
<Label Content="{Binding Commits.RowSelection.SelectedItem.FullCommitId, FallbackValue='00000000-0000'}"/>
<Label Content="{Binding Commits.RowSelection.SelectedItem.Author, FallbackValue='By: ', StringFormat='By: {0}'}"/>
<Label Content="{Binding Commits.RowSelection.SelectedItem.CommitAt, FallbackValue='At: ', StringFormat='At: {0}'}"/>
<TextBox Classes="TextArea" IsReadOnly="True" Text="{Binding Commits.RowSelection.SelectedItem.FullMessage, FallbackValue='Commit messages.'}"/>
</StackPanel>
</ScrollViewer>
</Border>

View File

@ -9,12 +9,5 @@
<Grid ColumnDefinitions="2*, *">
<TreeDataGrid Grid.Column="0" Grid.ColumnSpan="2" Source="{Binding FileTree}">
</TreeDataGrid>
<!-- <Border Grid.Column="1" Classes="Shadow" Theme="{StaticResource CardBorder}"> -->
<!-- <ScrollViewer> -->
<!-- <StackPanel Spacing="4"> -->
<!-- <Label Content="File History"/> -->
<!-- </StackPanel> -->
<!-- </ScrollViewer> -->
<!-- </Border> -->
</Grid>
</UserControl>

View File

@ -20,14 +20,21 @@
</StackPanel>
<TreeDataGrid Grid.Row="1" Grid.Column="0" Source="{Binding LocalChange}" CanUserSortColumns="True"/>
<Border Grid.Row="1" Grid.Column="2" Classes="Shadow" Theme="{StaticResource CardBorder}">
<u:Form HorizontalAlignment="Stretch">
<u:IconButton Icon="{StaticResource SemiIconDownload}" Content="Pull" HorizontalAlignment="Stretch"
Command="{Binding PullLatestRepositoryCommand}"/>
<StackPanel Spacing="8">
<u:ElasticWrapPanel Orientation="Horizontal" HorizontalAlignment="Stretch">
<u:IconButton
Icon="{StaticResource SemiIconDownload}" Content="Pull" HorizontalAlignment="Stretch"
Command="{Binding PullLatestRepositoryCommand}"/>
<u:IconButton
Icon="{StaticResource SemiIconBackward}" Content="Revert Select" HorizontalAlignment="Stretch"
Command="{Binding RevertSelectedChangesCommand}"/>
</u:ElasticWrapPanel>
<u:Divider></u:Divider>
<TextBox Text="{Binding LocalDatabase.CommitMessage}"
Classes="TextArea" MaxHeight="300" Watermark="Description of this commit"/>
<u:IconButton Content="Commit" Icon="{StaticResource SemiIconUpload}" HorizontalAlignment="Stretch"
Command="{Binding CommitSelectedChangesCommand}"/>
</u:Form>
</StackPanel>
</Border>
</Grid>
</UserControl>