mirror of
https://github.com/Smaug123/gitea-repo-config
synced 2025-10-05 15:38:41 +00:00
Initial commit
This commit is contained in:
12
.config/dotnet-tools.json
Normal file
12
.config/dotnet-tools.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"fantomas": {
|
||||||
|
"version": "5.2.0-alpha-008",
|
||||||
|
"commands": [
|
||||||
|
"fantomas"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
.editorconfig
Normal file
41
.editorconfig
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
root=true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset=utf-8
|
||||||
|
end_of_line=crlf
|
||||||
|
trim_trailing_whitespace=true
|
||||||
|
insert_final_newline=true
|
||||||
|
indent_style=space
|
||||||
|
indent_size=4
|
||||||
|
|
||||||
|
# ReSharper properties
|
||||||
|
resharper_xml_indent_size=2
|
||||||
|
resharper_xml_max_line_length=100
|
||||||
|
resharper_xml_tab_width=2
|
||||||
|
|
||||||
|
[*.{csproj,fsproj,sqlproj,targets,props,ts,tsx,css,json}]
|
||||||
|
indent_style=space
|
||||||
|
indent_size=2
|
||||||
|
|
||||||
|
[*.{fs,fsi}]
|
||||||
|
fsharp_bar_before_discriminated_union_declaration=true
|
||||||
|
fsharp_space_before_uppercase_invocation=true
|
||||||
|
fsharp_space_before_class_constructor=true
|
||||||
|
fsharp_space_before_member=true
|
||||||
|
fsharp_space_before_colon=true
|
||||||
|
fsharp_space_before_semicolon=true
|
||||||
|
fsharp_multiline_block_brackets_on_same_column=true
|
||||||
|
fsharp_newline_between_type_definition_and_members=true
|
||||||
|
fsharp_align_function_signature_to_indentation=true
|
||||||
|
fsharp_alternative_long_member_definitions=true
|
||||||
|
fsharp_multi_line_lambda_closing_newline=true
|
||||||
|
fsharp_experimental_keep_indent_in_branch=true
|
||||||
|
fsharp_max_value_binding_width=80
|
||||||
|
fsharp_max_record_width=0
|
||||||
|
max_line_length=120
|
||||||
|
end_of_line=lf
|
||||||
|
|
||||||
|
[*.{appxmanifest,build,dtd,nuspec,xaml,xamlx,xoml,xsd}]
|
||||||
|
indent_style=space
|
||||||
|
indent_size=2
|
||||||
|
tab_width=2
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* eol=auto
|
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
/packages/
|
||||||
|
riderModule.iml
|
||||||
|
/_ReSharper.Caches/
|
||||||
|
.idea/
|
||||||
|
*.user
|
||||||
|
*.DotSettings
|
||||||
|
.DS_Store
|
20
Gitea.App/Gitea.App.fsproj
Normal file
20
Gitea.App/Gitea.App.fsproj
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Program.fs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Gitea\Gitea.fsproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="7.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
183
Gitea.App/Program.fs
Normal file
183
Gitea.App/Program.fs
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
namespace Gitea
|
||||||
|
|
||||||
|
open System
|
||||||
|
open System.IO
|
||||||
|
open System.Net.Http
|
||||||
|
open Microsoft.Extensions.Logging
|
||||||
|
open Microsoft.Extensions.Logging.Console
|
||||||
|
open Microsoft.Extensions.Options
|
||||||
|
|
||||||
|
module Program =
|
||||||
|
|
||||||
|
let printUserErrors (m : Map<User, AlignmentError<UserInfo>>) =
|
||||||
|
m |> Map.iter (fun (User u) err -> printfn $"%s{u}: {err}")
|
||||||
|
|
||||||
|
let printRepoErrors (m : Map<User, Map<RepoName, AlignmentError<Repo>>>) =
|
||||||
|
m
|
||||||
|
|> Map.iter (fun (User u) errMap -> errMap |> Map.iter (fun (RepoName r) err -> printfn $"%s{u}: %s{r}: {err}"))
|
||||||
|
|
||||||
|
let rec getUserInputDefaultNo (getUserInput : unit -> string) (message : string) : bool =
|
||||||
|
Console.Write $"${message} (y/N): "
|
||||||
|
let answer = getUserInput ()
|
||||||
|
|
||||||
|
match answer with
|
||||||
|
| "y"
|
||||||
|
| "Y" -> true
|
||||||
|
| "n"
|
||||||
|
| "N"
|
||||||
|
| "" -> false
|
||||||
|
| _ -> getUserInputDefaultNo getUserInput message
|
||||||
|
|
||||||
|
let reconcileUserErrors
|
||||||
|
(log : ILogger)
|
||||||
|
(getUserInput : unit -> string)
|
||||||
|
(client : Gitea.Client)
|
||||||
|
(m : Map<User, AlignmentError<UserInfo>>)
|
||||||
|
=
|
||||||
|
let userInputLock = obj ()
|
||||||
|
|
||||||
|
m
|
||||||
|
|> Map.toSeq
|
||||||
|
|> Seq.map (fun (User user, err) ->
|
||||||
|
match err with
|
||||||
|
| AlignmentError.DoesNotExist desired ->
|
||||||
|
async {
|
||||||
|
let rand = Random ()
|
||||||
|
|
||||||
|
let pwd =
|
||||||
|
Array.init 15 (fun _ -> rand.Next (65, 65 + 25) |> byte)
|
||||||
|
|> System.Text.Encoding.ASCII.GetString
|
||||||
|
|
||||||
|
let options = Gitea.CreateUserOption ()
|
||||||
|
options.Email <- desired.Email
|
||||||
|
options.Username <- user
|
||||||
|
options.FullName <- user
|
||||||
|
|
||||||
|
options.Visibility <-
|
||||||
|
match desired.Visibility with
|
||||||
|
| None -> "public"
|
||||||
|
| Some v -> v
|
||||||
|
|
||||||
|
options.LoginName <- user
|
||||||
|
options.MustChangePassword <- Some true
|
||||||
|
options.Password <- pwd
|
||||||
|
let! _ = client.AdminCreateUser options |> Async.AwaitTask
|
||||||
|
|
||||||
|
lock
|
||||||
|
userInputLock
|
||||||
|
(fun () ->
|
||||||
|
log.LogCritical (
|
||||||
|
"Created user {User} with password {Password}, which you must now change",
|
||||||
|
user,
|
||||||
|
pwd
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return ()
|
||||||
|
}
|
||||||
|
| AlignmentError.UnexpectedlyPresent ->
|
||||||
|
async {
|
||||||
|
lock
|
||||||
|
userInputLock
|
||||||
|
(fun () ->
|
||||||
|
let answer =
|
||||||
|
getUserInputDefaultNo getUserInput $"User %s{user} unexpectedly present. Remove?"
|
||||||
|
|
||||||
|
if answer then
|
||||||
|
client.AdminDeleteUser(user).Result
|
||||||
|
else
|
||||||
|
log.LogCritical ("Refusing to delete user {User}, who is unexpectedly present.", user)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
| AlignmentError.ConfigurationDiffers (desired, actual) ->
|
||||||
|
let updates = UserInfo.Resolve desired actual
|
||||||
|
|
||||||
|
async {
|
||||||
|
lock
|
||||||
|
userInputLock
|
||||||
|
(fun () ->
|
||||||
|
let body = Gitea.EditUserOption ()
|
||||||
|
|
||||||
|
for update in updates do
|
||||||
|
match update with
|
||||||
|
| UserInfoUpdate.Admin (desired, _) -> body.Admin <- desired
|
||||||
|
| UserInfoUpdate.Email (desired, _) -> body.Email <- desired
|
||||||
|
| UserInfoUpdate.Visibility (desired, _) -> body.Visibility <- desired
|
||||||
|
| UserInfoUpdate.Website (desired, actual) ->
|
||||||
|
// Per https://github.com/go-gitea/gitea/issues/17126,
|
||||||
|
// the website parameter can't currently be edited.
|
||||||
|
// This is a bug that is unlikely to be fixed.
|
||||||
|
let actual =
|
||||||
|
match actual with
|
||||||
|
| None -> "<no website>"
|
||||||
|
| Some uri -> uri.ToString ()
|
||||||
|
|
||||||
|
log.LogCritical (
|
||||||
|
"User {User} has conflicting website, desired {DesiredWebsite}, existing {ActualWebsite}, which a bug in Gitea means can't be reconciled via the API.",
|
||||||
|
user,
|
||||||
|
desired,
|
||||||
|
actual
|
||||||
|
)
|
||||||
|
|
||||||
|
body.LoginName <- user
|
||||||
|
client.AdminEditUser(user, body).Result |> ignore
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|> Async.Parallel
|
||||||
|
|> fun a -> async.Bind (a, Array.iter id >> async.Return)
|
||||||
|
|
||||||
|
[<EntryPoint>]
|
||||||
|
let main argv =
|
||||||
|
let configFile, giteaApiToken, githubApiToken =
|
||||||
|
match argv with
|
||||||
|
| [| f ; giteaToken |] -> FileInfo f, giteaToken, None
|
||||||
|
| [| f ; giteaToken ; githubToken |] -> FileInfo f, giteaToken, Some githubToken
|
||||||
|
| _ -> failwithf $"malformed args: %+A{argv}"
|
||||||
|
|
||||||
|
let config = GiteaConfig.get configFile
|
||||||
|
|
||||||
|
let options =
|
||||||
|
let options = ConsoleLoggerOptions ()
|
||||||
|
|
||||||
|
{ new IOptionsMonitor<ConsoleLoggerOptions> with
|
||||||
|
member _.Get _ = options
|
||||||
|
member _.CurrentValue = options
|
||||||
|
|
||||||
|
member _.OnChange _ =
|
||||||
|
{ new IDisposable with
|
||||||
|
member _.Dispose () = ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use loggerProvider = new ConsoleLoggerProvider (options)
|
||||||
|
let logger = loggerProvider.CreateLogger "Gitea.App"
|
||||||
|
|
||||||
|
use client = new HttpClient ()
|
||||||
|
client.BaseAddress <- Uri Host
|
||||||
|
client.DefaultRequestHeaders.Add ("Authorization", $"token {giteaApiToken}")
|
||||||
|
|
||||||
|
let client = Gitea.Client client
|
||||||
|
|
||||||
|
task {
|
||||||
|
Console.WriteLine "Checking users..."
|
||||||
|
let! userErrors = Gitea.checkUsers config client
|
||||||
|
|
||||||
|
match userErrors with
|
||||||
|
| Ok () -> ()
|
||||||
|
| Error errors -> do! reconcileUserErrors logger Console.ReadLine client errors
|
||||||
|
|
||||||
|
Console.WriteLine "Checking repos..."
|
||||||
|
let! repoErrors = Gitea.checkRepos config client
|
||||||
|
|
||||||
|
match repoErrors with
|
||||||
|
| Ok () -> ()
|
||||||
|
| Error errors -> do! Gitea.reconcileRepoErrors logger client githubApiToken errors
|
||||||
|
|
||||||
|
match userErrors, repoErrors with
|
||||||
|
| Ok (), Ok () -> return 0
|
||||||
|
| Ok (), Error _ -> return 1
|
||||||
|
| Error _, Ok () -> return 2
|
||||||
|
| Error _, Error _ -> return 3
|
||||||
|
}
|
||||||
|
|> fun t -> t.Result
|
27
Gitea.Test/Gitea.Test.fsproj
Normal file
27
Gitea.Test/Gitea.Test.fsproj
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="TestJsonSchema.fs" />
|
||||||
|
<Content Include="GiteaConfig.json" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="FsUnit" Version="5.1.0" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
|
||||||
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
|
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||||
|
<PackageReference Include="NUnit.Analyzers" Version="3.3.0" />
|
||||||
|
<PackageReference Include="coverlet.collector" Version="3.1.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Gitea\Gitea.fsproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
34
Gitea.Test/GiteaConfig.json
Normal file
34
Gitea.Test/GiteaConfig.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"users": {
|
||||||
|
"admin": {
|
||||||
|
"isAdmin": true,
|
||||||
|
"email": "some-admin-email@example.com",
|
||||||
|
"visibility": "private"
|
||||||
|
},
|
||||||
|
"nonadmin-user": {
|
||||||
|
"isAdmin": false,
|
||||||
|
"email": "some-nonadmin-email@example.com",
|
||||||
|
"website": "https://example.com",
|
||||||
|
"visibility": "public"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"repos": {
|
||||||
|
"nonadmin-user": {
|
||||||
|
"synced-from-github-repo-1": {
|
||||||
|
"description": "A repo that is imported from GitHub",
|
||||||
|
"gitHub": "https://github.com/MyName/repo-name"
|
||||||
|
},
|
||||||
|
"synced-from-github-repo-2": {
|
||||||
|
"description": "Another repo that is imported from GitHub",
|
||||||
|
"gitHub": "https://github.com/MyName/repo-name-2"
|
||||||
|
},
|
||||||
|
"new-repo": {
|
||||||
|
"description": "A repo that's created directly on this Gitea",
|
||||||
|
"native": {
|
||||||
|
"defaultBranch": "main",
|
||||||
|
"private": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
91
Gitea.Test/TestJsonSchema.fs
Normal file
91
Gitea.Test/TestJsonSchema.fs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
namespace Gitea.Test
|
||||||
|
|
||||||
|
open System.IO
|
||||||
|
open System.Reflection
|
||||||
|
open Gitea
|
||||||
|
open NUnit.Framework
|
||||||
|
open FsUnitTyped
|
||||||
|
open Newtonsoft.Json
|
||||||
|
open Newtonsoft.Json.Schema
|
||||||
|
open Newtonsoft.Json.Schema.Generation
|
||||||
|
open Newtonsoft.Json.Serialization
|
||||||
|
|
||||||
|
[<TestFixture>]
|
||||||
|
module TestSchema =
|
||||||
|
let schemaGen = JSchemaGenerator ()
|
||||||
|
schemaGen.ContractResolver <- CamelCasePropertyNamesContractResolver ()
|
||||||
|
|
||||||
|
let rec findFileAbove (fileName : string) (di : DirectoryInfo) =
|
||||||
|
if isNull di then
|
||||||
|
failwith "hit the root without finding anything"
|
||||||
|
|
||||||
|
let candidate =
|
||||||
|
Path.Combine (di.FullName, fileName) |> FileInfo
|
||||||
|
|
||||||
|
if candidate.Exists then
|
||||||
|
candidate
|
||||||
|
else
|
||||||
|
findFileAbove fileName di.Parent
|
||||||
|
|
||||||
|
let rec findExampleFile (di : DirectoryInfo) =
|
||||||
|
if isNull di then
|
||||||
|
failwith "hit the root without finding anything"
|
||||||
|
|
||||||
|
let candidate =
|
||||||
|
Path.Combine (di.FullName, "GiteaConfig.json") |> FileInfo
|
||||||
|
|
||||||
|
if candidate.Exists then
|
||||||
|
candidate
|
||||||
|
else
|
||||||
|
findExampleFile di.Parent
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Schema is consistent`` () =
|
||||||
|
let schemaFile =
|
||||||
|
Assembly.GetExecutingAssembly().Location
|
||||||
|
|> FileInfo
|
||||||
|
|> fun fi -> fi.Directory
|
||||||
|
|> findFileAbove "Gitea/GiteaConfig.schema.json"
|
||||||
|
|
||||||
|
let existing = JSchema.Parse (File.ReadAllText schemaFile.FullName)
|
||||||
|
let derived = schemaGen.Generate typeof<SerialisedGiteaConfig>
|
||||||
|
|
||||||
|
existing.ToString () |> shouldEqual (derived.ToString ())
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Example conforms to schema`` () =
|
||||||
|
let executing =
|
||||||
|
Assembly.GetExecutingAssembly().Location
|
||||||
|
|> FileInfo
|
||||||
|
let schemaFile = findFileAbove "GiteaConfig.json" executing.Directory
|
||||||
|
|
||||||
|
let existing = JSchema.Parse (File.ReadAllText schemaFile.FullName)
|
||||||
|
|
||||||
|
let jsonFile = findExampleFile executing.Directory
|
||||||
|
let json = File.ReadAllText jsonFile.FullName
|
||||||
|
|
||||||
|
use reader = new JsonTextReader (new StringReader (json))
|
||||||
|
use validatingReader = new JSchemaValidatingReader (reader)
|
||||||
|
validatingReader.Schema <- existing
|
||||||
|
|
||||||
|
let messages = ResizeArray ()
|
||||||
|
validatingReader.ValidationEventHandler.Add (fun args -> messages.Add args.Message)
|
||||||
|
|
||||||
|
let ser = JsonSerializer ()
|
||||||
|
ser.ContractResolver <- CamelCasePropertyNamesContractResolver ()
|
||||||
|
let _config = ser.Deserialize<SerialisedGiteaConfig> validatingReader
|
||||||
|
|
||||||
|
messages |> shouldBeEmpty
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
[<Explicit "Run this to regenerate the schema file">]
|
||||||
|
let ``Update schema file`` () =
|
||||||
|
let schemaFile =
|
||||||
|
Assembly.GetExecutingAssembly().Location
|
||||||
|
|> FileInfo
|
||||||
|
|> fun fi -> fi.Directory
|
||||||
|
|> findFileAbove "Gitea/GiteaConfig.schema.json"
|
||||||
|
|
||||||
|
let schema = schemaGen.Generate typeof<SerialisedGiteaConfig>
|
||||||
|
|
||||||
|
File.WriteAllText (schemaFile.FullName, schema.ToString ())
|
28
Gitea.sln
Normal file
28
Gitea.sln
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Gitea", "Gitea\Gitea.fsproj", "{5F99DAF4-A9F0-4A76-A205-AF586C07FE40}"
|
||||||
|
EndProject
|
||||||
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Gitea.Test", "Gitea.Test\Gitea.Test.fsproj", "{1E3E6442-11C5-4366-A1E8-A38E069934F7}"
|
||||||
|
EndProject
|
||||||
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Gitea.App", "Gitea.App\Gitea.App.fsproj", "{77DA39F7-AF01-448A-B71C-3D495EE2F6F4}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{5F99DAF4-A9F0-4A76-A205-AF586C07FE40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{5F99DAF4-A9F0-4A76-A205-AF586C07FE40}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{5F99DAF4-A9F0-4A76-A205-AF586C07FE40}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{5F99DAF4-A9F0-4A76-A205-AF586C07FE40}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{1E3E6442-11C5-4366-A1E8-A38E069934F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{1E3E6442-11C5-4366-A1E8-A38E069934F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{1E3E6442-11C5-4366-A1E8-A38E069934F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{1E3E6442-11C5-4366-A1E8-A38E069934F7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{77DA39F7-AF01-448A-B71C-3D495EE2F6F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{77DA39F7-AF01-448A-B71C-3D495EE2F6F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{77DA39F7-AF01-448A-B71C-3D495EE2F6F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{77DA39F7-AF01-448A-B71C-3D495EE2F6F4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
20
Gitea/Array.fs
Normal file
20
Gitea/Array.fs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
namespace Gitea
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module internal Array =
|
||||||
|
|
||||||
|
/// f takes a page number and a count.
|
||||||
|
let getPaginated (f : int64 -> int64 -> 'a array Async) : 'a list Async =
|
||||||
|
let count = 30
|
||||||
|
|
||||||
|
let rec go (page : int) (acc : 'a array list) =
|
||||||
|
async {
|
||||||
|
let! result = f page count
|
||||||
|
|
||||||
|
if result.Length >= count then
|
||||||
|
return! go (page + 1) (result :: acc)
|
||||||
|
else
|
||||||
|
return (result :: acc) |> Seq.concat |> Seq.toList
|
||||||
|
}
|
||||||
|
|
||||||
|
go 1 []
|
6
Gitea/AssemblyInfo.fs
Normal file
6
Gitea/AssemblyInfo.fs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Gitea.AssemblyInfo
|
||||||
|
|
||||||
|
open System.Runtime.CompilerServices
|
||||||
|
|
||||||
|
[<assembly : InternalsVisibleTo("Gitea.Test")>]
|
||||||
|
do ()
|
126
Gitea/ConfigSchema.fs
Normal file
126
Gitea/ConfigSchema.fs
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
namespace Gitea
|
||||||
|
|
||||||
|
open System
|
||||||
|
open System.IO
|
||||||
|
open Newtonsoft.Json
|
||||||
|
|
||||||
|
type NativeRepo =
|
||||||
|
{
|
||||||
|
DefaultBranch : string
|
||||||
|
Private : bool option
|
||||||
|
}
|
||||||
|
|
||||||
|
static member internal OfSerialised (s : SerialisedNativeRepo) =
|
||||||
|
{
|
||||||
|
NativeRepo.DefaultBranch = s.DefaultBranch
|
||||||
|
Private = s.Private |> Option.ofNullable
|
||||||
|
}
|
||||||
|
|
||||||
|
type Repo =
|
||||||
|
{
|
||||||
|
Description : string
|
||||||
|
GitHub : Uri option
|
||||||
|
Native : NativeRepo option
|
||||||
|
}
|
||||||
|
|
||||||
|
static member Render (u : Gitea.Repository) : Repo =
|
||||||
|
{
|
||||||
|
Description = u.Description
|
||||||
|
GitHub =
|
||||||
|
if String.IsNullOrEmpty u.OriginalUrl then
|
||||||
|
None
|
||||||
|
else
|
||||||
|
Some (Uri u.OriginalUrl)
|
||||||
|
Native =
|
||||||
|
if String.IsNullOrEmpty u.OriginalUrl then
|
||||||
|
{
|
||||||
|
Private = u.Private
|
||||||
|
DefaultBranch = u.DefaultBranch
|
||||||
|
}
|
||||||
|
|> Some
|
||||||
|
else
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
static member internal OfSerialised (s : SerialisedRepo) =
|
||||||
|
{
|
||||||
|
Repo.Description = s.Description
|
||||||
|
GitHub = Option.ofObj s.GitHub
|
||||||
|
Native = s.Native |> Option.ofNullable |> Option.map NativeRepo.OfSerialised
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserInfoUpdate =
|
||||||
|
| Admin of desired : bool option * actual : bool option
|
||||||
|
| Email of desired : string * actual : string
|
||||||
|
| Website of desired : Uri * actual : Uri option
|
||||||
|
| Visibility of desired : string * actual : string option
|
||||||
|
|
||||||
|
type UserInfo =
|
||||||
|
{
|
||||||
|
IsAdmin : bool option
|
||||||
|
Email : string
|
||||||
|
Website : Uri option
|
||||||
|
Visibility : string option
|
||||||
|
}
|
||||||
|
|
||||||
|
static member Render (u : Gitea.User) : UserInfo =
|
||||||
|
{
|
||||||
|
IsAdmin = u.IsAdmin
|
||||||
|
Email = u.Email
|
||||||
|
Website =
|
||||||
|
if String.IsNullOrEmpty u.Website then
|
||||||
|
None
|
||||||
|
else
|
||||||
|
Some (Uri u.Website)
|
||||||
|
Visibility =
|
||||||
|
if String.IsNullOrEmpty u.Visibility then
|
||||||
|
None
|
||||||
|
else
|
||||||
|
Some u.Visibility
|
||||||
|
}
|
||||||
|
|
||||||
|
static member internal OfSerialised (s : SerialisedUserInfo) =
|
||||||
|
{
|
||||||
|
UserInfo.IsAdmin = s.IsAdmin |> Option.ofNullable
|
||||||
|
Email = s.Email
|
||||||
|
Website = Option.ofObj s.Website
|
||||||
|
Visibility = Option.ofObj s.Visibility
|
||||||
|
}
|
||||||
|
|
||||||
|
static member Resolve (desired : UserInfo) (actual : UserInfo) : UserInfoUpdate list =
|
||||||
|
[
|
||||||
|
if desired.IsAdmin <> actual.IsAdmin then
|
||||||
|
yield UserInfoUpdate.Admin (desired.IsAdmin, actual.IsAdmin)
|
||||||
|
if desired.Email <> actual.Email then
|
||||||
|
yield UserInfoUpdate.Email (desired.Email, actual.Email)
|
||||||
|
if desired.Website <> actual.Website then
|
||||||
|
match desired.Website with
|
||||||
|
| Some w -> yield UserInfoUpdate.Website (w, actual.Website)
|
||||||
|
| None -> ()
|
||||||
|
if desired.Visibility <> actual.Visibility then
|
||||||
|
match desired.Visibility with
|
||||||
|
| Some v -> yield UserInfoUpdate.Visibility (v, actual.Visibility)
|
||||||
|
| None -> ()
|
||||||
|
]
|
||||||
|
|
||||||
|
type GiteaConfig =
|
||||||
|
{
|
||||||
|
Users : Map<User, UserInfo>
|
||||||
|
Repos : Map<User, Map<RepoName, Repo>>
|
||||||
|
}
|
||||||
|
|
||||||
|
static member internal OfSerialised (s : SerialisedGiteaConfig) =
|
||||||
|
{
|
||||||
|
GiteaConfig.Users = s.Users |> Map.map (fun _ -> UserInfo.OfSerialised)
|
||||||
|
Repos = s.Repos |> Map.map (fun _ -> Map.map (fun _ -> Repo.OfSerialised))
|
||||||
|
}
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module GiteaConfig =
|
||||||
|
let get (file : FileInfo) : GiteaConfig =
|
||||||
|
let s =
|
||||||
|
use reader = new StreamReader (file.OpenRead ())
|
||||||
|
reader.ReadToEnd ()
|
||||||
|
|
||||||
|
JsonConvert.DeserializeObject<SerialisedGiteaConfig> s
|
||||||
|
|> GiteaConfig.OfSerialised
|
30
Gitea/Domain.fs
Normal file
30
Gitea/Domain.fs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
namespace Gitea
|
||||||
|
|
||||||
|
open System
|
||||||
|
open System.ComponentModel
|
||||||
|
|
||||||
|
[<TypeConverter(typeof<UserTypeConverter>)>]
|
||||||
|
type User =
|
||||||
|
| User of string
|
||||||
|
|
||||||
|
override this.ToString () =
|
||||||
|
match this with
|
||||||
|
| User u -> u
|
||||||
|
|
||||||
|
and UserTypeConverter () =
|
||||||
|
inherit TypeConverter ()
|
||||||
|
override _.CanConvertFrom (_, t : Type) : bool = t = typeof<string>
|
||||||
|
override _.ConvertFrom (_, _, v : obj) : obj = v |> unbox<string> |> User |> box
|
||||||
|
|
||||||
|
[<TypeConverter(typeof<RepoNameTypeConverter>)>]
|
||||||
|
type RepoName =
|
||||||
|
| RepoName of string
|
||||||
|
|
||||||
|
override this.ToString () =
|
||||||
|
match this with
|
||||||
|
| RepoName r -> r
|
||||||
|
|
||||||
|
and RepoNameTypeConverter () =
|
||||||
|
inherit TypeConverter ()
|
||||||
|
override _.CanConvertFrom (_, t : Type) : bool = t = typeof<string>
|
||||||
|
override _.ConvertFrom (_, _, v : obj) : obj = v |> unbox<string> |> RepoName |> box
|
201
Gitea/Gitea.fs
Normal file
201
Gitea/Gitea.fs
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
namespace Gitea
|
||||||
|
|
||||||
|
open System
|
||||||
|
open Microsoft.Extensions.Logging
|
||||||
|
|
||||||
|
type AlignmentError<'a> =
|
||||||
|
| UnexpectedlyPresent
|
||||||
|
| DoesNotExist of desired : 'a
|
||||||
|
| ConfigurationDiffers of desired : 'a * actual : 'a
|
||||||
|
|
||||||
|
override this.ToString () =
|
||||||
|
match this with
|
||||||
|
| UnexpectedlyPresent -> "Found on Gitea, but was not in configuration."
|
||||||
|
| DoesNotExist _ -> "Present in configuration, but absent on Gitea."
|
||||||
|
| ConfigurationDiffers (desired, actual) -> $"Differs from config. Desired: {desired}. Actual: {actual}."
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Gitea =
|
||||||
|
|
||||||
|
let checkUsers
|
||||||
|
(config : GiteaConfig)
|
||||||
|
(client : Gitea.Client)
|
||||||
|
: Async<Result<unit, Map<User, AlignmentError<UserInfo>>>>
|
||||||
|
=
|
||||||
|
async {
|
||||||
|
let desiredUsers = config.Users
|
||||||
|
|
||||||
|
let! actualUsers =
|
||||||
|
Array.getPaginated (fun page count ->
|
||||||
|
client.AdminGetAllUsers (Some page, Some count) |> Async.AwaitTask
|
||||||
|
)
|
||||||
|
|
||||||
|
let actualUsers =
|
||||||
|
actualUsers |> Seq.map (fun u -> User u.Login, UserInfo.Render u) |> Map.ofSeq
|
||||||
|
|
||||||
|
let errors =
|
||||||
|
actualUsers
|
||||||
|
|> Map.toSeq
|
||||||
|
|> Seq.choose (fun (user, actual) ->
|
||||||
|
match Map.tryFind user desiredUsers with
|
||||||
|
| None -> (user, AlignmentError.UnexpectedlyPresent) |> Some
|
||||||
|
| Some desired ->
|
||||||
|
if desired <> actual then
|
||||||
|
(user, AlignmentError.ConfigurationDiffers (desired, actual)) |> Some
|
||||||
|
else
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|> Map.ofSeq
|
||||||
|
|
||||||
|
let otherErrors =
|
||||||
|
desiredUsers
|
||||||
|
|> Map.toSeq
|
||||||
|
|> Seq.choose (fun (user, desired) ->
|
||||||
|
match Map.tryFind user actualUsers with
|
||||||
|
| None -> (user, AlignmentError.DoesNotExist desired) |> Some
|
||||||
|
| Some actual ->
|
||||||
|
if desired <> actual then
|
||||||
|
(user, AlignmentError.ConfigurationDiffers (desired, actual)) |> Some
|
||||||
|
else
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|> Map.ofSeq
|
||||||
|
|
||||||
|
let together = Map.union (fun _ x _ -> x) errors otherErrors
|
||||||
|
return if together.IsEmpty then Ok () else Error together
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: check whether mirrors are out of sync e.g. in Public/Private status
|
||||||
|
let checkRepos
|
||||||
|
(config : GiteaConfig)
|
||||||
|
(client : Gitea.Client)
|
||||||
|
: Async<Result<unit, Map<User, Map<RepoName, AlignmentError<Repo>>>>>
|
||||||
|
=
|
||||||
|
async {
|
||||||
|
let! errors =
|
||||||
|
config.Repos
|
||||||
|
|> Map.toSeq
|
||||||
|
|> Seq.map (fun (User user as u, desiredRepos) ->
|
||||||
|
async {
|
||||||
|
let! repos =
|
||||||
|
Array.getPaginated (fun page count ->
|
||||||
|
client.UserListRepos (user, Some page, Some count) |> Async.AwaitTask
|
||||||
|
)
|
||||||
|
|
||||||
|
let actualRepos =
|
||||||
|
repos |> Seq.map (fun repo -> RepoName repo.Name, Repo.Render repo) |> Map.ofSeq
|
||||||
|
|
||||||
|
let errors1 =
|
||||||
|
actualRepos
|
||||||
|
|> Map.toSeq
|
||||||
|
|> Seq.choose (fun (repo, actual) ->
|
||||||
|
match Map.tryFind repo desiredRepos with
|
||||||
|
| None -> Some (repo, AlignmentError.UnexpectedlyPresent)
|
||||||
|
| Some desired ->
|
||||||
|
if desired <> actual then
|
||||||
|
(repo, AlignmentError.ConfigurationDiffers (desired, actual)) |> Some
|
||||||
|
else
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|> Map.ofSeq
|
||||||
|
|
||||||
|
let errors2 =
|
||||||
|
desiredRepos
|
||||||
|
|> Map.toSeq
|
||||||
|
|> Seq.choose (fun (repo, desired) ->
|
||||||
|
match Map.tryFind repo actualRepos with
|
||||||
|
| None -> Some (repo, AlignmentError.DoesNotExist desired)
|
||||||
|
| Some actual ->
|
||||||
|
if desired <> actual then
|
||||||
|
(repo, AlignmentError.ConfigurationDiffers (desired, actual)) |> Some
|
||||||
|
else
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|> Map.ofSeq
|
||||||
|
|
||||||
|
return u, Map.union (fun _ v _ -> v) errors1 errors2
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|> Async.Parallel
|
||||||
|
|
||||||
|
let errors = errors |> Array.filter (fun (_, m) -> not m.IsEmpty)
|
||||||
|
|
||||||
|
return
|
||||||
|
if errors.Length = 0 then
|
||||||
|
Ok ()
|
||||||
|
else
|
||||||
|
Error (Map.ofArray errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
let reconcileRepoErrors
|
||||||
|
(logger : ILogger)
|
||||||
|
(client : Gitea.Client)
|
||||||
|
(githubApiToken : string option)
|
||||||
|
(m : Map<User, Map<RepoName, AlignmentError<Repo>>>)
|
||||||
|
: Async<unit>
|
||||||
|
=
|
||||||
|
m
|
||||||
|
|> Map.toSeq
|
||||||
|
|> Seq.collect (fun (User user, errMap) ->
|
||||||
|
errMap
|
||||||
|
|> Map.toSeq
|
||||||
|
|> Seq.map (fun (RepoName r, err) ->
|
||||||
|
match err with
|
||||||
|
| AlignmentError.DoesNotExist desired ->
|
||||||
|
async {
|
||||||
|
let! _ =
|
||||||
|
match desired.GitHub, desired.Native with
|
||||||
|
| None, Some native ->
|
||||||
|
let options = Gitea.CreateRepoOption ()
|
||||||
|
options.Description <- desired.Description
|
||||||
|
options.Name <- r
|
||||||
|
options.Private <- native.Private
|
||||||
|
options.DefaultBranch <- native.DefaultBranch
|
||||||
|
|
||||||
|
try
|
||||||
|
client.AdminCreateRepo (user, options) |> Async.AwaitTask
|
||||||
|
with e ->
|
||||||
|
raise (AggregateException ($"Error creating {user}:{r}", e))
|
||||||
|
| Some uri, None ->
|
||||||
|
let options = Gitea.MigrateRepoOptions ()
|
||||||
|
options.Description <- desired.Description
|
||||||
|
options.Mirror <- Some true
|
||||||
|
options.RepoName <- r
|
||||||
|
options.RepoOwner <- user
|
||||||
|
options.CloneAddr <- uri.ToString ()
|
||||||
|
options.Issues <- Some true
|
||||||
|
options.Labels <- Some true
|
||||||
|
options.Lfs <- Some true
|
||||||
|
options.Milestones <- Some true
|
||||||
|
options.Releases <- Some true
|
||||||
|
options.Wiki <- Some true
|
||||||
|
options.PullRequests <- Some true
|
||||||
|
// TODO - migrate private status
|
||||||
|
githubApiToken |> Option.iter (fun t -> options.AuthToken <- t)
|
||||||
|
|
||||||
|
try
|
||||||
|
client.RepoMigrate options |> Async.AwaitTask
|
||||||
|
with e ->
|
||||||
|
raise (AggregateException ($"Error migrating {user}:{r}", e))
|
||||||
|
| None, None ->
|
||||||
|
// TODO: express this in JsonSchema
|
||||||
|
failwith $"You must supply exactly one of Native or GitHub for {user}:{r}."
|
||||||
|
| Some _, Some _ ->
|
||||||
|
failwith $"Repo {user}:{r} has both Native and GitHub set; you must set exactly one."
|
||||||
|
|
||||||
|
logger.LogInformation ("Created repo {User}: {Repo}", user.ToString (), r.ToString ())
|
||||||
|
return ()
|
||||||
|
}
|
||||||
|
| err ->
|
||||||
|
async {
|
||||||
|
logger.LogInformation (
|
||||||
|
"Unable to reconcile: {User}, {Repo}: {Error}",
|
||||||
|
user.ToString (),
|
||||||
|
r.ToString (),
|
||||||
|
err
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|> Async.Parallel
|
||||||
|
|> fun a -> async.Bind (a, Array.iter id >> async.Return)
|
27
Gitea/Gitea.fsproj
Normal file
27
Gitea/Gitea.fsproj
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="AssemblyInfo.fs" />
|
||||||
|
<Compile Include="Map.fs" />
|
||||||
|
<Compile Include="GiteaClient.fs" />
|
||||||
|
<Compile Include="Domain.fs" />
|
||||||
|
<Compile Include="SerialisedConfigSchema.fs" />
|
||||||
|
<Compile Include="ConfigSchema.fs" />
|
||||||
|
<Compile Include="Array.fs" />
|
||||||
|
<Compile Include="Gitea.fs" />
|
||||||
|
<Content Include="GiteaConfig.schema.json" />
|
||||||
|
<EmbeddedResource Include="version.json" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
|
||||||
|
<PackageReference Include="SwaggerProvider" Version="1.0.1" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
11
Gitea/GiteaClient.fs
Normal file
11
Gitea/GiteaClient.fs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Gitea
|
||||||
|
|
||||||
|
open SwaggerProvider
|
||||||
|
|
||||||
|
[<AutoOpen>]
|
||||||
|
module GiteaClient =
|
||||||
|
|
||||||
|
[<Literal>]
|
||||||
|
let Host = "https://gitea.patrickstevens.co.uk/swagger.v1.json"
|
||||||
|
|
||||||
|
type Gitea = SwaggerClientProvider<Host>
|
109
Gitea/GiteaConfig.schema.json
Normal file
109
Gitea/GiteaConfig.schema.json
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
{
|
||||||
|
"definitions": {
|
||||||
|
"Nullable<SerialisedNativeRepo>": {
|
||||||
|
"description": "If this repo is to be created natively on Gitea, the information about the repo.",
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"defaultBranch": {
|
||||||
|
"description": "The default branch name for this repository, e.g. 'main'",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"private": {
|
||||||
|
"description": "Whether this repository is a Gitea private repo",
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"defaultBranch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"SerialisedRepo": {
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"description": {
|
||||||
|
"description": "The text that will accompany this repository in the Gitea UI",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"gitHub": {
|
||||||
|
"description": "If this repo is to sync from GitHub, the URI (e.g. 'https://github.com/Smaug123/nix-maui')",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "uri"
|
||||||
|
},
|
||||||
|
"native": {
|
||||||
|
"$ref": "#/definitions/Nullable<SerialisedNativeRepo>"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"description"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"SerialisedUserInfo": {
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"isAdmin": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"website": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "uri"
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"users": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/definitions/SerialisedUserInfo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"repos": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/definitions/SerialisedRepo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"users",
|
||||||
|
"repos"
|
||||||
|
]
|
||||||
|
}
|
21
Gitea/Map.fs
Normal file
21
Gitea/Map.fs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
namespace Gitea
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module internal Map =
|
||||||
|
|
||||||
|
let inline union<'k, 'v when 'k : comparison>
|
||||||
|
([<InlineIfLambda>] f : 'k -> 'v -> 'v -> 'v)
|
||||||
|
(m1 : Map<'k, 'v>)
|
||||||
|
(m2 : Map<'k, 'v>)
|
||||||
|
: Map<'k, 'v>
|
||||||
|
=
|
||||||
|
(m1, m2)
|
||||||
|
||> Map.fold (fun acc k v2 ->
|
||||||
|
acc
|
||||||
|
|> Map.change
|
||||||
|
k
|
||||||
|
(function
|
||||||
|
| None -> Some v2
|
||||||
|
| Some v1 -> Some (f k v1 v2)
|
||||||
|
)
|
||||||
|
)
|
57
Gitea/SerialisedConfigSchema.fs
Normal file
57
Gitea/SerialisedConfigSchema.fs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
namespace Gitea
|
||||||
|
|
||||||
|
open System
|
||||||
|
open System.ComponentModel
|
||||||
|
open Newtonsoft.Json
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
[<Struct>]
|
||||||
|
[<Description "Information about a repo that is to be created on Gitea without syncing from GitHub.">]
|
||||||
|
type internal SerialisedNativeRepo =
|
||||||
|
{
|
||||||
|
[<Description "The default branch name for this repository, e.g. 'main'">]
|
||||||
|
[<JsonProperty(Required = Required.Always)>]
|
||||||
|
DefaultBranch : string
|
||||||
|
[<Description "Whether this repository is a Gitea private repo">]
|
||||||
|
[<JsonProperty(Required = Required.DisallowNull)>]
|
||||||
|
Private : Nullable<bool>
|
||||||
|
}
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
[<CLIMutable>]
|
||||||
|
type internal SerialisedRepo =
|
||||||
|
{
|
||||||
|
[<JsonProperty(Required = Required.Always)>]
|
||||||
|
[<Description "The text that will accompany this repository in the Gitea UI">]
|
||||||
|
Description : string
|
||||||
|
[<Description "If this repo is to sync from GitHub, the URI (e.g. 'https://github.com/Smaug123/nix-maui')">]
|
||||||
|
[<JsonProperty(Required = Required.Default)>]
|
||||||
|
GitHub : Uri
|
||||||
|
[<Description "If this repo is to be created natively on Gitea, the information about the repo.">]
|
||||||
|
[<JsonProperty(Required = Required.Default)>]
|
||||||
|
Native : Nullable<SerialisedNativeRepo>
|
||||||
|
}
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
[<CLIMutable>]
|
||||||
|
type internal SerialisedUserInfo =
|
||||||
|
{
|
||||||
|
[<JsonProperty(Required = Required.DisallowNull)>]
|
||||||
|
IsAdmin : Nullable<bool>
|
||||||
|
[<JsonProperty(Required = Required.Always)>]
|
||||||
|
Email : string
|
||||||
|
[<JsonProperty(Required = Required.Default)>]
|
||||||
|
Website : Uri
|
||||||
|
[<JsonProperty(Required = Required.Default)>]
|
||||||
|
Visibility : string
|
||||||
|
}
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
[<CLIMutable>]
|
||||||
|
type internal SerialisedGiteaConfig =
|
||||||
|
{
|
||||||
|
[<JsonProperty(Required = Required.Always)>]
|
||||||
|
Users : Map<User, SerialisedUserInfo>
|
||||||
|
[<JsonProperty(Required = Required.Always)>]
|
||||||
|
Repos : Map<User, Map<RepoName, SerialisedRepo>>
|
||||||
|
}
|
17854
Gitea/swagger.v1.json
Normal file
17854
Gitea/swagger.v1.json
Normal file
File diff suppressed because it is too large
Load Diff
7
Gitea/version.json
Normal file
7
Gitea/version.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"version": "0.1",
|
||||||
|
"publicReleaseRefSpec": [
|
||||||
|
"^refs/heads/main$"
|
||||||
|
],
|
||||||
|
"pathFilters": null
|
||||||
|
}
|
6
hooks/pre-push
Executable file
6
hooks/pre-push
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
if ! dotnet tool run fantomas --check -r . ; then
|
||||||
|
echo "Formatting incomplete. Consider running 'dotnet tool run fantomas -r .'"
|
||||||
|
exit 1
|
||||||
|
fi
|
Reference in New Issue
Block a user