Skip to content

Commit 6a12cc6

Browse files
authored
feat: just-in-time create missing target organisations (#21)
1 parent e884a9b commit 6a12cc6

File tree

7 files changed

+101
-35
lines changed

7 files changed

+101
-35
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ A CLI to manage collections of Gitea repository mirrors, supporting either Gitea
99
## Features
1010

1111
- Mirror repositories from GitHub or Gitea to your Gitea instance
12+
- Automatic just-in-time creation of missing Gitea organizations
1213
- Configure mirroring interval and repository visibility
1314
- Batch-manage multiple repository mirrors
1415
- Filter actions by repository labels

docs/create.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ The `create` command creates mirrors in the target Gitea instance for repositori
1717
3. Create a mirror in the target Gitea server
1818
4. Set up the mirroring configuration
1919

20+
**Note**: Missing target organizations are automatically created as needed with the visibility of the first repository mirrored to that organization.
21+
2022
If no specific repositories are provided as arguments, all repositories defined in the configuration file will be processed.
2123

2224
## Examples

docs/recreate.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ The process:
1717
1. Deletes the existing mirror repository from the target Gitea instance
1818
2. Creates a new mirror with the same configuration but with updated credentials
1919

20+
**Note**: Missing target organizations are automatically created as needed with the visibility of the first repository mirrored to that organization.
21+
2022
If no specific repositories are provided as arguments, all repositories defined in the configuration file will be recreated.
2123

2224
## Examples

go.mod

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ go 1.24.0
55
require (
66
code.gitea.io/sdk/gitea v0.22.1
77
github.com/creasty/defaults v1.8.0
8-
github.com/goccy/go-yaml v1.18.0
8+
github.com/goccy/go-yaml v1.19.2
99
github.com/google/go-github/v72 v72.0.0
10-
github.com/spf13/cobra v1.10.1
10+
github.com/spf13/cobra v1.10.2
1111
github.com/spf13/viper v1.21.0
1212
github.com/stretchr/testify v1.11.1
1313
golang.org/x/term v0.39.0
@@ -19,21 +19,21 @@ require (
1919
github.com/davidmz/go-pageant v1.0.2 // indirect
2020
github.com/fsnotify/fsnotify v1.9.0 // indirect
2121
github.com/go-fed/httpsig v1.1.0 // indirect
22-
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
23-
github.com/google/go-querystring v1.1.0 // indirect
24-
github.com/hashicorp/go-version v1.7.0 // indirect
22+
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
23+
github.com/google/go-querystring v1.2.0 // indirect
24+
github.com/hashicorp/go-version v1.8.0 // indirect
2525
github.com/inconshreveable/mousetrap v1.1.0 // indirect
2626
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
2727
github.com/pmezard/go-difflib v1.0.0 // indirect
28-
github.com/sagikazarmark/locafero v0.11.0 // indirect
29-
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
28+
github.com/sagikazarmark/locafero v0.12.0 // indirect
3029
github.com/spf13/afero v1.15.0 // indirect
3130
github.com/spf13/cast v1.10.0 // indirect
3231
github.com/spf13/pflag v1.0.10 // indirect
3332
github.com/subosito/gotenv v1.6.0 // indirect
3433
go.yaml.in/yaml/v3 v3.0.4 // indirect
35-
golang.org/x/crypto v0.45.0 // indirect
34+
golang.org/x/crypto v0.47.0 // indirect
3635
golang.org/x/sys v0.40.0 // indirect
37-
golang.org/x/text v0.31.0 // indirect
36+
golang.org/x/text v0.33.0 // indirect
37+
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
3838
gopkg.in/yaml.v3 v3.0.1 // indirect
3939
)

go.sum

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,19 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
1515
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
1616
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
1717
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
18-
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
19-
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
20-
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
21-
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
22-
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
18+
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
19+
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
20+
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
21+
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
22+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
2323
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
2424
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
2525
github.com/google/go-github/v72 v72.0.0 h1:FcIO37BLoVPBO9igQQ6tStsv2asG4IPcYFi655PPvBM=
2626
github.com/google/go-github/v72 v72.0.0/go.mod h1:WWtw8GMRiL62mvIquf1kO3onRHeWWKmK01qdCY8c5fg=
27-
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
28-
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
29-
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
30-
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
27+
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
28+
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
29+
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
30+
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
3131
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
3232
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
3333
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -41,16 +41,14 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
4141
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
4242
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
4343
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
44-
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
45-
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
46-
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
47-
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
44+
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
45+
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
4846
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
4947
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
5048
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
5149
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
52-
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
53-
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
50+
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
51+
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
5452
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
5553
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
5654
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -65,8 +63,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
6563
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
6664
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
6765
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
68-
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
69-
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
66+
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
67+
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
7068
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
7169
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
7270
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -79,10 +77,9 @@ golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
7977
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
8078
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
8179
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
82-
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
83-
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
80+
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
81+
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
8482
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
85-
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
8683
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
8784
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
8885
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

pkg/gitea/controller.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import (
1010
)
1111

1212
type Controller struct {
13-
ctx context.Context
14-
client *gitea.Client
15-
forge config.Forge
13+
ctx context.Context
14+
client *gitea.Client
15+
forge config.Forge
16+
orgCache map[string]*gitea.Organization
1617
}
1718

1819
func NewController(ctx context.Context, forge config.Forge) (*Controller, error) {
@@ -22,9 +23,10 @@ func NewController(ctx context.Context, forge config.Forge) (*Controller, error)
2223
}
2324

2425
return &Controller{
25-
ctx: ctx,
26-
client: client,
27-
forge: forge,
26+
ctx: ctx,
27+
client: client,
28+
forge: forge,
29+
orgCache: make(map[string]*gitea.Organization),
2830
}, nil
2931
}
3032

pkg/gitea/mirror.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,63 @@ func (e *RepositoryNotMirror) Error() string {
1616
return "Repository is not a mirror"
1717
}
1818

19+
func (c *Controller) GetOrg(r *config.Repository) *gitea.Organization {
20+
// Check cache first
21+
if org, cached := c.orgCache[r.Owner]; cached {
22+
return org
23+
}
24+
25+
// Cache miss - query API
26+
org, _, err := c.client.GetOrg(r.Owner)
27+
if err != nil {
28+
// Cache the fact that org doesn't exist
29+
c.orgCache[r.Owner] = nil
30+
return nil
31+
}
32+
33+
// Cache the successful result
34+
c.orgCache[r.Owner] = org
35+
return org
36+
}
37+
38+
func (c *Controller) CreateOrg(orgName string, visibility gitea.VisibleType) (*gitea.Organization, error) {
39+
options := gitea.CreateOrgOption{
40+
Name: orgName,
41+
FullName: orgName,
42+
Visibility: visibility,
43+
}
44+
org, _, err := c.client.CreateOrg(options)
45+
if err != nil {
46+
return nil, err
47+
}
48+
return org, err
49+
}
50+
51+
func (c *Controller) EnsureOrg(r *config.Repository) error {
52+
// Check if organization already exists (uses cache)
53+
org := c.GetOrg(r)
54+
if org != nil {
55+
return nil
56+
}
57+
58+
// Determine visibility based on repository's PublicTarget setting
59+
visibility := gitea.VisibleTypePublic
60+
if !*r.PublicTarget {
61+
visibility = gitea.VisibleTypePrivate
62+
}
63+
64+
// Create the organization with appropriate visibility
65+
org, err := c.CreateOrg(r.Owner, visibility)
66+
if err != nil {
67+
return fmt.Errorf("creating organization %s: %w", r.Owner, err)
68+
}
69+
70+
// Update cache with newly created org
71+
c.orgCache[r.Owner] = org
72+
73+
return nil
74+
}
75+
1976
func (c *Controller) GetMirror(r *config.Repository) (*gitea.Repository, error) {
2077
repo, _, err := c.client.GetRepo(r.Owner, r.Name)
2178
if err != nil {
@@ -25,6 +82,11 @@ func (c *Controller) GetMirror(r *config.Repository) (*gitea.Repository, error)
2582
}
2683

2784
func (c *Controller) CreateMirror(source server.Server, r *config.Repository) (*gitea.Repository, error) {
85+
// Ensure the organization exists before attempting to create the mirror
86+
if err := c.EnsureOrg(r); err != nil {
87+
return nil, fmt.Errorf("ensuring organization %s: %w", r.Owner, err)
88+
}
89+
2890
cloneURL, err := source.GetCloneURL(r)
2991
if err != nil {
3092
return nil, err

0 commit comments

Comments
 (0)