Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow multiple push mirrors #97

Merged
merged 5 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 47 additions & 21 deletions Gitea.Declarative.Lib/ConfigSchema.fs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,28 @@ type MergeStyle =

static member toString (m : MergeStyle) = m.ToString ()

[<NoComparison>]
[<CustomEquality>]
type PushMirror =
{
GitHubAddress : Uri
/// Gitea should always tell us a remote name, but a user in their config can't.
RemoteName : string option
}

/// Equality check ignores remote names, which are not known to the user but which Gitea tracks internally.
override this.Equals (other : obj) : bool =
match other with
| :? PushMirror as other -> this.GitHubAddress.ToString () = other.GitHubAddress.ToString ()
| _ -> false

override this.GetHashCode () : int =
this.GitHubAddress.ToString().GetHashCode ()

static member OfSerialised (s : SerialisedPushMirror) : PushMirror =
{
GitHubAddress = Uri s.GitHubAddress
RemoteName = None
}

member this.ToSerialised () : SerialisedPushMirror =
Expand Down Expand Up @@ -82,7 +96,7 @@ type NativeRepo =
AllowRebase : bool option
AllowRebaseExplicit : bool option
AllowMergeCommits : bool option
Mirror : PushMirror option
Mirrors : PushMirror list
ProtectedBranches : ProtectedBranch Set
Collaborators : string Set
}
Expand All @@ -103,7 +117,7 @@ type NativeRepo =
AllowRebase = Some false
AllowRebaseExplicit = Some false
AllowMergeCommits = Some false
Mirror = None
Mirrors = []
ProtectedBranches = Set.empty
Collaborators = Set.empty
}
Expand All @@ -128,7 +142,7 @@ type NativeRepo =
AllowRebase = this.AllowRebase |> Option.orElse NativeRepo.Default.AllowRebase
AllowRebaseExplicit = this.AllowRebaseExplicit |> Option.orElse NativeRepo.Default.AllowRebaseExplicit
AllowMergeCommits = this.AllowMergeCommits |> Option.orElse NativeRepo.Default.AllowMergeCommits
Mirror = this.Mirror
Mirrors = this.Mirrors
ProtectedBranches = this.ProtectedBranches // TODO should this replace null with empty?
Collaborators = this.Collaborators
}
Expand All @@ -149,7 +163,12 @@ type NativeRepo =
AllowRebase = s.AllowRebase |> Option.ofNullable
AllowRebaseExplicit = s.AllowRebaseExplicit |> Option.ofNullable
AllowMergeCommits = s.AllowMergeCommits |> Option.ofNullable
Mirror = s.Mirror |> Option.ofNullable |> Option.map PushMirror.OfSerialised
Mirrors =
s.Mirrors
|> Option.ofObj
|> Option.defaultValue [||]
|> List.ofArray
|> List.map PushMirror.OfSerialised
ProtectedBranches =
match s.ProtectedBranches with
| null -> Set.empty
Expand Down Expand Up @@ -179,10 +198,7 @@ type NativeRepo =
AllowRebase = this.AllowRebase |> Option.toNullable
AllowRebaseExplicit = this.AllowRebaseExplicit |> Option.toNullable
AllowMergeCommits = this.AllowMergeCommits |> Option.toNullable
Mirror =
match this.Mirror with
| None -> Nullable ()
| Some mirror -> Nullable (mirror.ToSerialised ())
Mirrors = this.Mirrors |> List.toArray |> Array.map (fun a -> a.ToSerialised ())
ProtectedBranches = this.ProtectedBranches |> Seq.map (fun b -> b.ToSerialised ()) |> Array.ofSeq
Collaborators = Set.toArray this.Collaborators
}
Expand Down Expand Up @@ -239,17 +255,18 @@ type Repo =
}
|> async.Return
else
let repoFullName = u.FullName

async {
let! mirror =
let owner = u.Owner

let loginName = owner.LoginName

let! mirrors =
getAllPaginated (fun page count ->
client.RepoListPushMirrors (u.Owner.LoginName, u.FullName, Some page, Some count)
client.RepoListPushMirrors (loginName, repoFullName, Some page, Some count)
)

let mirror =
if mirror.Length = 0 then None
elif mirror.Length = 1 then Some mirror.[0]
else failwith "Multiple mirrors not supported yet"

let! (branchProtections : Gitea.BranchProtection[]) =
client.RepoListBranchProtection (u.Owner.LoginName, u.FullName)
|> Async.AwaitTask
Expand All @@ -259,16 +276,23 @@ type Repo =
client.RepoListCollaborators (u.Owner.LoginName, u.FullName, Some page, Some count)
)

let defaultBranch = u.DefaultBranch

let collaborators =
collaborators |> Seq.map (fun user -> user.LoginName) |> Set.ofSeq

let description = u.Description

return

{
Description = u.Description
Description = description
Deleted = None
GitHub = None
Native =
{
Private = u.Private
DefaultBranch = u.DefaultBranch
DefaultBranch = defaultBranch
IgnoreWhitespaceConflicts = u.IgnoreWhitespaceConflicts
HasPullRequests = u.HasPullRequests
HasProjects = u.HasProjects
Expand All @@ -281,11 +305,13 @@ type Repo =
AllowRebase = u.AllowRebase
AllowRebaseExplicit = u.AllowRebaseExplicit
AllowMergeCommits = u.AllowMergeCommits
Mirror =
mirror
|> Option.map (fun m ->
Mirrors =
mirrors
|> Array.toList
|> List.map (fun m ->
{
GitHubAddress = Uri m.RemoteAddress
RemoteName = Some m.RemoteName
}
)
ProtectedBranches =
Expand All @@ -302,7 +328,7 @@ type Repo =
}
)
|> Set.ofSeq
Collaborators = collaborators |> Seq.map (fun user -> user.LoginName) |> Set.ofSeq
Collaborators = collaborators
}
|> Some
}
Expand Down
202 changes: 158 additions & 44 deletions Gitea.Declarative.Lib/Gitea.fs
Original file line number Diff line number Diff line change
Expand Up @@ -361,34 +361,134 @@ module Gitea =
else
async.Return ()

// Push mirrors
do!
match desired.Mirror, actual.Mirror with
| None, None -> async.Return ()
| None, Some m ->
async { logger.LogError ("Refusing to delete push mirror for {User}:{Repo}", user, repoName) }
| Some desired, None ->
match githubApiToken with
| None ->
async {
logger.LogCritical (
"Cannot add push mirror for {User}:{Repo} due to lack of GitHub API token",
user,
repoName
let desired =
desired.Mirrors
|> List.groupBy (fun m -> (m.GitHubAddress : Uri).ToString ())
|> Map.ofList

let desired =
desired
|> Map.toSeq
|> Seq.map (fun (name, pm) ->
match pm with
| [] -> failwith "LOGIC ERROR"
| [ pm ] -> pm.GitHubAddress.ToString ()
| _ ->
failwith
$"Config validation failed on repo %s{repoName}: multiple push mirrors configured for target %s{name}"
)
|> Set.ofSeq

let actual =
actual.Mirrors
|> List.groupBy (fun m -> (m.GitHubAddress : Uri).ToString ())
|> Map.ofList

// If any mirror target has multiple push mirrors for it, just delete them all before continuing.
let deleteExisting =
actual
|> Map.toSeq
|> Seq.choose (fun (k, vs) ->
match vs with
| [] -> failwith "LOGIC ERROR"
| [ _ ] -> None
| vs ->
vs
|> List.map (fun pm ->
async {
logger.LogWarning (
"Multiple push mirrors on repo {Owner}/{RepoName} for target {PushMirrorTarget} found. Deleting them all before recreating.",
user,
repoName,
k
)

let! ct = Async.CancellationToken
// sigh, domain model - it's *such* a faff to represent this correctly though
do!
client.RepoDeletePushMirror (user, repoName, Option.get pm.RemoteName)
|> Async.AwaitTask
}
)
}
| Some token ->
async {
logger.LogInformation ("Setting up push mirror on {User}:{Repo}", user, repoName)
let pushMirrorOption = createPushMirrorOption desired.GitHubAddress token
let! _ = client.RepoAddPushMirror (user, repoName, pushMirrorOption) |> Async.AwaitTask
return ()
}
| Some desired, Some actual ->
if desired <> actual then
async { logger.LogCritical ("Push mirror on {User}:{Repo} differs.", user, repoName) }
else
async.Return ()
|> Async.Sequential
|> Async.map (Array.iter id)
|> Some
)
|> Seq.toList

let actual =
match deleteExisting with
| [] -> actual
| _ -> Map.empty

let distinctActual = actual.Keys |> Set.ofSeq
let presentButNotDesired = Set.difference distinctActual desired
let desiredButNotPresent = Set.difference desired distinctActual

let deleteUndesired =
presentButNotDesired
|> Seq.map (fun toDelete ->
logger.LogWarning (
"Deleting push mirror on repo {Owner}/{RepoName} for target {PushMirrorTarget}",
user,
repoName,
toDelete
)

let toDelete = actual.[toDelete]

toDelete
|> Seq.map (fun pm ->
async {
let! ct = Async.CancellationToken

do!
client.RepoDeletePushMirror (user, repoName, Option.get pm.RemoteName)
|> Async.AwaitTask
}
)
|> Async.Sequential
|> Async.map (Array.iter id)
)
|> Seq.toList

let addDesired =
desiredButNotPresent
|> Seq.map (fun toAdd ->
match githubApiToken with
| None ->
async {
logger.LogCritical (
"Cannot add push mirror for {User}:{Repo} due to lack of GitHub API token",
user,
repoName
)
}
| Some token ->
async {
logger.LogInformation ("Setting up push mirror on {User}:{Repo}", user, repoName)
let! ct = Async.CancellationToken
let pushMirrorOption = createPushMirrorOption (Uri toAdd) token

let! _ = client.RepoAddPushMirror (user, repoName, pushMirrorOption) |> Async.AwaitTask

return ()
}
)
|> Seq.toList

if deleteExisting.IsEmpty && deleteUndesired.IsEmpty && addDesired.IsEmpty then
async.Return ()
else
async {
do! deleteExisting |> Async.Sequential |> Async.map (Array.iter id)
do! deleteUndesired |> Async.Sequential |> Async.map (Array.iter id)
do! addDesired |> Async.Sequential |> Async.map (Array.iter id)
}

// Collaborators
do!
let desiredButNotPresent = Set.difference desired.Collaborators actual.Collaborators
let presentButNotDesired = Set.difference actual.Collaborators desired.Collaborators
Expand Down Expand Up @@ -547,30 +647,44 @@ module Gitea =
| Choice2Of2 e -> raise (AggregateException ($"Error creating {user}:{r}", e))
| Choice1Of2 _ -> ()

match native.Mirror, githubApiToken with
| None, _ -> ()
| Some mirror, None -> failwith "Cannot push to GitHub mirror with an API key"
| Some mirror, Some token ->
match native.Mirrors, githubApiToken with
| [], _ -> ()
| _ :: _, None -> failwith "Cannot push to GitHub mirror without an API key"
| mirrors, Some token ->
logger.LogInformation ("Setting up push mirror for {User}:{Repo}", user, r)
let options = Gitea.CreatePushMirrorOption ()
options.SyncOnCommit <- Some true
options.RemoteAddress <- (mirror.GitHubAddress : Uri).ToString ()
options.RemoteUsername <- token
options.RemotePassword <- token
options.Interval <- "8h0m0s"

let! mirrors =

let! actualMirrors =
getAllPaginated (fun page count ->
client.RepoListPushMirrors (user, r, Some page, Some count)
)

match mirrors |> Array.tryFind (fun m -> m.RemoteAddress = options.RemoteAddress) with
| None ->
let! _ = client.RepoAddPushMirror (user, r, options) |> Async.AwaitTask
()
| Some existing ->
if existing.SyncOnCommit <> Some true then
failwith $"sync on commit should have been true for {user}:{r}"
do!
mirrors
|> List.map (fun mirror ->
let options = Gitea.CreatePushMirrorOption ()
options.SyncOnCommit <- Some true
options.RemoteAddress <- (mirror.GitHubAddress : Uri).ToString ()
options.RemoteUsername <- token
options.RemotePassword <- token
options.Interval <- "8h0m0s"

async {
match
actualMirrors
|> Array.tryFind (fun m -> m.RemoteAddress = options.RemoteAddress)
with
| None ->
let! _ =
client.RepoAddPushMirror (user, r, options) |> Async.AwaitTask

()
| Some existing ->
if existing.SyncOnCommit <> Some true then
failwith $"sync on commit should have been true for {user}:{r}"
}
)
|> Async.Sequential
|> Async.map (Array.iter id)

()
| Some github, None ->
Expand Down
Loading