1547 lines
54 KiB
C#
1547 lines
54 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.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<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.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<bool> 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<int?> 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 = 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, 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<bool> 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<bool> 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<bool> UpdateIssueAsync(RepositoryModel repo, int 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, 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.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<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.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<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!.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<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.Soft;
|
|
var resetChanges = method is RepositoryResetMethod.Soft or RepositoryResetMethod.Hard;
|
|
var hardMode = method is RepositoryResetMethod.Hard;
|
|
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.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<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> MergeFilesAsync(RepositoryModel repo, RepositoryFileTreeAccessor repoAccessor, IEnumerable<WorkspaceFile> 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<MergeDialogView, MergeDialogViewModel>(vm, AppDefaultValues.HostId, opt);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.Error.WriteLine(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
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);
|
|
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, x.Size)).ToArray());
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
UIHelper.NotifyError(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?> CommitWorkspaceAsync
|
|
(RepositoryModel repo, IEnumerable<ChangeInfo> 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<WorkspaceFile>, List<WorkspaceFile>) CreateCommitManifestByCurrentBaselineAndChanges
|
|
(LocalFileTreeAccessor accessor, IEnumerable<ChangeInfo> changes, bool hard = false)
|
|
{
|
|
// Create a new depot file manifest.
|
|
var files = accessor.BaselineFiles.Values.ToList();
|
|
var overlayFsStatus = new List<WorkspaceFile>();
|
|
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<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;
|
|
}
|
|
|
|
public async ValueTask<bool> 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<bool> 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<bool> 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<bool> 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;
|
|
}
|
|
}
|
|
} |