Add branch protection (#31)

This commit is contained in:
Patrick Stevens
2023-04-18 23:21:24 +01:00
committed by GitHub
parent d6a4da547a
commit 651ca3aa41
7 changed files with 192 additions and 7 deletions

View File

@@ -0,0 +1,10 @@
namespace Gitea.Declarative
[<RequireQualifiedAccess>]
module Async =
let map f a =
async {
let! a = a
return f a
}

View File

@@ -34,6 +34,18 @@ type PushMirror =
GitHubAddress = Uri s.GitHubAddress GitHubAddress = Uri s.GitHubAddress
} }
type ProtectedBranch =
{
BranchName : string
BlockOnOutdatedBranch : bool option
}
static member OfSerialised (s : SerialisedProtectedBranch) : ProtectedBranch =
{
BranchName = s.BranchName
BlockOnOutdatedBranch = Option.ofNullable s.BlockOnOutdatedBranch
}
type NativeRepo = type NativeRepo =
{ {
DefaultBranch : string DefaultBranch : string
@@ -51,6 +63,7 @@ type NativeRepo =
AllowRebaseExplicit : bool option AllowRebaseExplicit : bool option
AllowMergeCommits : bool option AllowMergeCommits : bool option
Mirror : PushMirror option Mirror : PushMirror option
ProtectedBranches : ProtectedBranch Set
} }
static member Default : NativeRepo = static member Default : NativeRepo =
@@ -70,6 +83,7 @@ type NativeRepo =
AllowRebaseExplicit = Some false AllowRebaseExplicit = Some false
AllowMergeCommits = Some false AllowMergeCommits = Some false
Mirror = None Mirror = None
ProtectedBranches = Set.empty
} }
member this.OverrideDefaults () = member this.OverrideDefaults () =
@@ -93,6 +107,7 @@ type NativeRepo =
AllowRebaseExplicit = this.AllowRebaseExplicit |> Option.orElse NativeRepo.Default.AllowRebaseExplicit AllowRebaseExplicit = this.AllowRebaseExplicit |> Option.orElse NativeRepo.Default.AllowRebaseExplicit
AllowMergeCommits = this.AllowMergeCommits |> Option.orElse NativeRepo.Default.AllowMergeCommits AllowMergeCommits = this.AllowMergeCommits |> Option.orElse NativeRepo.Default.AllowMergeCommits
Mirror = this.Mirror Mirror = this.Mirror
ProtectedBranches = this.ProtectedBranches // TODO should this replace null with empty?
} }
static member internal OfSerialised (s : SerialisedNativeRepo) = static member internal OfSerialised (s : SerialisedNativeRepo) =
@@ -112,6 +127,10 @@ type NativeRepo =
AllowRebaseExplicit = s.AllowRebaseExplicit |> Option.ofNullable AllowRebaseExplicit = s.AllowRebaseExplicit |> Option.ofNullable
AllowMergeCommits = s.AllowMergeCommits |> Option.ofNullable AllowMergeCommits = s.AllowMergeCommits |> Option.ofNullable
Mirror = s.Mirror |> Option.ofNullable |> Option.map PushMirror.OfSerialised Mirror = s.Mirror |> Option.ofNullable |> Option.map PushMirror.OfSerialised
ProtectedBranches =
match s.ProtectedBranches with
| null -> Set.empty
| a -> a |> Seq.map ProtectedBranch.OfSerialised |> Set.ofSeq
} }
type GitHubRepo = type GitHubRepo =
@@ -145,7 +164,7 @@ type Repo =
} }
static member Render (client : Gitea.Client) (u : Gitea.Repository) : Repo Async = static member Render (client : Gitea.Client) (u : Gitea.Repository) : Repo Async =
if not (String.IsNullOrEmpty u.OriginalUrl) then if u.Mirror = Some true && not (String.IsNullOrEmpty u.OriginalUrl) then
{ {
Description = u.Description Description = u.Description
GitHub = GitHub =
@@ -166,6 +185,10 @@ type Repo =
elif mirror.Length = 1 then Some mirror.[0] elif mirror.Length = 1 then Some mirror.[0]
else failwith "Multiple mirrors not supported yet" else failwith "Multiple mirrors not supported yet"
let! branchProtections =
client.RepoListBranchProtection (u.Owner.LoginName, u.FullName)
|> Async.AwaitTask
return return
{ {
@@ -194,6 +217,15 @@ type Repo =
GitHubAddress = Uri m.RemoteAddress GitHubAddress = Uri m.RemoteAddress
} }
) )
ProtectedBranches =
branchProtections
|> Seq.map (fun bp ->
{
BranchName = bp.BranchName
BlockOnOutdatedBranch = bp.BlockOnOutdatedBranch
}
)
|> Set.ofSeq
} }
|> Some |> Some
} }

View File

@@ -19,6 +19,7 @@
<ItemGroup> <ItemGroup>
<Compile Include="AssemblyInfo.fs" /> <Compile Include="AssemblyInfo.fs" />
<Compile Include="Map.fs" /> <Compile Include="Map.fs" />
<Compile Include="Async.fs" />
<Compile Include="GiteaClient.fs" /> <Compile Include="GiteaClient.fs" />
<Compile Include="Domain.fs" /> <Compile Include="Domain.fs" />
<Compile Include="SerialisedConfigSchema.fs" /> <Compile Include="SerialisedConfigSchema.fs" />

View File

@@ -232,11 +232,18 @@ module Gitea =
} }
| AlignmentError.ConfigurationDiffers (desired, actual) -> | AlignmentError.ConfigurationDiffers (desired, actual) ->
match desired.GitHub, actual.GitHub with match desired.GitHub, actual.GitHub with
| None, Some _ | None, Some gitHub ->
async {
logger.LogCritical (
"Unable to reconcile the desire to move a repo from GitHub-based to Gitea-based. This feature is not exposed on the Gitea API. You must manually convert the following repo to a normal repository first: {User}:{Repo}.",
user,
r
)
}
| Some _, None -> | Some _, None ->
async { async {
logger.LogError ( logger.LogError (
"Unable to reconcile the desire to move a repo from Gitea to GitHub or vice versa: {User}, {Repo}.", "Unable to reconcile the desire to move a repo from Gitea-based to GitHub-based: {User}:{Repo}.",
user, user,
r r
) )
@@ -474,6 +481,86 @@ module Gitea =
async { logger.LogCritical ("Push mirror on {User}:{Repo} differs.", user, r) } async { logger.LogCritical ("Push mirror on {User}:{Repo} differs.", user, r) }
else else
async.Return () async.Return ()
do!
// TODO: lift this out to a function and then put it into the new-repo flow too
let extraActualProtected =
Set.difference actual.ProtectedBranches desired.ProtectedBranches
let extraDesiredProtected =
Set.difference desired.ProtectedBranches actual.ProtectedBranches
Seq.append
(Seq.map Choice1Of2 extraActualProtected)
(Seq.map Choice2Of2 extraDesiredProtected)
|> Seq.groupBy (fun b ->
match b with
| Choice1Of2 b -> b.BranchName
| Choice2Of2 b -> b.BranchName
)
|> Seq.map (fun (key, values) ->
match Seq.toList values with
| [] -> failwith "can't have appeared no times in a groupBy"
| [ Choice1Of2 x ] ->
// This is an extra rule; delete it
async {
logger.LogInformation (
"Deleting branch protection rule {BranchProtection} on {User}:{Repo}",
x.BranchName,
user,
r
)
let! _ =
client.RepoDeleteBranchProtection (user, r, x.BranchName)
|> Async.AwaitTask
return ()
}
| [ Choice2Of2 y ] ->
// This is an absent rule; add it
async {
logger.LogInformation (
"Creating branch protection rule {BranchProtection} on {User}:{Repo}",
y.BranchName,
user,
r
)
let s = Gitea.CreateBranchProtectionOption ()
s.BranchName <- y.BranchName
s.RuleName <- y.BranchName
s.BlockOnOutdatedBranch <- y.BlockOnOutdatedBranch
let! _ = client.RepoCreateBranchProtection (user, r, s) |> Async.AwaitTask
return ()
}
| [ Choice1Of2 x ; Choice2Of2 y ]
| [ Choice2Of2 y ; Choice1Of2 x ] ->
// Need to reconcile the two; the Choice2Of2 is what we want to keep
async {
logger.LogInformation (
"Reconciling branch protection rule {BranchProtection} on {User}:{Repo}",
y.BranchName,
user,
r
)
let s = Gitea.EditBranchProtectionOption ()
s.BlockOnOutdatedBranch <- y.BlockOnOutdatedBranch
let! _ =
client.RepoEditBranchProtection (user, r, y.BranchName, s)
|> Async.AwaitTask
return ()
}
| [ Choice1Of2 _ ; Choice1Of2 _ ]
| [ Choice2Of2 _ ; Choice2Of2 _ ] ->
failwith "can't have the same choice appearing twice"
| _ :: _ :: _ :: _ -> failwith "can't have appeared three times"
)
|> Async.Parallel
|> Async.map (Array.iter id)
} }
) )
) )

View File

@@ -188,6 +188,16 @@
"$ref": "#/definitions/SerialisedPushMirror" "$ref": "#/definitions/SerialisedPushMirror"
} }
] ]
},
"protectedBranches": {
"type": [
"array",
"null"
],
"description": "Protected branch configuration",
"items": {
"$ref": "#/definitions/SerialisedProtectedBranch"
}
} }
} }
}, },
@@ -195,12 +205,28 @@
"type": "object", "type": "object",
"description": "Information about a repo that is to be created on Gitea without syncing from GitHub.", "description": "Information about a repo that is to be created on Gitea without syncing from GitHub.",
"additionalProperties": false, "additionalProperties": false,
"required": [
"gitHubAddress"
],
"properties": { "properties": {
"gitHubAddress": { "gitHubAddress": {
"type": [ "type": "string"
"null", }
"string" }
] },
"SerialisedProtectedBranch": {
"type": "object",
"description": "Information about a repo that is to be created on Gitea without syncing from GitHub.",
"additionalProperties": false,
"required": [
"branchName"
],
"properties": {
"branchName": {
"type": "string"
},
"blockOnOutdatedBranch": {
"type": "boolean"
} }
} }
} }

View File

@@ -13,9 +13,22 @@ type SerialisedMergeStyle = string
[<Description "Information about a repo that is to be created on Gitea without syncing from GitHub.">] [<Description "Information about a repo that is to be created on Gitea without syncing from GitHub.">]
type SerialisedPushMirror = type SerialisedPushMirror =
{ {
[<JsonProperty(Required = Required.Always)>]
GitHubAddress : string GitHubAddress : string
} }
[<RequireQualifiedAccess>]
[<Struct>]
[<CLIMutable>]
[<Description "Information about a repo that is to be created on Gitea without syncing from GitHub.">]
type SerialisedProtectedBranch =
{
[<JsonProperty(Required = Required.Always)>]
BranchName : string
[<JsonProperty(Required = Required.DisallowNull)>]
BlockOnOutdatedBranch : Nullable<bool>
}
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
[<Struct>] [<Struct>]
[<CLIMutable>] [<CLIMutable>]
@@ -67,6 +80,9 @@ type internal SerialisedNativeRepo =
[<JsonProperty(Required = Required.DisallowNull)>] [<JsonProperty(Required = Required.DisallowNull)>]
[<Description "Configure a GitHub push mirror to sync this repo to">] [<Description "Configure a GitHub push mirror to sync this repo to">]
Mirror : Nullable<SerialisedPushMirror> Mirror : Nullable<SerialisedPushMirror>
[<JsonProperty(Required = Required.Default)>]
[<Description "Protected branch configuration">]
ProtectedBranches : SerialisedProtectedBranch array
} }
[<Struct>] [<Struct>]

View File

@@ -42,6 +42,19 @@
"gitHubAddress": "https://github.com/MyName/repo-name-3" "gitHubAddress": "https://github.com/MyName/repo-name-3"
} }
} }
},
"new-repo-mirrored-with-branches": {
"description": "A repo that's created directly on this Gitea and mirrored to GitHub",
"native": {
"defaultBranch": "main",
"private": false,
"mirror": {
"gitHubAddress": "https://github.com/MyName/repo-name-3"
},
"protectedBranches": [
{ "branchName": "main" }
]
}
} }
} }
} }