├── .gitignore ├── .golangci.yaml ├── LICENSE ├── Makefile ├── README.md ├── build ├── build.sh ├── clean.sh └── format.sh ├── cmd └── root.go ├── go.mod ├── go.sum ├── main.go └── pkg ├── mirror ├── auth.go ├── cloner.go └── pusher.go ├── options ├── .gitignore ├── option.go └── option_test.go ├── remote ├── gitee.go ├── github.go ├── gitlab.go └── remote.go └── utils ├── bytes.go ├── consts.go ├── guard.go ├── hashset.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | _output/ 10 | testbin/* 11 | 12 | 13 | # Test binary, build with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | # Kubernetes Generated files - skip generated files, except for vendored files 20 | 21 | # !vendor/**/zz_generated.* 22 | 23 | # editor and IDE paraphernalia 24 | .idea 25 | *.swp 26 | *.swo 27 | *~ 28 | vendor 29 | VERSION 30 | 31 | .DS_Store 32 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | skip-dirs: 3 | - vendor 4 | timeout: 30m 5 | skip-files: [ ] 6 | tests: false 7 | 8 | issues: 9 | max-same-issues: 0 10 | exclude-rules: [ ] 11 | 12 | linters: 13 | disable-all: true 14 | enable: 15 | - goimports 16 | - gofmt 17 | - ineffassign 18 | - staticcheck 19 | - unused 20 | 21 | linters-settings: # please keep this alphabetized 22 | staticcheck: 23 | go: "1.17" 24 | checks: [ 25 | "all", 26 | ] 27 | goimports: 28 | local-prefixes: github.com/kom0055/git-mirror 29 | unused: 30 | go: "1.17" 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2022 Kun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: format 2 | format: 3 | go mod download; \ 4 | go mod tidy; \ 5 | build/format.sh 6 | 7 | .PHONY: clean 8 | clean: 9 | build/clean.sh 10 | 11 | .PHONY: all 12 | all: format 13 | build/build.sh 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git-mirror an easy tool to mirror all you repos between Gitlab and Github 2 | 3 | If there are too many repos on your Gitlab, it's too difficult to migrate or mirror those repos. 4 | 5 | I was in the same situation before, to solve this, I wrote an easy tool to mirror all you visible repos between gitlab and github. 6 | For example, it will mirror-clone all you visible repos on Gitlab, create repos on your Github account, and mirror-push them. 7 | 8 | Gitee is not supported now. 9 | 10 | ## Build 11 | 12 | ```sh 13 | git clone https://github.com/kom0055/git-mirror.git 14 | cd git-mirror 15 | make 16 | ``` 17 | 18 | ## Usage 19 | 20 | ``` 21 | $ git-mirror --help 22 | to clone repo: gclone https://github.com/kom0055/git-mirror 23 | to sync all available repos: gclone --sync-from-remote --remote-type=gh https://github.com 24 | 25 | Usage: 26 | gclone [flags] 27 | 28 | Flags: 29 | --dest-ecdsa string ecdsa pem file 30 | --dest-ecdsa-passwd string ecdsa pem file passwd 31 | --dest-proto string proto: git, ssh, http or https 32 | --dest-remote-gitlab-addr string remote gitlab addr 33 | --dest-token string token or private key 34 | --dest-user string user name 35 | -h, --help help for gclone 36 | --source-ecdsa string ecdsa pem file 37 | --source-ecdsa-passwd string ecdsa pem file passwd 38 | --source-proto string proto: git, ssh, http or https 39 | --source-remote-gitlab-addr string remote gitlab addr 40 | --source-token string token or private key 41 | --source-user string user name 42 | --worker int worker num (default 8) 43 | ``` 44 | -------------------------------------------------------------------------------- /build/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | #set -x 7 | 8 | PACKAGE_NAME="gclone" 9 | BASE_DIR=$(cd $(dirname $0)/.. && pwd) 10 | OUTPUT_PATH=${BASE_DIR}/_output 11 | mkdir -p ${OUTPUT_PATH}/bin 12 | 13 | source "${BASE_DIR}/hack/version.sh" 14 | 15 | go build -gcflags=all="-N -l" -a -o "${OUTPUT_PATH}"/bin/${PACKAGE_NAME} \ 16 | -ldflags "$(api::version::ldflags)" "${OUTPUT_PATH}"/cmd/ 17 | -------------------------------------------------------------------------------- /build/clean.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | rm -rf ./_output 8 | -------------------------------------------------------------------------------- /build/format.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2020 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # This script checks whether the source codes need to be formatted or not by 18 | # `gofmt`. We should run `hack/update-gofmt.sh` if actually formats them. 19 | # Usage: `hack/verify-gofmt.sh`. 20 | # Note: GoFmt apparently is changing @ head... 21 | 22 | set -o errexit 23 | set -o nounset 24 | set -o pipefail 25 | 26 | SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. 27 | 28 | cd "${SCRIPT_ROOT}" 29 | 30 | find_files() { 31 | find . -not \( \ 32 | \( \ 33 | -wholename './output' \ 34 | -o -wholename './.git' \ 35 | -o -wholename './_output' \ 36 | -o -wholename './release' \ 37 | -o -wholename './target' \ 38 | -o -wholename '*/third_party/*' \ 39 | -o -wholename '*/vendor/*' \ 40 | -o -wholename './staging/src/k8s.io/client-go/*vendor/*' \ 41 | -o -wholename '*/bindata.go' \ 42 | \) -prune \ 43 | \) -name '*.go' -type f 44 | } 45 | 46 | # gofmt exits with non-zero exit code if it finds a problem unrelated to 47 | # formatting (e.g., a file does not parse correctly). Without "|| true" this 48 | # would have led to no useful error message from gofmt, because the script would 49 | # have failed before getting to the "echo" in the block below. 50 | 51 | find_files | xargs goimports -w -local github.com/kom0055/git-mirror 52 | find_files | xargs gofmt -s -d -w 53 | golangci-lint run -c "${SCRIPT_ROOT}"/.golangci.yaml "${SCRIPT_ROOT}"/... 54 | find_files | xargs cat | wc -l 55 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "log" 6 | "os" 7 | 8 | "github.com/kom0055/git-mirror/pkg/options" 9 | "github.com/kom0055/git-mirror/pkg/utils" 10 | ) 11 | 12 | var ( 13 | option = options.Option{} 14 | ) 15 | 16 | // rootCmd represents the base command when called without any subcommands 17 | var rootCmd = &cobra.Command{ 18 | Use: "gclone", 19 | Short: "clone repo to local path or sync all available repos to local path", 20 | Long: `to clone repo: gclone https://github.com/kom0055/git-mirror 21 | to sync all available repos: gclone --sync-from-remote --remote-type=gh https://github.com `, 22 | 23 | Run: func(cmd *cobra.Command, args []string) { 24 | if err := os.MkdirAll(utils.DefaultTmpPath, 0755); err != nil { 25 | log.Fatalf("failed to create tmp dir: %v\n", err) 26 | } 27 | defer func() { 28 | _ = os.RemoveAll(utils.DefaultTmpPath) 29 | }() 30 | 31 | if err := option.Mirror(cmd.Context()); err != nil { 32 | log.Fatalln(err) 33 | } 34 | 35 | }, 36 | } 37 | 38 | // Execute adds all child commands to the root command and sets flags appropriately. 39 | // This is called by main.main(). It only needs to happen once to the rootCmd. 40 | func Execute() { 41 | 42 | err := rootCmd.Execute() 43 | if err != nil { 44 | os.Exit(1) 45 | } 46 | } 47 | 48 | func init() { 49 | option = options.Option{ 50 | Source: options.BasicOpt{ 51 | EcdsaPemFile: os.Getenv("SOURCE_ECDSA_PEM_FILE"), 52 | EcdsaPemFilePasswd: os.Getenv("SOURCE_ECDSA_PEM_FILE_PASSWD"), 53 | RemoteGitlabAddr: os.Getenv("SOURCE_REMOTE_GITLAB_ADDR"), 54 | User: os.Getenv("SOURCE_USER"), 55 | Token: os.Getenv("SOURCE_TOKEN"), 56 | Proto: os.Getenv("SOURCE_PROTO"), 57 | GroupName: os.Getenv("SOURCE_GROUP_NAME"), 58 | }, 59 | Dest: options.BasicOpt{ 60 | EcdsaPemFile: os.Getenv("DEST_ECDSA_PEM_FILE"), 61 | EcdsaPemFilePasswd: os.Getenv("DEST_ECDSA_PEM_FILE_PASSWD"), 62 | RemoteGitlabAddr: os.Getenv("DEST_REMOTE_GITLAB_ADDR"), 63 | User: os.Getenv("DEST_USER"), 64 | Token: os.Getenv("DEST_TOKEN"), 65 | Proto: os.Getenv("DEST_PROTO"), 66 | GroupName: os.Getenv("DEST_GROUP_NAME"), 67 | }, 68 | Worker: 8, 69 | } 70 | 71 | rootCmd.PersistentFlags().IntVar(&option.Worker, "worker", option.Worker, "worker num") 72 | 73 | rootCmd.PersistentFlags().StringVar(&option.Source.EcdsaPemFile, "source-ecdsa", option.Source.EcdsaPemFile, "ecdsa pem file") 74 | rootCmd.PersistentFlags().StringVar(&option.Source.EcdsaPemFilePasswd, "source-ecdsa-passwd", option.Source.EcdsaPemFilePasswd, "ecdsa pem file passwd") 75 | rootCmd.PersistentFlags().StringVar(&option.Source.RemoteGitlabAddr, "source-remote-gitlab-addr", option.Source.RemoteGitlabAddr, "remote gitlab addr") 76 | rootCmd.PersistentFlags().StringVar(&option.Source.User, "source-user", option.Source.User, "user name") 77 | rootCmd.PersistentFlags().StringVar(&option.Source.Token, "source-token", option.Source.Token, "token or private key") 78 | rootCmd.PersistentFlags().StringVar(&option.Source.Proto, "source-proto", option.Source.Proto, "proto: git, ssh, http or https") 79 | 80 | rootCmd.PersistentFlags().StringVar(&option.Dest.EcdsaPemFile, "dest-ecdsa", option.Dest.EcdsaPemFile, "ecdsa pem file") 81 | rootCmd.PersistentFlags().StringVar(&option.Dest.EcdsaPemFilePasswd, "dest-ecdsa-passwd", option.Dest.EcdsaPemFilePasswd, "ecdsa pem file passwd") 82 | rootCmd.PersistentFlags().StringVar(&option.Dest.RemoteGitlabAddr, "dest-remote-gitlab-addr", option.Dest.RemoteGitlabAddr, "remote gitlab addr") 83 | rootCmd.PersistentFlags().StringVar(&option.Dest.User, "dest-user", option.Dest.User, "user name") 84 | rootCmd.PersistentFlags().StringVar(&option.Dest.Token, "dest-token", option.Dest.Token, "token or private key") 85 | rootCmd.PersistentFlags().StringVar(&option.Dest.Proto, "dest-proto", option.Dest.Proto, "proto: git, ssh, http or https") 86 | 87 | } 88 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kom0055/git-mirror 2 | 3 | go 1.20 4 | 5 | require ( 6 | gitee.com/openeuler/go-gitee v0.0.0-20220530104019-3af895bc380c 7 | github.com/go-git/go-git/v5 v5.7.0 8 | github.com/google/go-github/v47 v47.1.0 9 | github.com/google/uuid v1.3.0 10 | github.com/hashicorp/go-multierror v1.1.1 11 | github.com/kom0055/go-flinx v0.0.3 12 | github.com/spf13/cobra v1.7.0 13 | github.com/xanzy/go-gitlab v0.86.0 14 | golang.org/x/crypto v0.10.0 15 | golang.org/x/oauth2 v0.9.0 16 | golang.org/x/time v0.3.0 17 | ) 18 | 19 | require ( 20 | github.com/Microsoft/go-winio v0.6.1 // indirect 21 | github.com/ProtonMail/go-crypto v0.0.0-20230626094100-7e9e0395ebec // indirect 22 | github.com/acomagu/bufpipe v1.0.4 // indirect 23 | github.com/antihax/optional v1.0.0 // indirect 24 | github.com/cloudflare/circl v1.3.3 // indirect 25 | github.com/emirpasic/gods v1.18.1 // indirect 26 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 27 | github.com/go-git/go-billy/v5 v5.4.1 // indirect 28 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 29 | github.com/golang/protobuf v1.5.3 // indirect 30 | github.com/google/go-querystring v1.1.0 // indirect 31 | github.com/hashicorp/errwrap v1.0.0 // indirect 32 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 33 | github.com/hashicorp/go-retryablehttp v0.7.4 // indirect 34 | github.com/imdario/mergo v0.3.16 // indirect 35 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 36 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 37 | github.com/kevinburke/ssh_config v1.2.0 // indirect 38 | github.com/pjbgf/sha1cd v0.3.0 // indirect 39 | github.com/sergi/go-diff v1.3.1 // indirect 40 | github.com/skeema/knownhosts v1.1.1 // indirect 41 | github.com/spf13/pflag v1.0.5 // indirect 42 | github.com/xanzy/ssh-agent v0.3.3 // indirect 43 | golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect 44 | golang.org/x/mod v0.11.0 // indirect 45 | golang.org/x/net v0.11.0 // indirect 46 | golang.org/x/sys v0.9.0 // indirect 47 | golang.org/x/tools v0.10.0 // indirect 48 | google.golang.org/appengine v1.6.7 // indirect 49 | google.golang.org/protobuf v1.31.0 // indirect 50 | gopkg.in/warnings.v0 v0.1.2 // indirect 51 | ) 52 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | gitee.com/openeuler/go-gitee v0.0.0-20220530104019-3af895bc380c h1:miClKCIA2Zyqif+Mf0GOdd/1u2q2wW7/vVuHpQprwDw= 3 | gitee.com/openeuler/go-gitee v0.0.0-20220530104019-3af895bc380c/go.mod h1:qGJhn1KxC5UE4BUmxCE/hTpFfuKbd3U3V9fNROrspfE= 4 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 5 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 6 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 7 | github.com/ProtonMail/go-crypto v0.0.0-20230626094100-7e9e0395ebec h1:vV3RryLxt42+ZIVOFbYJCH1jsZNTNmj2NYru5zfx+4E= 8 | github.com/ProtonMail/go-crypto v0.0.0-20230626094100-7e9e0395ebec/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= 9 | github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= 10 | github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= 11 | github.com/ahmetb/go-linq/v3 v3.2.0 h1:BEuMfp+b59io8g5wYzNoFe9pWPalRklhlhbiU3hYZDE= 12 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 13 | github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= 14 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 15 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 16 | github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 17 | github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= 18 | github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= 19 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 20 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 21 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 23 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0= 25 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 26 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 27 | github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= 28 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 29 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 30 | github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= 31 | github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= 32 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8= 33 | github.com/go-git/go-git/v5 v5.7.0 h1:t9AudWVLmqzlo+4bqdf7GY+46SUuRsx59SboFxkq2aE= 34 | github.com/go-git/go-git/v5 v5.7.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8= 35 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 36 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 37 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 38 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 39 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 40 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 41 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 42 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 43 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 44 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 45 | github.com/google/go-github/v47 v47.1.0 h1:Cacm/WxQBOa9lF0FT0EMjZ2BWMetQ1TQfyurn4yF1z8= 46 | github.com/google/go-github/v47 v47.1.0/go.mod h1:VPZBXNbFSJGjyjFRUKo9vZGawTajnWzC/YjGw/oFKi0= 47 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 48 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 49 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 50 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 51 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 52 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 53 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 54 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 55 | github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= 56 | github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 57 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 58 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 59 | github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= 60 | github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= 61 | github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= 62 | github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= 63 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 64 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 65 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 66 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 67 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 68 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 69 | github.com/kom0055/go-flinx v0.0.3 h1:RvNbDpdA/Ij8+tCzrOMyeY6sV+bO1mfmcTauMLi0+r8= 70 | github.com/kom0055/go-flinx v0.0.3/go.mod h1:NmXN3rwgEc9ti/sMZsqrvtnLIMWzZ5Tute2nKaOVph0= 71 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 72 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 73 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 74 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 75 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 76 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 77 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 78 | github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= 79 | github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= 80 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 81 | github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= 82 | github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= 83 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 84 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 85 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 86 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 87 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 88 | github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= 89 | github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= 90 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 91 | github.com/skeema/knownhosts v1.1.1 h1:MTk78x9FPgDFVFkDLTrsnnfCJl7g1C/nnKvePgrIngE= 92 | github.com/skeema/knownhosts v1.1.1/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= 93 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 94 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 95 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 96 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 97 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 98 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 99 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 100 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 101 | github.com/xanzy/go-gitlab v0.86.0 h1:jR8V9cK9jXRQDb46KOB20NCF3ksY09luaG0IfXE6p7w= 102 | github.com/xanzy/go-gitlab v0.86.0/go.mod h1:5ryv+MnpZStBH8I/77HuQBsMbBGANtVpLWC15qOjWAw= 103 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 104 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 105 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 106 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 107 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 108 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 109 | golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 110 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 111 | golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= 112 | golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= 113 | golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= 114 | golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= 115 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 116 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 117 | golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= 118 | golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 119 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 120 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 121 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 122 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 123 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 124 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 125 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 126 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 127 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 128 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 129 | golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= 130 | golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= 131 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 132 | golang.org/x/oauth2 v0.9.0 h1:BPpt2kU7oMRq3kCHAA1tbSEshXRw1LpG2ztgDwrzuAs= 133 | golang.org/x/oauth2 v0.9.0/go.mod h1:qYgFZaFiu6Wg24azG8bdV52QJXJGbZzIIsRCdVKzbLw= 134 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 135 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 136 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 137 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 138 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 139 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 140 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 141 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 142 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 143 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 144 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 145 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 146 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 147 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 148 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 149 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 150 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 151 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 152 | golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= 153 | golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 154 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 155 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 156 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 157 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 158 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 159 | golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= 160 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 161 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 162 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 163 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 164 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 165 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 166 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 167 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 168 | golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= 169 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 170 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 171 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 172 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 173 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 174 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 175 | golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= 176 | golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= 177 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 178 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 179 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 180 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 181 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 182 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 183 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 184 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 185 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 186 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 187 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 188 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 189 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 190 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 191 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 192 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 193 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 194 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 195 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 196 | gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= 197 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Kun 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | package main 23 | 24 | import "github.com/kom0055/git-mirror/cmd" 25 | 26 | func main() { 27 | cmd.Execute() 28 | } 29 | -------------------------------------------------------------------------------- /pkg/mirror/auth.go: -------------------------------------------------------------------------------- 1 | package mirror 2 | 3 | import ( 4 | "fmt" 5 | "github.com/go-git/go-git/v5/plumbing/transport" 6 | "github.com/go-git/go-git/v5/plumbing/transport/http" 7 | "github.com/go-git/go-git/v5/plumbing/transport/ssh" 8 | 9 | "github.com/kom0055/git-mirror/pkg/utils" 10 | ) 11 | 12 | func buildAuth(permFile, permFilePasswd, userName, token string, proto string) (transport.AuthMethod, error) { 13 | switch proto { 14 | case utils.HttpProto, utils.HttpsProto: 15 | return &http.BasicAuth{ 16 | Username: userName, 17 | Password: token, 18 | }, nil 19 | case utils.SshProto, utils.GitProto: 20 | publicKeys, err := ssh.NewPublicKeysFromFile(utils.GitUserName, permFile, permFilePasswd) 21 | if err != nil { 22 | return nil, err 23 | } 24 | publicKeys.HostKeyCallback = utils.IgnoreHostKeyCB 25 | return publicKeys, nil 26 | } 27 | return nil, fmt.Errorf("unsupport protocol %s", proto) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/mirror/cloner.go: -------------------------------------------------------------------------------- 1 | package mirror 2 | 3 | import ( 4 | "context" 5 | "github.com/go-git/go-git/v5" 6 | "github.com/go-git/go-git/v5/plumbing/transport" 7 | 8 | "github.com/kom0055/git-mirror/pkg/utils" 9 | ) 10 | 11 | type Cloner interface { 12 | Clone(ctx context.Context, localPath, repoUrl string) (*git.Repository, error) 13 | } 14 | 15 | func NewCloner(permFile, permFilePasswd, userName, token string, proto string) (Cloner, error) { 16 | auth, err := buildAuth(permFile, permFilePasswd, userName, token, proto) 17 | if err != nil { 18 | return nil, err 19 | } 20 | return &cloner{ 21 | auth: auth, 22 | }, nil 23 | } 24 | 25 | type cloner struct { 26 | auth transport.AuthMethod 27 | } 28 | 29 | func (s *cloner) Clone(ctx context.Context, localPath, repoUrl string) (*git.Repository, error) { 30 | 31 | repo, err := git.PlainCloneContext(ctx, localPath, utils.IsBare, &git.CloneOptions{ 32 | URL: repoUrl, 33 | Auth: s.auth, 34 | Tags: git.AllTags, 35 | Mirror: utils.IsMirror, 36 | }) 37 | 38 | if err != nil { 39 | return nil, err 40 | } 41 | return repo, nil 42 | 43 | } 44 | -------------------------------------------------------------------------------- /pkg/mirror/pusher.go: -------------------------------------------------------------------------------- 1 | package mirror 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/go-git/go-git/v5" 7 | gitconfig "github.com/go-git/go-git/v5/config" 8 | "github.com/go-git/go-git/v5/plumbing/transport" 9 | 10 | "github.com/kom0055/git-mirror/pkg/utils" 11 | ) 12 | 13 | type Pusher interface { 14 | Push(ctx context.Context, repo *git.Repository, repoUrl string) error 15 | } 16 | 17 | func NewPusher(permFile, permFilePasswd, userName, token string, proto string) (Pusher, error) { 18 | auth, err := buildAuth(permFile, permFilePasswd, userName, token, proto) 19 | if err != nil { 20 | return nil, err 21 | } 22 | return &pusher{ 23 | auth: auth, 24 | }, nil 25 | } 26 | 27 | type pusher struct { 28 | auth transport.AuthMethod 29 | } 30 | 31 | func (s *pusher) Push(ctx context.Context, repo *git.Repository, url string) error { 32 | 33 | remote, err := repo.CreateRemote(&gitconfig.RemoteConfig{ 34 | Name: utils.DestRemoteName, 35 | URLs: []string{url}, 36 | Mirror: utils.IsMirror, 37 | }) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | if err := remote.PushContext(ctx, &git.PushOptions{ 43 | RefSpecs: []gitconfig.RefSpec{"+refs/tags/*:refs/tags/*", "+refs/heads/*:refs/heads/*", "+refs/merge-requests/*:refs/merge-requests/*"}, 44 | RemoteName: utils.DestRemoteName, 45 | Auth: s.auth, 46 | Force: true, 47 | Atomic: true, 48 | }); err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { 49 | return err 50 | } 51 | 52 | return nil 53 | 54 | } 55 | -------------------------------------------------------------------------------- /pkg/options/.gitignore: -------------------------------------------------------------------------------- 1 | cases_test.go -------------------------------------------------------------------------------- /pkg/options/option.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/go-git/go-git/v5/plumbing/transport" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | "sync" 13 | "sync/atomic" 14 | 15 | "github.com/hashicorp/go-multierror" 16 | 17 | "github.com/kom0055/git-mirror/pkg/mirror" 18 | "github.com/kom0055/git-mirror/pkg/remote" 19 | "github.com/kom0055/git-mirror/pkg/utils" 20 | ) 21 | 22 | type Option struct { 23 | Worker int 24 | Source BasicOpt 25 | Dest BasicOpt 26 | } 27 | 28 | type BasicOpt struct { 29 | EcdsaPemFile string 30 | EcdsaPemFilePasswd string 31 | RemoteGitlabAddr string 32 | User string 33 | Token string 34 | Proto string 35 | GroupName string 36 | } 37 | 38 | func (o *Option) Mirror(ctx context.Context) error { 39 | defer os.RemoveAll(utils.DefaultTmpPath) 40 | 41 | var ( 42 | source = o.Source 43 | dest = o.Dest 44 | ) 45 | cloner, err := mirror.NewCloner(source.EcdsaPemFile, source.EcdsaPemFilePasswd, source.User, source.Token, source.Proto) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | pusher, err := mirror.NewPusher(dest.EcdsaPemFile, dest.EcdsaPemFilePasswd, dest.User, dest.Token, dest.Proto) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | sourceRm, err := remote.NewRemote(ctx, source.RemoteGitlabAddr, source.Token, source.GroupName, source.Proto) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | destRm, err := remote.NewRemote(ctx, dest.RemoteGitlabAddr, dest.Token, dest.GroupName, dest.Proto) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | projects, err := sourceRm.FetchAllProjects(ctx) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | errCh := make(chan error, 1000) 71 | worker := o.Worker 72 | if worker < 1 { 73 | worker = 1 74 | } 75 | ctrl := make(chan struct{}, worker) 76 | utils.GoRoutine(func() { 77 | defer close(errCh) 78 | defer close(ctrl) 79 | wg := &sync.WaitGroup{} 80 | finished := &atomic.Int64{} 81 | 82 | mapMutex := &sync.RWMutex{} 83 | processingProjects := map[string]struct{}{} 84 | achieves := atomic.Int64{} 85 | total := int64(len(projects)) 86 | for i := range projects { 87 | wg.Add(1) 88 | ctrl <- struct{}{} 89 | project := projects[i] 90 | id := project.Identity() 91 | func() { 92 | mapMutex.Lock() 93 | defer mapMutex.Unlock() 94 | processingProjects[id] = struct{}{} 95 | }() 96 | utils.GoRoutine(func() { 97 | defer func() { 98 | delete(processingProjects, id) 99 | processingProjectArr := make([]string, 0, len(processingProjects)) 100 | for name := range processingProjects { 101 | processingProjectArr = append(processingProjectArr, name) 102 | } 103 | log.Printf("total: %v, remain: %v, processing: %v, %v", total, total-achieves.Add(1), len(processingProjects), processingProjectArr) 104 | }() 105 | defer func() { 106 | <-ctrl 107 | }() 108 | defer wg.Done() 109 | defer finished.Add(1) 110 | 111 | ep, err := transport.NewEndpoint(project.URL) 112 | if err != nil { 113 | errCh <- fmt.Errorf("parse ssh url %s failed: %s", project.URL, err) 114 | return 115 | } 116 | localPath := filepath.Join(utils.DefaultTmpPath, ep.Host, strings.TrimSuffix(ep.Path, ".git")) 117 | defer func() { 118 | _ = os.RemoveAll(localPath) 119 | }() 120 | 121 | _ = os.RemoveAll(localPath) 122 | log.Printf("clone %s/%s ", project.Namespace, project.Name) 123 | repo, err := cloner.Clone(ctx, localPath, project.URL) 124 | if err != nil { 125 | if errors.Is(err, transport.ErrEmptyRemoteRepository) { 126 | return 127 | } 128 | log.Printf("clone %s/%s failed: %s", project.Namespace, project.Name, err) 129 | errCh <- fmt.Errorf("clone %s/%s failed: %s", project.Namespace, project.Name, err) 130 | return 131 | } 132 | 133 | destRepoUrl, err := destRm.GetProjectUrl(ctx, project) 134 | if err != nil { 135 | log.Printf("get dest repo %s/%s url failed: %v", project.Namespace, project.Name, err) 136 | errCh <- fmt.Errorf("get dest repo %s/%s url failed: %v", project.Namespace, project.Name, err) 137 | return 138 | } 139 | 140 | log.Printf("push %v/%v to %s", project.Namespace, project.Name, destRepoUrl) 141 | if err := pusher.Push(ctx, repo, destRepoUrl); err != nil { 142 | log.Printf("push %s failed: %s", destRepoUrl, err) 143 | errCh <- fmt.Errorf("push %s failed: %s", destRepoUrl, err) 144 | return 145 | } 146 | }) 147 | } 148 | 149 | wg.Wait() 150 | }) 151 | 152 | var multiErr *multierror.Error 153 | for err := range errCh { 154 | multiErr = multierror.Append(multiErr, err) 155 | } 156 | 157 | return multiErr.ErrorOrNil() 158 | } 159 | -------------------------------------------------------------------------------- /pkg/options/option_test.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestClone(t *testing.T) { 9 | 10 | var ( 11 | gitlabOpt = BasicOpt{ 12 | EcdsaPemFile: "/Users/myuserr/.ssh/id_ecdsa", 13 | EcdsaPemFilePasswd: "", 14 | RemoteGitlabAddr: "http://gitlab.mydomain.com/", 15 | User: "user1", 16 | Token: "xxxx", 17 | Proto: "ssh", 18 | GroupName: "group1", 19 | } 20 | 21 | githubOpt = BasicOpt{ 22 | EcdsaPemFile: "/Users/myuser/.ssh/id_ecdsa", 23 | EcdsaPemFilePasswd: "", 24 | User: "user12", 25 | Token: "xxxx", 26 | Proto: "ssh", 27 | GroupName: "group2", 28 | } 29 | ) 30 | o := Option{ 31 | Source: gitlabOpt, 32 | Dest: githubOpt, 33 | Worker: 8, 34 | } 35 | ctx := context.Background() 36 | if err := o.Mirror(ctx); err != nil { 37 | t.Fatal(err) 38 | } 39 | } 40 | 41 | func TestClone2(t *testing.T) { 42 | 43 | var ( 44 | gitlabOpt = BasicOpt{ 45 | EcdsaPemFile: "", 46 | EcdsaPemFilePasswd: "", 47 | RemoteGitlabAddr: "http://gitlab.mydomain.com/", 48 | User: "xxx", 49 | Token: "xxx", 50 | Proto: "http", 51 | GroupName: "mygroup1", 52 | } 53 | 54 | githubOpt = BasicOpt{ 55 | EcdsaPemFile: "/Users/myuser/.ssh/id_ecdsa", 56 | EcdsaPemFilePasswd: "", 57 | User: "myuser", 58 | Token: "xxx", 59 | Proto: "ssh", 60 | GroupName: "mygroup2", 61 | } 62 | ) 63 | o := Option{ 64 | Worker: 8, 65 | Source: gitlabOpt, 66 | Dest: githubOpt, 67 | } 68 | ctx := context.Background() 69 | if err := o.Mirror(ctx); err != nil { 70 | t.Fatal(err) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pkg/remote/gitee.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | _ "gitee.com/openeuler/go-gitee/gitee" 5 | ) 6 | -------------------------------------------------------------------------------- /pkg/remote/github.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/kom0055/go-flinx" 7 | "golang.org/x/oauth2" 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/google/go-github/v47/github" 12 | 13 | "github.com/kom0055/git-mirror/pkg/utils" 14 | ) 15 | 16 | var ( 17 | privateVisibility = "private" 18 | ) 19 | 20 | func newGhImpl(ctx context.Context, token, orgName, proto string) (Remote, error) { 21 | tc := oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{ 22 | AccessToken: token, 23 | })) 24 | 25 | client := github.NewClient(tc) 26 | org, _, err := client.Organizations.Get(ctx, orgName) 27 | if err != nil || org == nil { 28 | return nil, fmt.Errorf("failed to get org %s: %v", orgName, err) 29 | } 30 | return &ghRemote{ 31 | proto: proto, 32 | org: org, 33 | client: client, 34 | }, nil 35 | } 36 | 37 | type ghRemote struct { 38 | proto string 39 | org *github.Organization 40 | client *github.Client 41 | } 42 | 43 | func (r *ghRemote) FetchAllProjects(ctx context.Context) ([]*Project, error) { 44 | 45 | allGithubProjects := []*github.Repository{} 46 | perPage := 50 47 | for i := 0; ; i++ { 48 | projects, _, err := r.client.Repositories.List(ctx, "", &github.RepositoryListOptions{ 49 | Sort: "created", 50 | Direction: "asc", 51 | ListOptions: github.ListOptions{ 52 | Page: i, 53 | PerPage: perPage, 54 | }, 55 | }) 56 | if err != nil { 57 | return nil, err 58 | } 59 | if len(projects) == 0 { 60 | break 61 | } 62 | allGithubProjects = append(allGithubProjects, projects...) 63 | } 64 | allProjects := flinx.ToSlice(flinx.Select(r.fromGithubRepo)(flinx.DistinctBy(func(t *github.Repository) int64 { 65 | return *t.ID 66 | })(flinx.FromSlice(allGithubProjects)))) 67 | 68 | return allProjects, nil 69 | 70 | } 71 | 72 | func (r *ghRemote) GetProjectUrl(ctx context.Context, project *Project) (string, error) { 73 | orgName := *r.org.Login 74 | repoName := strings.ReplaceAll(strings.ToLower(fmt.Sprintf("%s-%s", project.Namespace, project.Name)), " ", "-") 75 | repo, resp, err := r.client.Repositories.Get(ctx, orgName, repoName) 76 | if err != nil { 77 | if resp == nil || resp.StatusCode != http.StatusNotFound { 78 | return "", err 79 | 80 | } 81 | repo, _, err = r.client.Repositories.Create(ctx, orgName, &github.Repository{ 82 | Name: &repoName, 83 | Visibility: &privateVisibility, 84 | }) 85 | if err != nil { 86 | return "", err 87 | } 88 | } 89 | 90 | switch r.proto { 91 | case utils.HttpProto, utils.HttpsProto: 92 | return *repo.CloneURL, nil 93 | case utils.SshProto, utils.GitProto: 94 | return *repo.SSHURL, nil 95 | 96 | } 97 | return "", fmt.Errorf("unknown proto: %s", r.proto) 98 | 99 | } 100 | 101 | func (r *ghRemote) fromGithubRepo(repo *github.Repository) *Project { 102 | var ( 103 | repoUrl string 104 | ) 105 | switch r.proto { 106 | case utils.HttpProto, utils.HttpsProto: 107 | repoUrl = *repo.CloneURL 108 | case utils.SshProto, utils.GitProto: 109 | repoUrl = *repo.SSHURL 110 | 111 | } 112 | return &Project{ 113 | Name: *repo.Name, 114 | Namespace: *repo.Owner.Login, 115 | URL: repoUrl, 116 | RelativePath: *repo.FullName, 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /pkg/remote/gitlab.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/kom0055/go-flinx" 7 | _ "github.com/kom0055/go-flinx" 8 | "github.com/xanzy/go-gitlab" 9 | "golang.org/x/time/rate" 10 | "net/http" 11 | "strings" 12 | 13 | "github.com/kom0055/git-mirror/pkg/utils" 14 | ) 15 | 16 | var ( 17 | sortAsc = "asc" 18 | sortDesc = "desc" 19 | orderById = "id" 20 | orderByActivity = "last_activity_at" 21 | ) 22 | 23 | func newGlImpl(ctx context.Context, glUrl, token, groupName, proto string) (Remote, error) { 24 | client, err := gitlab.NewClient(token, gitlab.WithBaseURL(glUrl), gitlab.WithCustomLimiter(rate.NewLimiter(5, 3))) 25 | if err != nil { 26 | return nil, err 27 | } 28 | group, _, err := client.Groups.GetGroup(groupName, &gitlab.GetGroupOptions{}, gitlab.WithContext(ctx)) 29 | if err != nil || group == nil { 30 | return nil, fmt.Errorf("failed to get group %s: %v", groupName, err) 31 | } 32 | return &glRemote{ 33 | proto: proto, 34 | group: group, 35 | client: client, 36 | }, nil 37 | } 38 | 39 | type glRemote struct { 40 | proto string 41 | group *gitlab.Group 42 | client *gitlab.Client 43 | } 44 | 45 | func (r *glRemote) GetProjectUrl(ctx context.Context, project *Project) (string, error) { 46 | group := r.group 47 | 48 | repoName := strings.ReplaceAll(strings.ToLower(fmt.Sprintf("%s-%s", project.Namespace, project.Name)), " ", "-") 49 | repoPath := strings.ReplaceAll(strings.ToLower(fmt.Sprintf("%s/%s", group.Name, repoName)), " ", "-") 50 | repo, resp, err := r.client.Projects.GetProject(repoPath, &gitlab.GetProjectOptions{}, gitlab.WithContext(ctx)) 51 | if err != nil { 52 | if resp == nil || resp.StatusCode == http.StatusNotFound { 53 | return "", err 54 | 55 | } 56 | repo, _, err = r.client.Projects.CreateProject(&gitlab.CreateProjectOptions{ 57 | Name: &repoName, 58 | NamespaceID: &group.ID, 59 | 60 | Visibility: gitlab.Visibility(gitlab.PrivateVisibility), 61 | }, gitlab.WithContext(ctx)) 62 | if err != nil { 63 | return "", err 64 | } 65 | } 66 | 67 | switch r.proto { 68 | case utils.HttpProto, utils.HttpsProto: 69 | return repo.HTTPURLToRepo, nil 70 | case utils.SshProto, utils.GitProto: 71 | 72 | return repo.SSHURLToRepo, nil 73 | 74 | } 75 | return "", fmt.Errorf("unknown proto: %s", r.proto) 76 | 77 | } 78 | 79 | func (r *glRemote) FetchAllProjects(ctx context.Context) ([]*Project, error) { 80 | 81 | allGitlabProjects := []*gitlab.Project{} 82 | perPage := 50 83 | for i := 0; ; i++ { 84 | projects, _, err := r.client.Projects.ListProjects(&gitlab.ListProjectsOptions{ 85 | ListOptions: gitlab.ListOptions{ 86 | Page: i, 87 | PerPage: perPage, 88 | }, 89 | Sort: &sortAsc, 90 | OrderBy: &orderById, 91 | }, gitlab.WithContext(ctx)) 92 | if err != nil { 93 | return nil, err 94 | } 95 | if len(projects) == 0 { 96 | break 97 | } 98 | allGitlabProjects = append(allGitlabProjects, projects...) 99 | } 100 | 101 | allProjects := flinx.ToSlice(flinx.Select(r.fromGitlabProject)(flinx.DistinctBy(func(t *gitlab.Project) int { 102 | return t.ID 103 | })(flinx.FromSlice(allGitlabProjects)))) 104 | 105 | return allProjects, nil 106 | } 107 | 108 | func (r *glRemote) fromGitlabProject(repo *gitlab.Project) *Project { 109 | var ( 110 | repoUrl string 111 | ) 112 | switch r.proto { 113 | case utils.HttpProto, utils.HttpsProto: 114 | repoUrl = repo.HTTPURLToRepo 115 | case utils.SshProto, utils.GitProto: 116 | 117 | repoUrl = repo.SSHURLToRepo 118 | 119 | } 120 | 121 | return &Project{ 122 | Name: repo.Name, 123 | Namespace: repo.Namespace.Path, 124 | URL: repoUrl, 125 | RelativePath: repo.PathWithNamespace, 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /pkg/remote/remote.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | type Remote interface { 9 | FetchAllProjects(ctx context.Context) ([]*Project, error) 10 | GetProjectUrl(ctx context.Context, project *Project) (string, error) 11 | } 12 | 13 | func NewRemote(ctx context.Context, glUrl, token, org, proto string) (Remote, error) { 14 | if len(glUrl) == 0 { 15 | return newGhImpl(ctx, token, org, proto) 16 | } 17 | 18 | return newGlImpl(ctx, glUrl, token, org, proto) 19 | } 20 | 21 | type Project struct { 22 | Name string 23 | Namespace string 24 | URL string 25 | RelativePath string 26 | } 27 | 28 | func (p Project) Identity() string { 29 | return fmt.Sprintf("%s/%s", p.Namespace, p.Name) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/utils/bytes.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "unsafe" 4 | 5 | // BytesToString converts byte slice to string without a memory allocation. 6 | func BytesToString(b []byte) string { 7 | return *(*string)(unsafe.Pointer(&b)) 8 | } 9 | -------------------------------------------------------------------------------- /pkg/utils/consts.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/google/uuid" 6 | "os" 7 | ) 8 | 9 | const ( 10 | IsBare = true 11 | IsMirror = true 12 | GitUserName = "git" 13 | DestRemoteName = "dest" 14 | 15 | GitProto = "git" 16 | SshProto = "ssh" 17 | HttpProto = "http" 18 | HttpsProto = "https" 19 | 20 | TmpPathPattern = "repo" 21 | ) 22 | 23 | var ( 24 | DefaultTmpPath = fmt.Sprintf("%s/%s/%s", os.TempDir(), TmpPathPattern, uuid.NewString()) 25 | ) 26 | -------------------------------------------------------------------------------- /pkg/utils/guard.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log" 5 | "runtime/debug" 6 | ) 7 | 8 | func Guard() { 9 | if r := recover(); r != nil { 10 | log.Printf("recover panic: %+v, stack: %+v", 11 | r, BytesToString(debug.Stack())) 12 | } 13 | 14 | } 15 | 16 | func GoRoutine(method func()) { 17 | if method == nil { 18 | return 19 | } 20 | go func() { 21 | defer Guard() 22 | method() 23 | }() 24 | 25 | } 26 | -------------------------------------------------------------------------------- /pkg/utils/hashset.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type ( 4 | Empty struct{} 5 | Any[K comparable] map[K]Empty 6 | ) 7 | 8 | func NewAnySet[K comparable](items ...K) Any[K] { 9 | ss := Any[K]{} 10 | ss.Insert(items...) 11 | return ss 12 | } 13 | 14 | func AnyKeySet[K comparable, V any](theMap map[K]V) Any[K] { 15 | ret := NewAnySet[K]() 16 | 17 | for k := range theMap { 18 | ret.Insert(k) 19 | } 20 | return ret 21 | } 22 | 23 | func (s Any[K]) Insert(items ...K) Any[K] { 24 | for _, item := range items { 25 | s[item] = Empty{} 26 | } 27 | return s 28 | } 29 | 30 | func (s Any[K]) Delete(items ...K) Any[K] { 31 | for _, item := range items { 32 | delete(s, item) 33 | } 34 | return s 35 | } 36 | 37 | func (s Any[K]) Has(item K) bool { 38 | _, contained := s[item] 39 | return contained 40 | } 41 | 42 | func (s Any[K]) HasAll(items ...K) bool { 43 | for _, item := range items { 44 | if !s.Has(item) { 45 | return false 46 | } 47 | } 48 | return true 49 | } 50 | 51 | func (s Any[K]) HasAny(items ...K) bool { 52 | for _, item := range items { 53 | if s.Has(item) { 54 | return true 55 | } 56 | } 57 | return false 58 | } 59 | 60 | func (s Any[K]) Difference(s2 Any[K]) Any[K] { 61 | result := NewAnySet[K]() 62 | for key := range s { 63 | if !s2.Has(key) { 64 | result.Insert(key) 65 | } 66 | } 67 | return result 68 | } 69 | 70 | func (s Any[K]) Union(s2 Any[K]) Any[K] { 71 | result := NewAnySet[K]() 72 | for key := range s { 73 | result.Insert(key) 74 | } 75 | for key := range s2 { 76 | result.Insert(key) 77 | } 78 | return result 79 | } 80 | 81 | func (s Any[K]) Intersection(s2 Any[K]) Any[K] { 82 | var walk, other Any[K] 83 | result := NewAnySet[K]() 84 | if s.Len() < s2.Len() { 85 | walk = s 86 | other = s2 87 | } else { 88 | walk = s2 89 | other = s 90 | } 91 | for key := range walk { 92 | if other.Has(key) { 93 | result.Insert(key) 94 | } 95 | } 96 | return result 97 | } 98 | 99 | // IsSuperset returns true if and only if s1 is a superset of s2. 100 | func (s Any[K]) IsSuperset(s2 Any[K]) bool { 101 | for item := range s2 { 102 | if !s.Has(item) { 103 | return false 104 | } 105 | } 106 | return true 107 | } 108 | 109 | func (s Any[K]) Equal(s2 Any[K]) bool { 110 | return len(s) == len(s2) && s.IsSuperset(s2) 111 | } 112 | 113 | func (s Any[K]) List() []K { 114 | res := make([]K, 0, len(s)) 115 | for key := range s { 116 | res = append(res, key) 117 | } 118 | return res 119 | } 120 | 121 | func (s Any[K]) PopAny() (K, bool) { 122 | for key := range s { 123 | s.Delete(key) 124 | return key, true 125 | } 126 | var zeroValue K 127 | return zeroValue, false 128 | } 129 | 130 | func (s Any[K]) Len() int { 131 | return len(s) 132 | } 133 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "golang.org/x/crypto/ssh" 5 | "net" 6 | ) 7 | 8 | func IgnoreHostKeyCB(_ string, _ net.Addr, _ ssh.PublicKey) error { 9 | return nil 10 | } 11 | --------------------------------------------------------------------------------