7 Commits
4 changed files with 75 additions and 53 deletions
+4 -2
View File
@@ -10,7 +10,9 @@ ENABLE_PUSH_CREATE_USER = true
ENABLE_PUSH_CREATE_ORG = true
```
Optionally, create a new organization in your Gitea instance to store your `ggv` vendor repos.
Create a new organization in your Gitea instance to store your `ggv` vendor repos.
This is necessary not just to keep your Gitea clean, but also to tell `ggv` what repos to ignore as inputs
(you don't want to vendor your vendor repos!).
> [!WARNING]
>It is recommended to create a user that only has access to that one organization and use said new user to generate the below API key.
@@ -22,7 +24,7 @@ This key needs `repository` "Read and Write" access. Everything else can be left
## Usage
`echo 'EXAMPLE_API_TOKEN' | ggv https://gitea.example.com myOutputOrganization`
I recommend setting up a chron job or some form of automation for this. My deployment involves running this on my Gitea mirror (this server mirrors my main Gitea instance as well as my GitHub) weekly using a chron job, then my main Gitea instance mirrors the output repos from this tool.
I recommend setting up a cron job or some form of automation for this.
> [!WARNING]
>This tool assumes your default branch is what you want to vendor.
+7 -7
View File
@@ -1,6 +1,6 @@
module ggv
go 1.26.4
go 1.26.5
require (
github.com/go-git/go-git/v6 v6.0.0-alpha.4
@@ -10,16 +10,16 @@ require (
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.4.1 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cloudflare/circl v1.6.4 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg/v2 v2.0.2 // indirect
github.com/go-git/go-billy/v6 v6.0.0-alpha.1 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/net v0.54.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/sys v0.46.0 // indirect
)
+16 -16
View File
@@ -6,8 +6,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cloudflare/circl v1.6.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U=
github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -25,8 +25,8 @@ github.com/go-git/go-git/v6 v6.0.0-alpha.4 h1:aDTc2UGanmaE7FkGLSlBEB9nohMnQ+RKXc
github.com/go-git/go-git/v6 v6.0.0-alpha.4/go.mod h1:4ODa/G7hPWrh4Y+7lmt59Ij3zW38IEfvRoAZxLYYBhc=
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@@ -42,18 +42,18 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+48 -28
View File
@@ -8,11 +8,13 @@ import (
"os"
"os/exec"
"strings"
"time"
"github.com/go-git/go-git/v6"
"github.com/go-git/go-git/v6/config"
"github.com/go-git/go-git/v6/plumbing"
gitclient "github.com/go-git/go-git/v6/plumbing/client"
"github.com/go-git/go-git/v6/plumbing/object"
githttp "github.com/go-git/go-git/v6/plumbing/transport/http"
"github.com/go-git/go-git/v6/storage/memory"
"github.com/rwinkhart/go-boilerplate/back"
@@ -25,25 +27,28 @@ const hashName = ".gvv-hash"
type giteaRespT struct {
Data []struct {
MirrorInterval string `json:"mirror_interval"`
Language string `json:"language"`
MasterURL string `json:"original_url"`
Name string `json:"name"`
Owner struct {
Organization string `json:"username"`
} `json:"owner"`
Language string `json:"language"`
MasterURL string `json:"original_url"`
Name string `json:"name"`
}
}
func logError(message string) {
fmt.Println(back.AnsiWarning + message + back.AnsiReset)
}
func main() {
// args
if len(os.Args) < 2 {
fmt.Println("Usage: " + os.Args[0] + " <base url> [output organization]\n\nAPI token can be piped into stdin to run non-interactively!")
if len(os.Args) < 3 {
fmt.Println("Usage: " + os.Args[0] + " <base url> <output organization>\n\nAPI token can be piped into stdin to run non-interactively!")
os.Exit(0)
}
baseURL := os.Args[1]
apiURL := baseURL + "/api/v1/repos/search?sort=updated&order=asc&limit=999"
var organization string
if len(os.Args) == 3 {
organization = os.Args[2]
}
outputOrganization := os.Args[2]
fmt.Print("Input token: ")
token := back.ReadFromStdin()
if len(token) == 0 {
@@ -82,7 +87,7 @@ func main() {
os.Mkdir(workingDir, 0755)
repoLoop:
for _, repoResp := range giteaResp.Data {
if repoResp.Language == "Go" && repoResp.MirrorInterval != "" {
if repoResp.Owner.Organization != outputOrganization && repoResp.Language == "Go" {
currentRepoDir := workingDir + "/" + repoResp.Name + "-ggv"
//// skip if local copy is already up-to-date
targetRepoDir := finishedDir + "/" + repoResp.Name + "-ggv"
@@ -101,10 +106,11 @@ repoLoop:
if strings.TrimSpace(string(localHash)) == ref2.Hash().String() {
fmt.Println("Skipping " + repoResp.MasterURL + " (already up-to-date)")
if err = os.MkdirAll(currentRepoDir, 0755); err != nil {
other.PrintError("Failed to create dummy repo for "+repoResp.MasterURL+": "+err.Error(), 7)
logError("Failed to create dummy repo for " + repoResp.MasterURL + ": " + err.Error())
continue repoLoop
}
if err = os.WriteFile(currentRepoDir+"/"+hashName, localHash, 0644); err != nil {
other.PrintError("Failed to write dummy head hash for "+repoResp.MasterURL+": "+err.Error(), 8)
logError("Failed to write dummy head hash for " + repoResp.MasterURL + ": " + err.Error())
}
continue repoLoop
}
@@ -114,53 +120,66 @@ repoLoop:
break
}
}
} else {
logError("Failed to fetch remote refs for " + repoResp.MasterURL + ": " + err.Error())
continue repoLoop
}
}
//// clone
oldRepo, err := git.PlainClone(currentRepoDir, &git.CloneOptions{URL: repoResp.MasterURL, Progress: nil, Depth: 1})
if err != nil {
other.PrintError("Failed to clone "+repoResp.MasterURL+": "+err.Error(), 7)
logError("Failed to clone " + repoResp.MasterURL + ": " + err.Error())
continue repoLoop
}
//// store commit hash
head, err := oldRepo.Head()
if err != nil {
other.PrintError("Failed to get head of "+repoResp.MasterURL+": "+err.Error(), 8)
logError("Failed to get head of " + repoResp.MasterURL + ": " + err.Error())
continue repoLoop
}
if err = os.WriteFile(currentRepoDir+"/"+hashName, []byte(head.Hash().String()), 0644); err != nil {
other.PrintError("Failed to write head hash for "+repoResp.MasterURL+": "+err.Error(), 9)
logError("Failed to write head hash for " + repoResp.MasterURL + ": " + err.Error())
continue repoLoop
}
//// vendor
vendorCmd := exec.Command("go", "mod", "vendor")
vendorCmd.Dir = currentRepoDir
if err = vendorCmd.Run(); err != nil {
other.PrintError("Failed to vendor "+repoResp.MasterURL+": "+err.Error(), 10)
logError("Failed to vendor " + repoResp.MasterURL + ": " + err.Error())
continue repoLoop
}
//// re-init
if err = os.RemoveAll(currentRepoDir + "/.git"); err != nil {
other.PrintError("Failed to remove .git directory for "+repoResp.MasterURL+": "+err.Error(), 11)
logError("Failed to remove .git directory for " + repoResp.MasterURL + ": " + err.Error())
continue repoLoop
}
newRepo, err := git.PlainInit(currentRepoDir, false)
if err != nil {
other.PrintError("Failed to init in "+currentRepoDir+": "+err.Error(), 12)
logError("Failed to init in " + currentRepoDir + ": " + err.Error())
continue repoLoop
}
//// set origin
if _, err = newRepo.CreateRemote(&config.RemoteConfig{
Name: "origin",
URLs: []string{baseURL + "/" + organization + "/" + repoResp.Name + "-ggv.git"},
URLs: []string{baseURL + "/" + outputOrganization + "/" + repoResp.Name + "-ggv.git"},
}); err != nil {
other.PrintError("Failed to set origin for "+currentRepoDir+": "+err.Error(), 13)
logError("Failed to set origin for " + currentRepoDir + ": " + err.Error())
continue repoLoop
}
//// add
wt, err := newRepo.Worktree()
if err != nil {
other.PrintError("Failed to get worktree for "+currentRepoDir+": "+err.Error(), 14)
logError("Failed to get worktree for " + currentRepoDir + ": " + err.Error())
continue repoLoop
}
if err = wt.AddGlob("."); err != nil {
other.PrintError("Failed to add files in "+currentRepoDir+": "+err.Error(), 15)
logError("Failed to add files in " + currentRepoDir + ": " + err.Error())
continue repoLoop
}
//// commit
if _, err = wt.Commit("go-gitea-vendor", &git.CommitOptions{}); err != nil {
other.PrintError("Failed to commit in "+currentRepoDir+": "+err.Error(), 16)
if _, err = wt.Commit("go-gitea-vendor", &git.CommitOptions{Author: &object.Signature{Name: "ggv", Email: "gvv@local.local", When: time.Now()}}); err != nil {
logError("Failed to commit in " + currentRepoDir + ": " + err.Error())
continue repoLoop
}
//// push
if err = newRepo.Push(&git.PushOptions{
@@ -172,16 +191,17 @@ repoLoop:
}),
},
}); err != nil {
other.PrintError("Failed to push "+currentRepoDir+": "+err.Error(), 17)
logError("Failed to push " + currentRepoDir + ": " + err.Error())
continue repoLoop
}
}
}
// end cleanup
if err = os.RemoveAll(finishedDir); err != nil {
other.PrintError("Failed to remove old repos dir: "+err.Error(), 18)
other.PrintError("Failed to remove old repos dir: "+err.Error(), 7)
}
if err = os.Rename(workingDir, finishedDir); err != nil {
other.PrintError("Failed to rename new repos dir: "+err.Error(), 20)
other.PrintError("Failed to rename new repos dir: "+err.Error(), 8)
}
}