1
0

1339 lines
47 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using AvaloniaEdit.Utils;
using Flawless.Abstraction;
using Flawless.Client.Models;
using Flawless.Client.Remote;
using Flawless.Core.BinaryDataFormat;
using Newtonsoft.Json;
using Refit;
using CommitManifest = Flawless.Core.Modal.CommitManifest;
using DepotLabel = Flawless.Core.Modal.DepotLabel;
using WorkspaceFile = Flawless.Core.Modal.WorkspaceFile;
namespace Flawless.Client.Service;
public class RepositoryService : BaseService<RepositoryService>
{
public ObservableCollection<RepositoryModel> Repositories => _repositories;
private readonly ObservableCollection<RepositoryModel> _repositories = new();
private readonly Dictionary<RepositoryModel, RepositoryLocalDatabaseModel> _localRepoDbModel = new();
private readonly HashSet<RepositoryModel> _openedRepos = new();
private bool TryCreateRepositoryBaseStorageStructure(RepositoryModel repo)
{
var dbPath = PathUtility.GetWorkspaceDbPath(Api.Current.Username.Value!, repo.OwnerName, repo.Name);
if (File.Exists(dbPath)) return false;
// Get directories
var localRepoDb = GetRepositoryLocalDatabase(repo);
var folderPath = PathUtility.GetWorkspaceManagerPath(Api.Current.Username.Value!, 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 localRepo = GetRepositoryLocalDatabase(repo);
localRepo.LastOprationTime = DateTime.Now;
var dbPath = PathUtility.GetWorkspaceDbPath(Api.Current.Username.Value!, repo.OwnerName, repo.Name);
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
using var writeFs = new StreamWriter(new FileStream(dbPath, FileMode.OpenOrCreate));
JsonSerializer.CreateDefault().Serialize(writeFs, localRepo);
return true;
}
public RepositoryLocalDatabaseModel GetRepositoryLocalDatabase(RepositoryModel repo)
{
if (!_localRepoDbModel.TryGetValue(repo, out var localRepo))
{
var dbPath = PathUtility.GetWorkspaceDbPath(Api.Current.Username.Value!, 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.
localRepo.RootModal = repo;
localRepo.LocalAccessor = new LocalFileTreeAccessor(repo, []);
}
else
{
// Create new one.
localRepo = new RepositoryLocalDatabaseModel
{
RootModal = repo,
LocalAccessor = new LocalFileTreeAccessor(repo, [])
};
}
_localRepoDbModel.Add(repo, localRepo);
}
return localRepo;
}
public async ValueTask<RepositoryModel?> CreateRepositoryOnServerAsync(string repositoryName, string description)
{
RepositoryModel repo;
var api = Api.C;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return null;
}
var r = await api.Gateway.RepoCreate(repositoryName, description);
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)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return null;
}
return repo;
}
public async ValueTask<bool> UpdateRepositoriesFromServerAsync()
{
var api = Api.C;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return false;
}
var result = (await api.Gateway.RepoList()).Result;
var dict = result.ToDictionary(rp => RepositoryModel.GetStandaloneName(rp.RepositoryName, rp.OwnerUsername));
for (var i = 0; i < Repositories.Count; i++)
{
var ele = Repositories[i];
if (!dict.Remove(ele.StandaloneName, out var role))
{
Repositories.RemoveAt(i);
i -= 1;
continue;
}
ele.Archived = ele.Archived;
ele.Description = ele.Description;
}
foreach (var (repoStandaloneName, rsp) in dict)
{
var repo = new RepositoryModel();
repo.Name = rsp.RepositoryName;
repo.OwnerName = rsp.OwnerUsername;
repo.StandaloneName = repoStandaloneName;
repo.Description = rsp.Description;
repo.Archived = rsp.IsArchived;
repo.OwnByCurrentUser = (int) rsp.Role == (int) RepositoryModel.RepositoryRole.Owner;
Repositories.Add(repo);
}
}
catch (Exception e)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return false;
}
return true;
}
public async ValueTask<bool> UpdateRepositoriesDownloadedStatusFromDiskAsync()
{
foreach (var repo in _repositories)
{
var isFolderExists = Directory.Exists(PathUtility.GetWorkspacePath(Api.Current.Username.Value!, repo.OwnerName, repo.Name));
var isDbFileExists = File.Exists(PathUtility.GetWorkspaceDbPath(Api.Current.Username.Value!, repo.OwnerName, repo.Name));
repo.IsDownloaded = isFolderExists && isDbFileExists;
}
return true;
}
public async ValueTask<bool> DeleteRepositoryOnServerAsync(RepositoryModel repo)
{
var api = Api.C;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return false;
}
await api.Gateway.DeleteRepo(repo.Name, repo.OwnerName);
return true;
}
catch (Exception e)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return false;
}
}
public async ValueTask<bool> DeleteMemberFromServerAsync(RepositoryModel repo, RepositoryModel.Member member)
{
var api = Api.C;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return false;
}
await api.Gateway.DeleteUser(repo.OwnerName, member.Username);
return await UpdateMembersFromServerAsync(repo);
}
catch (Exception e)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return false;
}
}
public async ValueTask<bool> ModifyMemberFromServerAsync(RepositoryModel repo, RepositoryModel.Member member)
{
var api = Api.C;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return false;
}
await api.Gateway.UpdateUser(repo.Name, member.Username, member.Role);
return await UpdateMembersFromServerAsync(repo);
}
catch (Exception e)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return false;
}
}
public async ValueTask<bool> AddMemberFromServerAsync(RepositoryModel repo, string grantedTo, RepositoryModel.RepositoryRole role)
{
var api = Api.C;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return false;
}
await api.Gateway.UpdateUser(repo.Name, grantedTo, role);
return await UpdateMembersFromServerAsync(repo);
}
catch (Exception e)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return false;
}
}
public async ValueTask<bool> UpdateIssuesListFromServerAsync(RepositoryModel repo)
{
var api = Api.C;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return false;
}
var issues = (await api.Gateway.List(repo.Name, repo.OwnerName))
.Result.ToImmutableDictionary(x => (ulong) x.Id);
for (var i = 0; i < repo.Issues.Count; i++)
{
if (!issues.ContainsKey(repo.Issues[i].Id))
repo.Issues.RemoveAt(i);
}
foreach (var (id, info) in issues)
{
var i = repo.Issues.FirstOrDefault(x => x.Id == id);
var tags = info.Tag.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (i == null)
{
i = new RepositoryModel.Issue
{
Author = info.Author,
Id = id,
Title = info.Title,
Description = null,
Closed = info.Closed,
CreatedAt = info.CreateAt.UtcDateTime,
LastUpdatedAt = info.LastUpdate.UtcDateTime
};
i.Tags.AddRange(tags);
repo.Issues.Add(i);
}
else
{
i.Title = info.Title;
i.Closed = info.Closed;
i.LastUpdatedAt = info.LastUpdate.UtcDateTime;
i.Author = info.Author;
i.Tags.Clear();
i.Tags.AddRange(tags);
}
}
repo.Issues.Sort((x, y) => (int) ((long) x.Id - (long) y.Id));
}
catch (Exception e)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return false;
}
return true;
}
public async ValueTask<bool> UpdateIssueDetailsFromServerAsync(RepositoryModel repo, ulong issueId, bool titleOnly)
{
var api = Api.C;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return false;
}
var issue = await api.Gateway.Issue(repo.Name, repo.OwnerName, (long)issueId);
var entity = repo.Issues.FirstOrDefault(x => x.Id == issueId);
var tags = issue.Tag.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (entity == null)
{
entity = new RepositoryModel.Issue
{
Author = issue.Author,
Id = (ulong) issue.Id,
Title = issue.Title,
Description = null,
Closed = issue.Closed,
CreatedAt = issue.CreateAt.UtcDateTime,
LastUpdatedAt = issue.LastUpdate.UtcDateTime
};
entity.Tags.AddRange(tags);
repo.Issues.Add(entity);
if (!titleOnly)
{
var details = (await api.Gateway.Comments(repo.Name, repo.OwnerName, (long)issueId))
.Result.ToImmutableDictionary(x => x.CommentId);
entity.Comments.AddRange(details.Select(x => new RepositoryModel.Comment
{
Id = (ulong)x.Key,
Author = x.Value.Author,
Content = x.Value.Content,
CreatedAt = x.Value.CreatedAt.UtcDateTime,
ReplyTo = x.Value.ReplyToId.HasValue ? (ulong)x.Value.ReplyToId.Value : null
}));
}
}
else
{
entity.Title = issue.Title;
entity.Closed = issue.Closed;
entity.LastUpdatedAt = issue.LastUpdate.UtcDateTime;
entity.Author = issue.Author;
entity.Tags.Clear();
entity.Tags.AddRange(tags);
if (!titleOnly)
{
var details = (await api.Gateway.Comments(repo.Name, repo.OwnerName, (long)issueId))
.Result.ToImmutableDictionary(x => x.CommentId);
for (var i = 0; i < entity.Comments.Count; i++)
{
var c = entity.Comments[i];
if (!details.ContainsKey((long) c.Id)) repo.Issues.RemoveAt(i);
}
foreach (var (key, value) in details)
{
var d = entity.Comments.FirstOrDefault(x => x.Id == (ulong) key);
if (d == null)
{
var c = new RepositoryModel.Comment
{
Id = (ulong)key,
Author = value.Author,
Content = value.Content,
CreatedAt = value.CreatedAt.UtcDateTime,
ReplyTo = value.ReplyToId.HasValue ? (ulong)value.ReplyToId.Value : null
};
entity.Comments.Add(c);
}
else
{
d.Author = value.Author;
d.Content = value.Content;
d.CreatedAt = value.CreatedAt.UtcDateTime;
d.ReplyTo = value.ReplyToId.HasValue ? (ulong)value.ReplyToId.Value : null;
}
}
}
}
repo.Issues.Sort((x, y) => (int) ((long) x.Id - (long) y.Id));
}
catch (Exception e)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return false;
}
return true;
}
public async ValueTask<ulong?> CreateIssueAsync(RepositoryModel repo, string title, string description, IEnumerable<string>? tags)
{
var api = Api.C;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return null;
}
StringBuilder? tagString = null;
if (tags != null)
{
new StringBuilder().AppendJoin(',', tags);
}
var issue = await api.Gateway.Create(
repo.OwnerName,
repo.Name,
new CreateIssueRequest { Title = title, Description = description, Tag = tagString?.ToString()});
var entity = new RepositoryModel.Issue
{
Author = issue.Author,
Id = (ulong) issue.Id,
Title = issue.Title,
Description = null,
Closed = issue.Closed,
CreatedAt = issue.CreateAt.UtcDateTime,
LastUpdatedAt = issue.LastUpdate.UtcDateTime
};
entity.Tags.AddRange(tags ?? []);
repo.Issues.Add(entity);
repo.Issues.Sort((x, y) => (int) ((long) x.Id - (long) y.Id));
return entity.Id;
}
catch (Exception e)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return null;
}
}
public async ValueTask<bool> CloseIssueAsync(RepositoryModel repo, ulong issueId)
{
var api = Api.C;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return false;
}
await api.Gateway.Close(repo.OwnerName, repo.Name, (long) issueId);
await UpdateIssueDetailsFromServerAsync(repo, issueId, true);
return true;
}
catch (Exception e)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return false;
}
}
public async ValueTask<bool> ReopenIssueAsync(RepositoryModel repo, ulong issueId)
{
var api = Api.C;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return false;
}
await api.Gateway.Reopen(repo.OwnerName, repo.Name, (long) issueId);
await UpdateIssueDetailsFromServerAsync(repo, issueId, true);
return true;
}
catch (Exception e)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return false;
}
}
public async ValueTask<bool> AddCommentAsync(RepositoryModel repo, ulong issueId, string content, ulong? replyTo)
{
var api = Api.C;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return false;
}
await api.Gateway.Comment(repo.OwnerName, repo.Name, (long) issueId,
new AddCommentRequest { Content = content, ReplyTo = replyTo.HasValue ? (long) replyTo.Value : null });
return true;
}
catch (Exception e)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return false;
}
}
public async ValueTask<bool> UpdateIssueAsync(RepositoryModel repo, ulong issueId, string? title, string? description, IEnumerable<string>? tags)
{
var api = Api.C;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return false;
}
StringBuilder? tagString = null;
if (tags != null)
{
new StringBuilder().AppendJoin(',', tags);
}
await api.Gateway.Edit(repo.OwnerName, repo.Name, (long) issueId,
new UpdateIssueRequest { Title = title, Description = description, Tag = tagString?.ToString()});
return true;
}
catch (Exception e)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return false;
}
}
public async ValueTask<bool> UpdateMembersFromServerAsync(RepositoryModel repo)
{
var api = Api.C;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return false;
}
var members = await api.Gateway.GetUsers(repo.Name, repo.OwnerName);
// Update existed
var dict = members.Result.ToDictionary(m => m.Username);
dict.Add(repo.OwnerName, new RepoUserRole{ Username = repo.OwnerName, Role = RepositoryRole._3});
for (var i = 0; i < repo.Members.Count; i++)
{
var ele = repo.Members[i];
if (!dict.Remove(ele.Username, out var role))
{
repo.Members.RemoveAt(i);
i -= 1;
continue;
}
ele.Username = role.Username;
ele.Role = (RepositoryModel.RepositoryRole) role.Role;
ele.CanEdit = ele.Role != RepositoryModel.RepositoryRole.Owner && repo.OwnByCurrentUser;
}
// Add missing
foreach (var role in dict.Values)
{
var rl = (RepositoryModel.RepositoryRole)role.Role;
var r = new RepositoryModel.Member
{
Username = role.Username,
Role = rl,
CanEdit = rl != RepositoryModel.RepositoryRole.Owner && repo.OwnByCurrentUser
};
repo.Members.Add(r);
}
}
catch (Exception e)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return false;
}
return true;
}
public bool IsRepositoryOpened(RepositoryModel repo)
{
return _openedRepos.Any(r => r == repo);
}
public async ValueTask<bool> 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<bool> OpenRepositoryOnStorageAsync(RepositoryModel repo)
{
if (!await UpdateRepositoriesDownloadedStatusFromDiskAsync() || repo.IsDownloaded == false) return false;
if (!await UpdateCommitsHistoryFromServerAsync(repo)) return false;
var ls = GetRepositoryLocalDatabase(repo);
if (ls.CurrentCommit != null)
{
var accessor = await DownloadDepotsAndUseLocalCachesToGenerateRepositoryFileTreeAccessorFromServerAsync(repo, ls.CurrentCommit.Value);
if (accessor == null) return false;
ls.RepoAccessor = accessor;
// Remember to cache accessor everytime it will being used.
await accessor.CreateCacheAsync();
ls.LocalAccessor.SetBaseline(accessor);
}
SaveRepositoryLocalDatabaseChanges(repo);
_openedRepos.Add(repo);
return true;
}
public async ValueTask<bool> CreateNewRepositoryOnStorageAsync(RepositoryModel repo)
{
// Create basic structures.
if (!TryCreateRepositoryBaseStorageStructure(repo)) return false;
SaveRepositoryLocalDatabaseChanges(repo);
repo.IsDownloaded = true;
_openedRepos.Add(repo);
return true;
}
public async ValueTask<bool> CloneRepositoryFromRemoteAsync(RepositoryModel repo)
{
// Create basic structures.
if (!TryCreateRepositoryBaseStorageStructure(repo)) return false;
if (!await UpdateCommitsHistoryFromServerAsync(repo)) return false;
var peekCommit = repo.Commits.MaxBy(sl => sl.CommittedOn);
if (peekCommit == null) return false; // Should not use this function!
// Download base repo info
var accessor = await DownloadDepotsAndUseLocalCachesToGenerateRepositoryFileTreeAccessorFromServerAsync(repo, peekCommit.CommitId);
if (accessor == null)
{
await DeleteFromDiskAsync(repo);
return false;
};
// Remember to cache accessor everytime it will being used.
await accessor.CreateCacheAsync();
var ls = GetRepositoryLocalDatabase(repo);
ls.CurrentCommit = peekCommit.CommitId;
ls.RepoAccessor = accessor;
ls.LocalAccessor.SetBaseline(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);
if (!accessor.TryWriteDataIntoStream(f.WorkPath, pfs))
throw new InvalidDataException($"Can not write {f.WorkPath} into repository.");
}
}
catch (Exception e)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
await DeleteFromDiskAsync(repo);
return false;
}
SaveRepositoryLocalDatabaseChanges(repo);
repo.IsDownloaded = true;
_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> SetPointerAndMergeDepotsWithLocalFromRemoteAsync
(RepositoryModel repo, Guid commitId, IReadOnlyDictionary<string, WorkspaceFile> localChangesTable,
RepositoryResetMethod method)
{
// Try Download base repo info
var accessor = await DownloadDepotsAndUseLocalCachesToGenerateRepositoryFileTreeAccessorFromServerAsync(repo, commitId);
if (accessor == null)
{
await DeleteFromDiskAsync(repo);
return false;
};
// Remember to cache accessor everytime it will being used.
await accessor.CreateCacheAsync();
var ls = GetRepositoryLocalDatabase(repo);
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);
if (directory != null) Directory.CreateDirectory(directory);
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)
{
// Revert baseline
try
{
ls.RepoAccessor = oldAccessor;
if (oldAccessor != null) ls.LocalAccessor.SetBaseline(oldAccessor);
else ls.LocalAccessor.SetBaseline([]);
}
catch (Exception exception) { Console.WriteLine(exception); }
UIHelper.NotifyError(e);
Console.WriteLine(e);
await DeleteFromDiskAsync(repo);
return false;
}
// Dispose old RepoAccessor
try { if (oldAccessor != null) await oldAccessor.DisposeAsync(); }
catch (Exception exception) { Console.WriteLine(exception); }
SaveRepositoryLocalDatabaseChanges(repo);
repo.IsDownloaded = true;
_openedRepos.Add(repo);
return true;
}
public async ValueTask<bool?> IsPointedToCommitNotPeekResultFromServerAsync(RepositoryModel repo)
{
var api = Api.C;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return false;
}
var rsp = await api.Gateway.PeekCommit(repo.OwnerName, repo.Name);
var emptyRepo = repo.Commits.Count == 0;
// If they both empty
if ((rsp.Result == Guid.Empty) && emptyRepo) return false;
if (emptyRepo) return true;
var ptr = GetRepositoryLocalDatabase(repo).CurrentCommit;
return rsp.Result != ptr;
}
catch (Exception e)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return false;
}
}
public async ValueTask<bool> UpdateCommitsHistoryFromServerAsync(RepositoryModel repo)
{
var api = Api.C;
try
{
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return false;
}
var rsp = await api.Gateway.ListCommit(repo.OwnerName, repo.Name);
// 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)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return false;
}
return true;
}
public async ValueTask<RepositoryFileTreeAccessor?>
DownloadDepotsAndUseLocalCachesToGenerateRepositoryFileTreeAccessorFromServerAsync
(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 depotsRoot = PathUtility.GetWorkspaceDepotCachePath(Api.Current.Username.Value!, repo.OwnerName, repo.Name);
Directory.CreateDirectory(depotsRoot);
// 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);
if (downloadedDepots == null) return null;
try
{
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 streamMap = downloadedDepots.ToDictionary(i => i.Item1, i => i.Item2!);
foreach (var dl in mainDepotLabel)
{
// If this file is not being opened, open it from file system
if (!streamMap.ContainsKey(dl.Id))
{
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);
}
catch (Exception e)
{
UIHelper.NotifyError(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 id, Stream stream)> depots)
{
var depotsRoot = PathUtility.GetWorkspaceDepotCachePath(Api.Current.Username.Value!, repo.OwnerName, repo.Name);
foreach (var d in depots)
{
var dst = Path.Combine(depotsRoot, d.id.ToString());
await using var ws = new FileStream(dst, FileMode.Create);
// Make sure always to be at begin.
d.stream.Seek(0, SeekOrigin.Begin);
await d.stream.CopyToAsync(ws);
d.stream.Seek(0, SeekOrigin.Begin);
}
}
public async ValueTask<(Guid, Stream)[]?> DownloadDepotsAndCopyNetworkStreamIntoNewMemoryStreamFromServerAsync
(RepositoryModel repo, IEnumerable<DepotLabel> depotsId)
{
try
{
var api = Api.C;
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return null;
}
var result = new List<(Guid, Stream)>();
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)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return null;
}
}
public async ValueTask<CommitManifest?> DownloadManifestFromServerAsync(RepositoryModel repo, Guid manifestId)
{
try
{
var api = Api.C;
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return null;
}
var rsp = await api.Gateway.FetchManifest(repo.OwnerName, repo.Name, manifestId.ToString());
return new(
rsp.ManifestId,
rsp.Depot.Select(x => new DepotLabel(x.Id, x.Length)).ToArray(),
rsp.FilePaths.Select(x => new WorkspaceFile(x.ModifyTime.UtcDateTime, x.WorkPath)).ToArray());
}
catch (Exception e)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return null;
}
}
public async ValueTask<bool> DeleteFromDiskAsync(RepositoryModel repo)
{
try
{
var path = PathUtility.GetWorkspacePath(Api.Current.Username.Value!, repo.OwnerName, repo.Name);
if (Directory.Exists(path)) Directory.Delete(path, true);
}
catch (Exception e)
{
UIHelper.NotifyError(e);
Console.WriteLine(e);
return false;
}
repo.IsDownloaded = false;
return true;
}
public async ValueTask<CommitManifest?> CommitWorkspaceAsBaselineAsync
(RepositoryModel repo, IEnumerable<ChangeInfo> changes, string message)
{
// Check if current version is the latest
var api = Api.C;
var requireUpdate = await IsPointedToCommitNotPeekResultFromServerAsync(repo);
if (!requireUpdate.HasValue) return null;
if (requireUpdate.Value)
{
await UIHelper.SimpleAlert("Can not commit workspace at this time! Please pull from remote server first...");
return null;
}
var localDb = GetRepositoryLocalDatabase(repo);
var manifestList = CreateCommitManifestByCurrentBaselineAndChanges(localDb.LocalAccessor, changes);
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(Api.Current.Username.Value!, 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<ChangeInfo> 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 ChangeInfoType.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 ChangeInfoType.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 ChangeInfoType.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 ChangeInfoType.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(Api.Current.Username.Value!, 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;
}
}