diff --git a/Flawless-Version-Control.sln.DotSettings.user b/Flawless-Version-Control.sln.DotSettings.user index a3f7ca9..7c73cf0 100644 --- a/Flawless-Version-Control.sln.DotSettings.user +++ b/Flawless-Version-Control.sln.DotSettings.user @@ -22,13 +22,13 @@ ForceIncluded ForceIncluded ForceIncluded - <SessionState ContinuousTestingMode="0" IsActive="True" IsLocked="True" Name="PathValidationTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <TestAncestor> - <TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit</TestId> - </TestAncestor> + <SessionState ContinuousTestingMode="0" IsActive="True" IsLocked="True" Name="PathValidationTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit</TestId> + </TestAncestor> </SessionState> - <SessionState ContinuousTestingMode="0" Name="PathValidationTest #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <TestAncestor> - <TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit.PathValidationTest</TestId> - </TestAncestor> + <SessionState ContinuousTestingMode="0" Name="PathValidationTest #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit.PathValidationTest</TestId> + </TestAncestor> </SessionState> \ No newline at end of file diff --git a/Flawless.Abstract/WorkPath.cs b/Flawless.Abstract/WorkPath.cs index 5f91e0d..786b54d 100644 --- a/Flawless.Abstract/WorkPath.cs +++ b/Flawless.Abstract/WorkPath.cs @@ -402,6 +402,16 @@ public static class WorkPath return null; } + + /// + /// Get a relative work path from another work path in case first one is a sub path of second one. + /// The parent one. + /// The child one. + /// Same or is child of relatedTo will return true. + public static bool IsPathRelated(string relatedTo, string workPath) + { + return workPath.StartsWith(EndsInDirectorySeparator(relatedTo) ? relatedTo[..^1] : relatedTo); + } private static void CombineInternal(StringBuilder sb, string addon) { diff --git a/Flawless.Server/Controllers/RepositoryControl.cs b/Flawless.Server/Controllers/RepositoryControl.cs index 4fbc99c..dce3c88 100644 --- a/Flawless.Server/Controllers/RepositoryControl.cs +++ b/Flawless.Server/Controllers/RepositoryControl.cs @@ -106,8 +106,26 @@ public class RepositoryControl( Commits = commit }); } - + + [HttpGet("list/locked")] + public async Task 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 (IActionResult) grantIssue; + var lockers = await dbContext.Repositories + .Where(r => r.Owner.UserName == userName && r.Name == repositoryName) + .SelectMany(r => r.Locked) + .Select(l => new { Path = l.Path, Owner = l.LockDownUser.UserName}) + .ToArrayAsync(); + + return Ok(new + { + Lockers = lockers + }); + } + [HttpGet("peek/commit")] public async Task PeekCommitAsync(string userName, string repositoryName) { @@ -128,11 +146,86 @@ public class RepositoryControl( }); } + + [HttpGet("lock")] + public async Task 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"); + } + + [HttpGet("unlock")] + public async Task 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("commit")] public async Task CommitAsync(string userName, string repositoryName, [FromForm] FormCommitRequest req) { var user = (await userManager.GetUserAsync(HttpContext.User))!; - var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Guest); + var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Developer); if (grantIssue is not Repository rp) return (IActionResult) grantIssue; if (req.Depot != null ^ req.MainDepotId != null) return await CommitInternalAsync(rp, user, req); diff --git a/Flawless.Server/Models/Repository.cs b/Flawless.Server/Models/Repository.cs index 8c2c90a..0fa0d4c 100644 --- a/Flawless.Server/Models/Repository.cs +++ b/Flawless.Server/Models/Repository.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Flawless.Server.Controllers; namespace Flawless.Server.Models; @@ -24,4 +25,6 @@ public class Repository public List Commits { get; set; } = new(); public List Depots { get; set; } = new(); + + public List Locked { get; set; } = new(); } \ No newline at end of file diff --git a/Flawless.Server/Models/RepositoryLockedFile.cs b/Flawless.Server/Models/RepositoryLockedFile.cs new file mode 100644 index 0000000..a3bf69e --- /dev/null +++ b/Flawless.Server/Models/RepositoryLockedFile.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace Flawless.Server.Models; + +public class RepositoryLockedFile +{ + [Key] + public required string Path { get; set; } + + public required AppUser LockDownUser { get; set; } +} \ No newline at end of file diff --git a/Flawless.Server/Services/AppDbContext.cs b/Flawless.Server/Services/AppDbContext.cs index 1f0018c..c940d01 100644 --- a/Flawless.Server/Services/AppDbContext.cs +++ b/Flawless.Server/Services/AppDbContext.cs @@ -1,4 +1,5 @@ -using Flawless.Server.Models; +using Flawless.Communication.Shared; +using Flawless.Server.Models; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; @@ -11,4 +12,17 @@ public class AppDbContext(DbContextOptions options) public DbSet RefreshTokens { get; set; } public DbSet Repositories { get; set; } + + public async ValueTask<(bool existed, bool authorized)> CheckRepositoryExistedAuthorizedAsync( + AppUser owner, string name, AppUser user, RepositoryRole role) + { + var r = await Repositories + .FirstOrDefaultAsync(r => r.Name == name && r.Owner == owner); + + var existed = r != null; + var authorized = existed && (r!.Owner == owner || r.Members.Any(m => m.User == owner && m.Role >= role)); + + return (existed, authorized); + } + } \ No newline at end of file diff --git a/Flawless.Server/Utility/RepoLocktableUtility.cs b/Flawless.Server/Utility/RepoLocktableUtility.cs new file mode 100644 index 0000000..51f681a --- /dev/null +++ b/Flawless.Server/Utility/RepoLocktableUtility.cs @@ -0,0 +1,66 @@ +using Flawless.Abstraction; +using Flawless.Server.Models; + +namespace Flawless.Server.Utility; + +public enum RepoLocktableOptResult +{ + Expand, + Success, + Already, + IsChild, + Rejected, +} + +public static class RepoLocktableUtility +{ + public static RepositoryLockedFile? GetFileLockByPath(List table, string testPath) + { + return table.FirstOrDefault(f => WorkPath.IsPathRelated(f.Path, testPath)); + } + + public static RepoLocktableOptResult AddFileLockOwner(List table, string testPath, AppUser owner) + { + var expanded = false; + foreach (var entry in table) + { + if (!WorkPath.IsPathRelated(entry.Path, testPath)) continue; + + // Check is locked by others + if (entry.LockDownUser != owner) return RepoLocktableOptResult.Rejected; + return testPath == entry.Path ? RepoLocktableOptResult.Already : RepoLocktableOptResult.IsChild; + } + + // Check if includes can be locked + for (var i = 0; i < table.Count; i++) + { + var entry = table[i]; + if (!WorkPath.IsPathRelated(testPath, entry.Path)) continue; + + if (entry.LockDownUser != owner) return RepoLocktableOptResult.Rejected; + table.RemoveAt(i); + i -= 1; + expanded = true; + } + + table.Add(new RepositoryLockedFile { Path = testPath, LockDownUser = owner }); + return expanded ? RepoLocktableOptResult.Expand : RepoLocktableOptResult.Success; + } + + public static RepoLocktableOptResult RemoveFileLockOwner(List table, string testPath, AppUser? owner) + { + for (var i = 0; i < table.Count; i++) + { + var entry = table[i]; + if (!WorkPath.IsPathRelated(entry.Path, testPath)) continue; + + if (owner != null && entry.LockDownUser != owner) return RepoLocktableOptResult.Rejected; + if (testPath != entry.Path) return RepoLocktableOptResult.IsChild; + + table.Remove(entry); + return RepoLocktableOptResult.Success; + } + + return RepoLocktableOptResult.Rejected; + } +} \ No newline at end of file