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.Reactive.Disposables; using System.Text; using System.Threading.Tasks; using AvaloniaEdit.Utils; using Flawless.Abstraction; using Flawless.Client.Models; using Flawless.Client.Remote; using Flawless.Client.ViewModels.ModalBox; using Flawless.Client.Views.ModalBox; using Flawless.Core.BinaryDataFormat; using Newtonsoft.Json; using Refit; using Ursa.Controls; 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 { public ObservableCollection Repositories => _repositories; private readonly ObservableCollection _repositories = new(); private readonly Dictionary _localRepoDbModel = new(); private readonly HashSet _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 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 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 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 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 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 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 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 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.OwnerName, repo.Name)) .Result.ToImmutableDictionary(x => 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 UpdateIssueDetailsFromServerAsync(RepositoryModel repo, int 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, 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 = 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, issueId)) .Result.ToImmutableDictionary(x => x.CommentId); entity.Comments.AddRange(details.Select(x => new RepositoryModel.Comment { Id = x.Key, Author = x.Value.Author, Content = x.Value.Content, CreatedAt = x.Value.CreatedAt.UtcDateTime, ReplyTo = x.Value.ReplyToId.HasValue ? 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, issueId)) .Result.ToImmutableDictionary(x => x.CommentId); for (var i = 0; i < entity.Comments.Count; i++) { var c = entity.Comments[i]; if (!details.ContainsKey(c.Id)) repo.Issues.RemoveAt(i); } foreach (var (key, value) in details) { var d = entity.Comments.FirstOrDefault(x => x.Id == key); if (d == null) { var c = new RepositoryModel.Comment { Id = key, Author = value.Author, Content = value.Content, CreatedAt = value.CreatedAt.UtcDateTime, ReplyTo = value.ReplyToId.HasValue ? 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 ? 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 CreateIssueAsync(RepositoryModel repo, string title, string description, IEnumerable? 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 = 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 CloseIssueAsync(RepositoryModel repo, int 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 ReopenIssueAsync(RepositoryModel repo, int 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 AddCommentAsync(RepositoryModel repo, int issueId, string content, int? replyTo) { var api = Api.C; try { if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync())) { api.ClearGateway(); return false; } var req = new AddCommentRequest { Content = content, ReplyTo = replyTo.HasValue ? replyTo.Value : null }; await api.Gateway.Comment(repo.OwnerName, repo.Name, issueId, req); return true; } catch (Exception e) { UIHelper.NotifyError(e); Console.WriteLine(e); return false; } } public async ValueTask UpdateIssueAsync(RepositoryModel repo, int issueId, string? title, string? description, IEnumerable? 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, 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 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.OwnerName, repo.Name); // 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 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 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 CreateNewRepositoryOnStorageAsync(RepositoryModel repo) { // Create basic structures. if (!TryCreateRepositoryBaseStorageStructure(repo)) return false; SaveRepositoryLocalDatabaseChanges(repo); repo.IsDownloaded = true; _openedRepos.Add(repo); return true; } public async ValueTask 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.TryWriteDataIntoDisk(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 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!.TryWriteDataIntoDisk(ci.File.WorkPath, wfs); break; case ChangeInfoType.Folder: default: break; } } catch (Exception e) { UIHelper.NotifyError(e); Console.WriteLine(e); } } return true; } public async ValueTask SetPointerAndMergeDepotsWithLocalFromRemoteAsync (RepositoryModel repo, Guid commitId, IReadOnlyDictionary 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.Soft; var resetChanges = method is RepositoryResetMethod.Soft or RepositoryResetMethod.Hard; var hardMode = method is RepositoryResetMethod.Hard; List 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.TryWriteDataIntoDisk(f.WorkPath, pfs)) throw new InvalidDataException($"Can not write {f.WorkPath} into repository. (Reset changes)"); } else { if (!accessor.TryWriteDataIntoDisk(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. if (unmerged.Count > 0) { await MergeFilesAsync(repo, accessor, unmerged); } } 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 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 MergeFilesAsync(RepositoryModel repo, RepositoryFileTreeAccessor repoAccessor, IEnumerable unmerged) { var root = PathUtility.GetWorkspaceManagerPath(Api.C.Username.Value, repo.OwnerName, repo.Name); var tempFolder = Path.Combine(root, "MergeInTemp"); var wsRoot = PathUtility.GetWorkspacePath(Api.C.Username.Value, repo.OwnerName, repo.Name); var left = Path.Combine(tempFolder, "left"); var right = Path.Combine(tempFolder, "right"); using (Disposable.Create(tempFolder, r => Directory.Delete(r, true))) { Directory.CreateDirectory(root); foreach (var p in unmerged) { try { // Prepare for left and right file var wfs = WorkPath.ToPlatformPath(wsRoot, p.WorkPath); File.Copy(wfs, left, true); await using var rightFs = File.Create(right); repoAccessor.TryWriteDataIntoDisk(p.WorkPath, rightFs, out _); // Raise dialog window var opt = UIHelper.DefaultOverlayDialogOptionsYesNo(); opt.Buttons = DialogButton.None; opt.Title = "Merge files"; opt.IsCloseButtonVisible = true; var vm = new MergeDialogViewModel(p.WorkPath, wfs, p.WorkPath, left, right); await OverlayDialog.ShowModal(vm, AppDefaultValues.HostId, opt); } catch (Exception e) { Console.Error.WriteLine(e); } } } return true; } public async ValueTask 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 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 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); return null; } } public async ValueTask 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, x.Size)).ToArray()); } catch (Exception e) { UIHelper.NotifyError(e); return null; } } public async ValueTask 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 CommitWorkspaceAsync (RepositoryModel repo, IEnumerable changes, string message, bool forceBaseline = false) { // Check if current version is the latest var api = Api.C; if (!await UpdateCommitsHistoryFromServerAsync(repo)) return null; 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, miniManifestList) = CreateCommitManifestByCurrentBaselineAndChanges(localDb.LocalAccessor, changes); var snapshot = manifestList.Select(l => $"{l.ModifyTime.ToBinary()}${l.WorkPath}"); Guid commitId; try { // Renew for once. if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync())) { api.ClearGateway(); return null; } // Decide create new depot, create full depot or create addon depot. var hasAddOrModify = localDb.LocalAccessor.Changes.Any(x => x.Value.Type == ChangeInfoType.Add || x.Value.Type == ChangeInfoType.Modify); var newDepot = forceBaseline || hasAddOrModify || localDb.RepoAccessor == null; if (newDepot) { var changesSize = localDb.LocalAccessor.Changes.Values.Sum(x => (long) x.File.Size); var baselineSize = localDb.RepoAccessor?.Sum(x => (long) x.Size) ?? 0; var changesRatio = changesSize / (double) baselineSize; var isBaseline = forceBaseline || localDb.CurrentCommit == null || changesRatio > 0.8f; var depotFiles = isBaseline ? manifestList : miniManifestList; // Generate depot var tempDepotPath = await CreateDepotIntoTempFileAsync(repo, depotFiles); if (tempDepotPath == null) return null; Console.WriteLine($"Create new {(isBaseline ? "baseline" : "overlay")} depot at \"{tempDepotPath}\""); // Upload and create commit await using var str = File.OpenRead(tempDepotPath); if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync())) { api.ClearGateway(); return null; } var requireDepot = isBaseline ? [] : new[] {repo.Commits.First().DepotId.ToString()}; var rsp = await api.Gateway.CreateCommit(repo.OwnerName, repo.Name, new StreamPart(str, Path.GetFileName(tempDepotPath)), message, snapshot, requireDepot, ""); commitId = rsp.CommitId; // 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); } else { // Upload and create commit if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync())) { api.ClearGateway(); return null; } var rsp = await api.Gateway.CreateCommit(repo.OwnerName, repo.Name, null!, message, snapshot, [], repo.Commits.First().DepotId.ToString()); commitId = rsp.CommitId; } // Fetch mapped manifest var manifest = await DownloadManifestFromServerAsync(repo, commitId); if (manifest == null) return null; var accessor = await DownloadDepotsAndUseLocalCachesToGenerateRepositoryFileTreeAccessorFromServerAsync(repo, commitId); if (accessor == null) return null; //todo this is a really fatal issue... await accessor.CreateCacheAsync(); if (localDb.RepoAccessor != null) { try { await localDb.RepoAccessor.DisposeAsync(); } catch (Exception e) { UIHelper.NotifyError(e); Console.WriteLine(e); } } // Point to latest state. localDb.RepoAccessor = accessor; localDb.CurrentCommit = commitId; localDb.LocalAccessor.SetBaseline(accessor); SaveRepositoryLocalDatabaseChanges(repo); Console.WriteLine($"Successful create commit as {commitId}"); return manifest; } catch (Exception e) { UIHelper.NotifyError(e); Console.WriteLine(e); return null; } } private (List, List) CreateCommitManifestByCurrentBaselineAndChanges (LocalFileTreeAccessor accessor, IEnumerable changes, bool hard = false) { // Create a new depot file manifest. var files = accessor.BaselineFiles.Values.ToList(); var overlayFsStatus = new List(); 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); overlayFsStatus.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; overlayFsStatus.Add(c.File); break; } } } return (files, overlayFsStatus); } public async ValueTask CreateDepotIntoTempFileAsync(RepositoryModel repo, IEnumerable 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; } public async ValueTask UpdateWebhooksFromServerAsync(RepositoryModel repo) { var api = Api.C; try { if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync())) { api.ClearGateway(); return false; } var webhooks = await api.Gateway.WebhooksGet(repo.OwnerName, repo.Name); var dict = webhooks.ToDictionary(w => w.Id); for (var i = 0; i < repo.Webhooks.Count; i++) { var wh = repo.Webhooks[i]; if (!dict.Remove(wh.Id, out var newWh)) { repo.Webhooks.RemoveAt(i); i -= 1; continue; } wh.TargetUrl = newWh.TargetUrl; wh.Active = newWh.IsActive; wh.EventType = (WebhookEventType) newWh.EventType; } foreach (var wh in dict.Values) { repo.Webhooks.Add(new RepositoryModel.Webhook { Id = wh.Id, TargetUrl = wh.TargetUrl, Active = wh.IsActive, CreatedAt = wh.CreatedAt.UtcDateTime, EventType = (WebhookEventType) wh.EventType }); } return true; } catch (Exception e) { UIHelper.NotifyError(e); Console.WriteLine(e); return false; } } public async ValueTask AddWebhookAsync(RepositoryModel repo, string targetUrl, string secret, WebhookEventType evt) { var api = Api.C; try { if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync())) { api.ClearGateway(); return false; } await api.Gateway.Create(repo.OwnerName, repo.Name, new WebhookCreateRequest { TargetUrl = targetUrl, Secret = secret, EventType = (int) evt, }); return await UpdateWebhooksFromServerAsync(repo); } catch (Exception e) { UIHelper.NotifyError(e); Console.WriteLine(e); return false; } } public async ValueTask DeleteWebhookAsync(RepositoryModel repo, int webhookId) { var api = Api.C; try { if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync())) { api.ClearGateway(); return false; } await api.Gateway.WebhooksDelete(repo.OwnerName, repo.Name, webhookId); return await UpdateWebhooksFromServerAsync(repo); } catch (Exception e) { UIHelper.NotifyError(e); Console.WriteLine(e); return false; } } public async ValueTask ToggleWebhookAsync(RepositoryModel repo, int webhookId, bool activate) { var api = Api.C; try { if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync())) { api.ClearGateway(); return false; } await api.Gateway.Toggle(repo.OwnerName, repo.Name, webhookId, activate); return await UpdateWebhooksFromServerAsync(repo); } catch (Exception e) { UIHelper.NotifyError(e); Console.WriteLine(e); return false; } } }