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