600 lines
25 KiB
C#
600 lines
25 KiB
C#
using System.Text.Json;
|
|
using Flawless.Communication.Request;
|
|
using Flawless.Communication.Response;
|
|
using Flawless.Communication.Shared;
|
|
using Flawless.Core.BinaryDataFormat;
|
|
using Flawless.Core.Modal;
|
|
using Flawless.Server.Models;
|
|
using Flawless.Server.Services;
|
|
using Flawless.Server.Utility;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Flawless.Server.Controllers;
|
|
|
|
[ApiController, Microsoft.AspNetCore.Authorization.Authorize, Route("api/repo/{userName}/{repositoryName}")]
|
|
public class RepositoryInnieController(
|
|
UserManager<AppUser> userManager,
|
|
AppDbContext dbContext,
|
|
PathTransformer transformer) : ControllerBase
|
|
{
|
|
|
|
public class FormCommitRequest : CommitRequest
|
|
{
|
|
public IFormFile? Depot { get; set; }
|
|
}
|
|
|
|
|
|
#region Unresoted
|
|
|
|
[HttpPost("delete_repo")]
|
|
public async Task<IActionResult> DeleteRepositoryAsync(string repositoryName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3)
|
|
return BadRequest(new FailedResponse("Repository name is empty or too short!"));
|
|
|
|
var u = (await userManager.GetUserAsync(HttpContext.User))!;
|
|
var rp = await dbContext.Repositories.FirstOrDefaultAsync(rp => rp.Name == repositoryName && u == rp.Owner);
|
|
if (rp == null) return BadRequest(new FailedResponse("Repository not found!"));
|
|
|
|
dbContext.Repositories.Remove(rp);
|
|
await dbContext.SaveChangesAsync();
|
|
return Ok();
|
|
}
|
|
|
|
[HttpGet("get_info")]
|
|
public async Task<ActionResult<RepositoryInfoResponse>> IsRepositoryArchiveAsync(string repositoryName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3)
|
|
return BadRequest(new FailedResponse("Repository name is empty or too short!"));
|
|
|
|
var u = (await userManager.GetUserAsync(HttpContext.User))!;
|
|
var rp = await dbContext.Repositories
|
|
.Include(repository => repository.Owner)
|
|
.Include(repository => repository.Commits)
|
|
.Include(repository => repository.Members)
|
|
.ThenInclude(repositoryMember => repositoryMember.User)
|
|
.FirstOrDefaultAsync(rp => rp.Name == repositoryName && u == rp.Owner);
|
|
|
|
if (rp == null) return BadRequest(new FailedResponse("Repository not found!"));
|
|
|
|
return Ok(new RepositoryInfoResponse
|
|
{
|
|
RepositoryName = rp.Name,
|
|
OwnerUsername = rp.Owner.UserName!,
|
|
Description = rp.Description,
|
|
IsArchived = rp.IsArchived,
|
|
Role = rp.Members.First(m => m.User == u).Role
|
|
});
|
|
}
|
|
|
|
[HttpPost("archive_repo")]
|
|
public async Task<IActionResult> ArchiveRepositoryAsync(string repositoryName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3)
|
|
return BadRequest(new FailedResponse("Repository name is empty or too short!"));
|
|
|
|
var u = (await userManager.GetUserAsync(HttpContext.User))!;
|
|
var rp = await dbContext.Repositories.FirstOrDefaultAsync(rp => rp.Name == repositoryName && u == rp.Owner);
|
|
if (rp == null) return BadRequest(new FailedResponse("Repository not found!"));
|
|
if (rp.IsArchived) return BadRequest(new FailedResponse("Repository is archived!"));
|
|
|
|
rp.IsArchived = true;
|
|
await dbContext.SaveChangesAsync();
|
|
return Ok();
|
|
}
|
|
|
|
|
|
[HttpPost("unarchive_repo")]
|
|
public async Task<IActionResult> UnarchiveRepositoryAsync(string repositoryName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3)
|
|
return BadRequest(new FailedResponse("Repository name is empty or too short!"));
|
|
|
|
var u = (await userManager.GetUserAsync(HttpContext.User))!;
|
|
var rp = await dbContext.Repositories.FirstOrDefaultAsync(rp => rp.Name == repositoryName && u == rp.Owner);
|
|
if (rp == null) return BadRequest(new FailedResponse("Repository not found!"));
|
|
if (!rp.IsArchived) return BadRequest(new FailedResponse("Repository is not archived!"));
|
|
|
|
rp.IsArchived = false;
|
|
await dbContext.SaveChangesAsync();
|
|
return Ok();
|
|
}
|
|
|
|
[HttpGet("get_users")]
|
|
public async Task<ActionResult<ListingResponse<RepoUserRole>>> GetUsersAsync(string repositoryName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3)
|
|
return BadRequest(new FailedResponse("Repository name is empty or too short!"));
|
|
|
|
var u = (await userManager.GetUserAsync(HttpContext.User))!;
|
|
var rp = await dbContext.Repositories
|
|
.Include(repository => repository.Owner)
|
|
.Include(repository => repository.Members)
|
|
.ThenInclude(repositoryMember => repositoryMember.User)
|
|
.FirstOrDefaultAsync(rp => rp.Name == repositoryName && u == rp.Owner);
|
|
|
|
if (rp == null) return BadRequest(new FailedResponse("Repository not found!"));
|
|
|
|
return Ok(new ListingResponse<RepoUserRole>(rp.Members.Select(pm => new RepoUserRole
|
|
{
|
|
Username = pm.User.UserName!,
|
|
Role = pm.Role
|
|
}).ToArray()));
|
|
}
|
|
|
|
[HttpPost("update_user")]
|
|
public async Task<IActionResult> UpdateUserAsync(string repositoryName, [FromBody] RepoUserRole r)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3)
|
|
return BadRequest(new FailedResponse("Repository name is empty or too short!"));
|
|
|
|
var u = (await userManager.GetUserAsync(HttpContext.User))!;
|
|
var tu = await userManager.FindByNameAsync(r.Username);
|
|
if (tu == null) return BadRequest(new FailedResponse("User not found!"));
|
|
if (u == tu) return BadRequest(new FailedResponse("Not able to update the role on self-own repository!"));
|
|
|
|
var rp = await dbContext.Repositories
|
|
.Include(repository => repository.Members)
|
|
.ThenInclude(repositoryMember => repositoryMember.User)
|
|
.FirstOrDefaultAsync(rp => rp.Name == repositoryName && u == rp.Owner);
|
|
|
|
if (rp == null) return BadRequest(new FailedResponse("Repository not found!"));
|
|
|
|
var m = rp.Members.FirstOrDefault(m => m.User == tu);
|
|
if (m == null)
|
|
{
|
|
m = new RepositoryMember
|
|
{
|
|
User = tu,
|
|
Role = r.Role ?? RepositoryRole.Guest
|
|
};
|
|
|
|
rp.Members.Add(m);
|
|
}
|
|
else
|
|
{
|
|
m.Role = r.Role ?? RepositoryRole.Guest;
|
|
}
|
|
|
|
await dbContext.SaveChangesAsync();
|
|
return Ok();
|
|
}
|
|
|
|
[HttpPost("delete_user")]
|
|
public async Task<IActionResult> DeleteUserAsync(string repositoryName, [FromBody] RepoUserRole r)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3)
|
|
return BadRequest(new FailedResponse("Repository name is empty or too short!"));
|
|
|
|
var u = (await userManager.GetUserAsync(HttpContext.User))!;
|
|
var tu = await userManager.FindByNameAsync(r.Username);
|
|
if (tu == null) return BadRequest(new FailedResponse("User not found!"));
|
|
if (u == tu) return BadRequest(new FailedResponse("Not able to update the role on self-own repository!"));
|
|
|
|
var rp = await dbContext.Repositories
|
|
.Include(repository => repository.Members)
|
|
.ThenInclude(repositoryMember => repositoryMember.User)
|
|
.FirstOrDefaultAsync(rp => rp.Name == repositoryName && u == rp.Owner);
|
|
|
|
if (rp == null) return BadRequest(new FailedResponse("Repository not found!"));
|
|
|
|
var m = rp.Members.FirstOrDefault(m => m.User == tu);
|
|
if (m == null) return BadRequest(new FailedResponse("User is not being granted to this repository!"));
|
|
|
|
rp.Members.Remove(m);
|
|
await dbContext.SaveChangesAsync();
|
|
return Ok();
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
private bool UserNotGranted(out IActionResult? rsp, AppUser user, Repository repo, RepositoryRole minRole)
|
|
{
|
|
if (repo.Owner == user || repo.Members.Any(m => m.User == user && m.Role >= minRole))
|
|
{
|
|
rsp = null;
|
|
return false;
|
|
}
|
|
|
|
rsp = Unauthorized(new FailedResponse("You are not allowed to do this operation"));
|
|
return true;
|
|
}
|
|
|
|
private async ValueTask<object> ValidateRepositoryAsync(
|
|
string userName, string repositoryName, AppUser user, RepositoryRole role)
|
|
{
|
|
var rp = await dbContext.Repositories
|
|
.Include(r => r.Members)
|
|
.ThenInclude(m => m.User)
|
|
.Include(r => r.Owner)
|
|
.FirstOrDefaultAsync(r => r.Owner.UserName == userName && r.Name == repositoryName);
|
|
|
|
if (rp == null) return NotFound(new FailedResponse($"Could not find repository {userName}:{repositoryName}"));
|
|
if (UserNotGranted(out var rsp, user, rp, role)) return rsp!;
|
|
|
|
return rp;
|
|
}
|
|
|
|
|
|
[HttpPost("fetch_manifest")]
|
|
public async Task<ActionResult<CommitManifest>> DownloadManifestAsync(string userName, string repositoryName, [FromQuery] string commitId)
|
|
{
|
|
if (!Guid.TryParse(commitId, out var commitGuid)) return BadRequest(new FailedResponse("Invalid commit id"));
|
|
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
|
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Guest);
|
|
if (grantIssue is not Repository rp) return (ActionResult) grantIssue;
|
|
|
|
// Start checkout file.
|
|
var target = transformer.GetCommitManifestPath(rp.Id, commitGuid);
|
|
if (!System.IO.File.Exists(target)) return NotFound(new FailedResponse($"Could not find commit manifest {target}"));
|
|
|
|
await using var manifest = System.IO.File.OpenRead(target);
|
|
return await JsonSerializer.DeserializeAsync<CommitManifest>(manifest);
|
|
}
|
|
|
|
|
|
[HttpPost("fetch_depot")]
|
|
public async Task<ActionResult<Stream>> DownloadDepotAsync(string userName, string repositoryName, [FromQuery] string depotId)
|
|
{
|
|
if (!Guid.TryParse(depotId, out var depotGuid)) return BadRequest(new FailedResponse("Invalid depot id"));
|
|
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
|
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Guest);
|
|
if (grantIssue is not Repository rp) return (ActionResult) grantIssue;
|
|
|
|
// Start checkout file.
|
|
var target = transformer.GetDepotPath(rp.Id, depotGuid);
|
|
if (System.IO.File.Exists(target)) return File(System.IO.File.OpenRead(target), "application/octet-stream");
|
|
return NotFound(new FailedResponse($"Could not find depot {target}"));
|
|
|
|
}
|
|
|
|
[HttpPost("list_commit")]
|
|
public async Task<ActionResult<ListingResponse<RepositoryCommitResponse>>> ListCommitsAsync(string userName, string repositoryName)
|
|
{
|
|
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
|
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Guest);
|
|
if (grantIssue is not Repository) return (ActionResult) grantIssue;
|
|
|
|
var commit = dbContext.Repositories
|
|
.Where(r => r.Owner.UserName == userName && r.Name == repositoryName)
|
|
.SelectMany(r => r.Commits)
|
|
.Include(r => r.Author).Include(repositoryCommit => repositoryCommit.MainDepot)
|
|
.OrderByDescending(r => r.CommittedOn)
|
|
.AsAsyncEnumerable();
|
|
|
|
List<RepositoryCommitResponse> r = new();
|
|
await foreach (var cm in commit)
|
|
r.Add(new RepositoryCommitResponse(
|
|
cm.Id, cm.Author.UserName!, cm.CommittedOn, cm.Message, cm.MainDepot.DepotId));
|
|
|
|
return Ok(new ListingResponse<RepositoryCommitResponse>(r.ToArray()));
|
|
}
|
|
|
|
[HttpPost("list_locked_files")]
|
|
public async Task<ActionResult<ListingResponse<LockFileInfo>>> ListLocksAsync(string userName, string repositoryName)
|
|
{
|
|
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
|
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Developer);
|
|
if (grantIssue is not Repository) return (ActionResult) grantIssue;
|
|
|
|
var lockers = await dbContext.Repositories
|
|
.Where(r => r.Owner.UserName == userName && r.Name == repositoryName)
|
|
.SelectMany(r => r.Locked)
|
|
.Select(l => new LockFileInfo(l.Path, l.LockDownUser.UserName))
|
|
.ToArrayAsync();
|
|
|
|
return Ok(new ListingResponse<LockFileInfo>(lockers));
|
|
}
|
|
|
|
[HttpGet("peek_commit")]
|
|
public async Task<ActionResult<PeekResponse<Guid>>> PeekCommitAsync(string userName, string repositoryName)
|
|
{
|
|
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
|
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Guest);
|
|
if (grantIssue is not Repository rp) return (ActionResult) grantIssue;
|
|
|
|
var commit = await dbContext.Repositories
|
|
.Where(r => r.Owner.UserName == userName && r.Name == repositoryName)
|
|
.SelectMany(r => r.Commits)
|
|
.Include(r => r.Author)
|
|
.OrderByDescending(r => r.CommittedOn)
|
|
.FirstOrDefaultAsync();
|
|
|
|
return Ok(new PeekResponse<Guid>(commit?.Id ?? Guid.Empty));
|
|
}
|
|
|
|
|
|
[HttpPost("lock_file")]
|
|
public async Task<IActionResult> LockAsync(string userName, string repositoryName, string path)
|
|
{
|
|
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
|
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Developer);
|
|
if (grantIssue is not Repository) return (IActionResult) grantIssue;
|
|
|
|
var lockers = await dbContext.Repositories
|
|
.Include(repository => repository.Locked)
|
|
.FirstAsync(r => r.Owner.UserName == userName && r.Name == repositoryName);
|
|
|
|
try
|
|
{
|
|
switch (RepoLocktableUtility.AddFileLockOwner(lockers.Locked, path, user))
|
|
{
|
|
case RepoLocktableOptResult.Expand:
|
|
case RepoLocktableOptResult.IsChild:
|
|
return Ok();
|
|
|
|
case RepoLocktableOptResult.Success:
|
|
return Created();
|
|
|
|
case RepoLocktableOptResult.Rejected:
|
|
return Conflict("It was locked by others");
|
|
|
|
case RepoLocktableOptResult.Already:
|
|
return Accepted("Already been locked by you");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
await dbContext.SaveChangesAsync();
|
|
}
|
|
|
|
return BadRequest("Unknown error");
|
|
}
|
|
|
|
[HttpPost("unlock_file")]
|
|
public async Task<IActionResult> UnockAsync(string userName, string repositoryName, string path)
|
|
{
|
|
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
|
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Developer);
|
|
if (grantIssue is not Repository) return (IActionResult) grantIssue;
|
|
|
|
var lockers = await dbContext.Repositories
|
|
.Include(repository => repository.Locked)
|
|
.FirstAsync(r => r.Owner.UserName == userName && r.Name == repositoryName);
|
|
|
|
try
|
|
{
|
|
switch (RepoLocktableUtility.RemoveFileLockOwner(lockers.Locked, path, user))
|
|
{
|
|
case RepoLocktableOptResult.Expand:
|
|
case RepoLocktableOptResult.IsChild:
|
|
return Ok();
|
|
|
|
case RepoLocktableOptResult.Success:
|
|
return Created();
|
|
|
|
case RepoLocktableOptResult.Rejected:
|
|
return Conflict("It was locked by others");
|
|
|
|
case RepoLocktableOptResult.Already:
|
|
return Accepted("Already been unlocked");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
await dbContext.SaveChangesAsync();
|
|
}
|
|
|
|
return BadRequest("Unknown error");
|
|
}
|
|
|
|
[HttpPost("create_commit")]
|
|
[RequestSizeLimit(long.MaxValue)]
|
|
[ProducesResponseType<CommitSuccessResponse>(200)]
|
|
public async Task<IActionResult> CommitAsync(string userName, string repositoryName, [FromForm] FormCommitRequest req)
|
|
{
|
|
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
|
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Developer);
|
|
if (grantIssue is not Repository rp) return (IActionResult) grantIssue;
|
|
|
|
// Appoint or upload a commit - two in one choice.
|
|
if (req.Depot != null ^ req.MainDepotId != null) return await CommitInternalAsync(rp, user, req);
|
|
|
|
// Not valid response
|
|
if (req.Depot != null) return BadRequest(new FailedResponse("You can not choose a existed depot while upload a new depot!"));
|
|
return BadRequest(new FailedResponse("You must choose a existed depot upload a new depot as baseline!"));
|
|
}
|
|
|
|
private async Task<IActionResult> CommitInternalAsync(Repository rp, AppUser user, FormCommitRequest req)
|
|
{
|
|
var actualFs = req.WorkspaceSnapshot.Select(s =>
|
|
{
|
|
var idx = s.IndexOf('$');
|
|
if (idx < 0) throw new InvalidDataException($"WorkPath '{s}' is invalid!");
|
|
var dateTimeStr = s.Substring(0, idx);
|
|
var pathStr = s.Substring(idx + 1);
|
|
|
|
return new WorkspaceFile(DateTime.FromBinary(long.Parse(dateTimeStr)), pathStr);
|
|
}).ToArray();
|
|
|
|
var test = new HashSet<WorkspaceFile>(actualFs);
|
|
var createNewDepot = false;
|
|
RepositoryDepot mainDepot;
|
|
List<DepotLabel> depotLabels = new();
|
|
|
|
// Judge if receive new depot or use old one...
|
|
if (req.Depot == null)
|
|
{
|
|
// Use existed depots
|
|
var mainDepotId = Guid.Parse(req.MainDepotId!);
|
|
var preDepot = await StencilWorkspaceSnapshotViaDatabaseAsync(rp.Id, mainDepotId, test);
|
|
|
|
if (preDepot == null)
|
|
return BadRequest(new FailedResponse("Not a valid main depot!", "NoMainDepot"));
|
|
|
|
// If still not able to resolve
|
|
if (test.Count > 0)
|
|
return BadRequest(new FailedResponse(test, "DependencyUnsolved"));
|
|
|
|
// Set this depot label...
|
|
mainDepot = preDepot;
|
|
depotLabels.Add(new DepotLabel(mainDepot.DepotId, mainDepot.Length));
|
|
depotLabels.AddRange(mainDepot.Dependencies.Select(d => new DepotLabel(d.DepotId, d.Length)));
|
|
}
|
|
else
|
|
{
|
|
// Get a new depot from request - this will flat to direct depot reference to avoid deep search that
|
|
// raise memory issue.
|
|
var actualRequiredDepots = new HashSet<Guid>();
|
|
|
|
// Read and create a new depot
|
|
using var cacheStream = new MemoryStream(new byte[req.Depot.Length], true);
|
|
await req.Depot.CopyToAsync(cacheStream, HttpContext.RequestAborted);
|
|
|
|
// Look if main depot can do this...
|
|
await StencilWorkspaceSnapshotAsync(cacheStream, test);
|
|
|
|
// Oh no, will we need to load their parents?
|
|
var unresolvedCount = test.Count;
|
|
if (unresolvedCount != 0)
|
|
{
|
|
// Yes
|
|
foreach (var subDepot in req.RequiredDepots?.Select(Guid.Parse) ?? [])
|
|
{
|
|
await StencilWorkspaceSnapshotViaDatabaseAsync(rp.Id, subDepot, test);
|
|
|
|
var rest = test.Count;
|
|
if (rest == 0)
|
|
{
|
|
actualRequiredDepots.Add(subDepot);
|
|
break;
|
|
}
|
|
|
|
// If test is changed?
|
|
if (unresolvedCount != rest)
|
|
{
|
|
actualRequiredDepots.Add(subDepot);
|
|
unresolvedCount = rest;
|
|
}
|
|
}
|
|
|
|
// If still not able to resolve
|
|
if (unresolvedCount > 0)
|
|
return BadRequest(new
|
|
{
|
|
Reason = "Unable to resolve workspace snapshot! May lost some essential dependency.",
|
|
Unresolved = test
|
|
});
|
|
}
|
|
|
|
// Great, a valid depot, so create it in db.
|
|
mainDepot = new RepositoryDepot { Length = req.Depot.Length };
|
|
mainDepot.Dependencies.AddRange(
|
|
await dbContext.Repositories
|
|
.Where(r => r.Id == rp.Id)
|
|
.SelectMany(r => r.Depots)
|
|
.Where(cm => actualRequiredDepots.Contains(cm.DepotId))
|
|
.ToArrayAsync());
|
|
|
|
// Create commit and let it alloc a main key
|
|
rp.Depots.Add(mainDepot);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
// Then write depot into disk
|
|
var depotPath = transformer.GetDepotPath(rp.Id, mainDepot.DepotId);
|
|
Directory.CreateDirectory(Path.GetDirectoryName(depotPath)!);
|
|
await using (var depotStream = System.IO.File.Create(depotPath))
|
|
{
|
|
cacheStream.Seek(0, SeekOrigin.Begin);
|
|
await cacheStream.CopyToAsync(depotStream, HttpContext.RequestAborted);
|
|
}
|
|
|
|
// Everything alright, so make response
|
|
depotLabels.Add(new DepotLabel(mainDepot.DepotId, mainDepot.Length));
|
|
depotLabels.AddRange(mainDepot.Dependencies.Select(d => new DepotLabel(d.DepotId, d.Length)));
|
|
|
|
}
|
|
|
|
// Create commit info
|
|
var commit = new RepositoryCommit
|
|
{
|
|
Author = user,
|
|
CommittedOn = DateTime.UtcNow,
|
|
MainDepot = mainDepot,
|
|
Message = req.Message,
|
|
};
|
|
|
|
try
|
|
{
|
|
// Write changes into db.
|
|
rp.Commits.Add(commit);
|
|
await dbContext.SaveChangesAsync();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// Revert depot create operation
|
|
System.IO.File.Delete(transformer.GetDepotPath(rp.Id, mainDepot.DepotId));
|
|
throw;
|
|
}
|
|
|
|
// Create manifest file and write to disk
|
|
var manifestPath = transformer.GetCommitManifestPath(rp.Id, commit.Id);
|
|
var manifest = new CommitManifest
|
|
{
|
|
ManifestId = commit.Id,
|
|
Depot = depotLabels.ToArray(),
|
|
FilePaths = actualFs
|
|
};
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(manifestPath)!);
|
|
await using (var manifestStream = System.IO.File.Create(manifestPath))
|
|
await JsonSerializer.SerializeAsync(manifestStream, manifest);
|
|
|
|
return Ok(new CommitSuccessResponse(commit.CommittedOn, commit.Id, mainDepot.DepotId));
|
|
}
|
|
|
|
private static async ValueTask StencilWorkspaceSnapshotAsync(Stream depotStream, HashSet<WorkspaceFile> unresolved)
|
|
{
|
|
// Get version
|
|
depotStream.Seek(0, SeekOrigin.Begin); // Fix: Get an invalid version code due to offset is bad.
|
|
var version = DataTransformer.GuessStandardDepotHeaderVersion(depotStream);
|
|
if (version != 1) throw new InvalidDataException($"Unable to get depot header version, feedback is {version}.");
|
|
|
|
// Get header
|
|
depotStream.Seek(0, SeekOrigin.Begin);
|
|
var header = DataTransformer.ExtractStandardDepotHeaderV1(depotStream);
|
|
|
|
// Start validation
|
|
depotStream.Seek(0, SeekOrigin.Begin);
|
|
await foreach (var inf in DataTransformer.ExtractDepotFileInfoMapAsync(depotStream, header.FileMapSize))
|
|
{
|
|
var test = new WorkspaceFile
|
|
{
|
|
ModifyTime = inf.ModifyTime,
|
|
WorkPath = inf.Path,
|
|
};
|
|
|
|
unresolved.Remove(test);
|
|
if (unresolved.Count == 0) break; // Early quit to avoid extra performance loss.
|
|
}
|
|
}
|
|
|
|
private async Task<RepositoryDepot?> StencilWorkspaceSnapshotViaDatabaseAsync(Guid rpId, Guid depotId, HashSet<WorkspaceFile> unresolved)
|
|
{
|
|
var depot = await dbContext.Repositories
|
|
.SelectMany(r => r.Depots)
|
|
.Include(r => r.Dependencies)
|
|
.ThenInclude(repositoryDepot => repositoryDepot.Dependencies)
|
|
.FirstOrDefaultAsync(r => r.DepotId == depotId);
|
|
|
|
await RecurringAsync(transformer, rpId, depot, unresolved);
|
|
return depot;
|
|
|
|
static async ValueTask RecurringAsync(PathTransformer transformer, Guid rpId, RepositoryDepot? depot, HashSet<WorkspaceFile> unresolved)
|
|
{
|
|
if (depot == null) return;
|
|
var path = transformer.GetDepotPath(rpId, depot.DepotId);
|
|
|
|
// require do not extend lifetime scope.
|
|
await using (var fs = new BufferedStream(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)))
|
|
await StencilWorkspaceSnapshotAsync(fs, unresolved);
|
|
|
|
for (var i = 0; i < depot.Dependencies.Count && unresolved.Count > 0; i++)
|
|
await RecurringAsync(transformer, rpId, depot.Dependencies[i], unresolved);
|
|
|
|
}
|
|
}
|
|
} |