mirror of
https://github.com/gabrie30/ghorg.git
synced 2025-08-09 15:57:09 +02:00
530 lines
16 KiB
Go
530 lines
16 KiB
Go
// Package cmd encapsulates the logic for all cli commands
|
|
package cmd
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gabrie30/ghorg/colorlog"
|
|
"github.com/gabrie30/ghorg/configs"
|
|
"github.com/gabrie30/ghorg/internal/bitbucket"
|
|
"github.com/gabrie30/ghorg/internal/github"
|
|
"github.com/gabrie30/ghorg/internal/gitlab"
|
|
"github.com/gabrie30/ghorg/internal/repo"
|
|
"github.com/korovkin/limiter"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
protocol string
|
|
path string
|
|
parentFolder string
|
|
branch string
|
|
token string
|
|
cloneType string
|
|
scmType string
|
|
bitbucketUsername string
|
|
namespace string
|
|
color string
|
|
baseURL string
|
|
concurrency string
|
|
outputDir string
|
|
skipArchived bool
|
|
backup bool
|
|
args []string
|
|
cloneErrors []string
|
|
cloneInfos []string
|
|
targetCloneSource string
|
|
)
|
|
|
|
func init() {
|
|
rootCmd.PersistentFlags().StringVarP(&color, "color", "", "", "GHORG_COLOR - toggles colorful output on/off (default on)")
|
|
rootCmd.AddCommand(cloneCmd)
|
|
cloneCmd.Flags().StringVar(&protocol, "protocol", "", "GHORG_CLONE_PROTOCOL - protocol to clone with, ssh or https, (default https)")
|
|
cloneCmd.Flags().StringVarP(&path, "path", "p", "", "GHORG_ABSOLUTE_PATH_TO_CLONE_TO - absolute path the ghorg_* directory will be created. Must end with / (default $HOME/Desktop/)")
|
|
cloneCmd.Flags().StringVarP(&branch, "branch", "b", "", "GHORG_BRANCH - branch left checked out for each repo cloned (default master)")
|
|
cloneCmd.Flags().StringVarP(&token, "token", "t", "", "GHORG_GITHUB_TOKEN/GHORG_GITLAB_TOKEN/GHORG_BITBUCKET_APP_PASSWORD - scm token to clone with")
|
|
cloneCmd.Flags().StringVarP(&bitbucketUsername, "bitbucket-username", "", "", "GHORG_BITBUCKET_USERNAME - bitbucket only: username associated with the app password")
|
|
cloneCmd.Flags().StringVarP(&scmType, "scm", "s", "", "GHORG_SCM_TYPE - type of scm used, github, gitlab or bitbucket (default github)")
|
|
// TODO: make gitlab terminology make sense https://about.gitlab.com/2016/01/27/comparing-terms-gitlab-github-bitbucket/
|
|
cloneCmd.Flags().StringVarP(&cloneType, "clone-type", "c", "", "GHORG_CLONE_TYPE - clone target type, user or org (default org)")
|
|
cloneCmd.Flags().StringVarP(&namespace, "namespace", "n", "", "GHORG_GITLAB_DEFAULT_NAMESPACE - gitlab only: limits clone targets to a specific namespace e.g. --namespace=gitlab-org/security-products")
|
|
cloneCmd.Flags().BoolVar(&skipArchived, "skip-archived", false, "GHORG_SKIP_ARCHIVED skips archived repos, github/gitlab only")
|
|
cloneCmd.Flags().BoolVar(&skipArchived, "preserve-dir", false, "GHORG_PRESERVE_DIRECTORY_STRUCTURE clones repos in a directory structure that matches gitlab namespaces eg company/unit/subunit/app would clone into *_ghorg/unit/subunit/app, gitlab only")
|
|
cloneCmd.Flags().BoolVar(&backup, "backup", false, "GHORG_BACKUP backup mode, clone as mirror, no working copy (ignores branch parameter)")
|
|
cloneCmd.Flags().StringVarP(&baseURL, "base-url", "", "", "GHORG_SCM_BASE_URL change SCM base url, for on self hosted instances (currently gitlab only, use format of https://git.mydomain.com/api/v3)")
|
|
cloneCmd.Flags().StringVarP(&concurrency, "concurrency", "", "", "GHORG_CONCURRENCY max goroutines to spin up while cloning (default 25)")
|
|
cloneCmd.Flags().StringVarP(&outputDir, "output-dir", "", "", "GHORG_OUTPUT_DIR name of directory repos will be cloned into, will force underscores and always append _ghorg (default {org/repo being cloned}_ghorg)")
|
|
|
|
}
|
|
|
|
var cloneCmd = &cobra.Command{
|
|
Use: "clone",
|
|
Short: "Clone user or org repos from GitHub, GitLab, or Bitbucket",
|
|
Long: `Clone user or org repos from GitHub, GitLab, or Bitbucket. See $HOME/ghorg/conf.yaml for defaults, its likely you will need to update some of these values of use the flags to overwrite them. Values are set first by a default value, then based off what is set in $HOME/ghorg/conf.yaml, finally the cli flags, which have the highest level of precedence.`,
|
|
Run: cloneFunc,
|
|
}
|
|
|
|
func cloneFunc(cmd *cobra.Command, argz []string) {
|
|
|
|
if cmd.Flags().Changed("color") {
|
|
colorToggle := cmd.Flag("color").Value.String()
|
|
if colorToggle == "on" {
|
|
os.Setenv("GHORG_COLOR", colorToggle)
|
|
} else {
|
|
os.Setenv("GHORG_COLOR", "off")
|
|
}
|
|
|
|
}
|
|
|
|
if len(argz) < 1 {
|
|
colorlog.PrintError("You must provide an org or user to clone")
|
|
os.Exit(1)
|
|
}
|
|
|
|
if cmd.Flags().Changed("path") {
|
|
absolutePath := ensureTrailingSlash(cmd.Flag("path").Value.String())
|
|
os.Setenv("GHORG_ABSOLUTE_PATH_TO_CLONE_TO", absolutePath)
|
|
}
|
|
|
|
if cmd.Flags().Changed("protocol") {
|
|
protocol := cmd.Flag("protocol").Value.String()
|
|
os.Setenv("GHORG_CLONE_PROTOCOL", protocol)
|
|
}
|
|
|
|
if cmd.Flags().Changed("branch") {
|
|
os.Setenv("GHORG_BRANCH", cmd.Flag("branch").Value.String())
|
|
}
|
|
|
|
if cmd.Flags().Changed("bitbucket-username") {
|
|
os.Setenv("GHORG_BITBUCKET_USERNAME", cmd.Flag("bitbucket-username").Value.String())
|
|
}
|
|
|
|
if cmd.Flags().Changed("namespace") {
|
|
os.Setenv("GHORG_GITLAB_DEFAULT_NAMESPACE", cmd.Flag("namespace").Value.String())
|
|
}
|
|
|
|
if cmd.Flags().Changed("clone-type") {
|
|
cloneType := strings.ToLower(cmd.Flag("clone-type").Value.String())
|
|
os.Setenv("GHORG_CLONE_TYPE", cloneType)
|
|
}
|
|
|
|
if cmd.Flags().Changed("scm") {
|
|
scmType := strings.ToLower(cmd.Flag("scm").Value.String())
|
|
os.Setenv("GHORG_SCM_TYPE", scmType)
|
|
}
|
|
|
|
if cmd.Flags().Changed("base-url") {
|
|
url := cmd.Flag("base-url").Value.String()
|
|
os.Setenv("GHORG_SCM_BASE_URL", url)
|
|
}
|
|
|
|
if cmd.Flags().Changed("concurrency") {
|
|
g := cmd.Flag("concurrency").Value.String()
|
|
os.Setenv("GHORG_CONCURRENCY", g)
|
|
}
|
|
|
|
if cmd.Flags().Changed("skip-archived") {
|
|
os.Setenv("GHORG_SKIP_ARCHIVED", "true")
|
|
}
|
|
|
|
if cmd.Flags().Changed("preserve-dir") {
|
|
os.Setenv("GHORG_PRESERVE_DIRECTORY_STRUCTURE", "true")
|
|
}
|
|
|
|
if cmd.Flags().Changed("backup") {
|
|
os.Setenv("GHORG_BACKUP", "true")
|
|
}
|
|
|
|
if cmd.Flags().Changed("output-dir") {
|
|
d := cmd.Flag("output-dir").Value.String()
|
|
os.Setenv("GHORG_OUTPUT_DIR", d)
|
|
}
|
|
|
|
configs.GetOrSetToken()
|
|
|
|
if cmd.Flags().Changed("token") {
|
|
if os.Getenv("GHORG_SCM_TYPE") == "github" {
|
|
os.Setenv("GHORG_GITHUB_TOKEN", cmd.Flag("token").Value.String())
|
|
} else if os.Getenv("GHORG_SCM_TYPE") == "gitlab" {
|
|
os.Setenv("GHORG_GITLAB_TOKEN", cmd.Flag("token").Value.String())
|
|
} else if os.Getenv("GHORG_SCM_TYPE") == "bitbucket" {
|
|
os.Setenv("GHORG_BITBUCKET_APP_PASSWORD", cmd.Flag("token").Value.String())
|
|
}
|
|
}
|
|
|
|
err := configs.VerifyTokenSet()
|
|
if err != nil {
|
|
colorlog.PrintError(err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
err = configs.VerifyConfigsSetCorrectly()
|
|
if err != nil {
|
|
colorlog.PrintError(err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
parseParentFolder(argz)
|
|
args = argz
|
|
targetCloneSource = argz[0]
|
|
|
|
CloneAllRepos()
|
|
}
|
|
|
|
// TODO: Figure out how to use go channels for this
|
|
func getAllOrgCloneUrls() ([]repo.Data, error) {
|
|
asciiTime()
|
|
PrintConfigs()
|
|
var repos []repo.Data
|
|
var err error
|
|
switch os.Getenv("GHORG_SCM_TYPE") {
|
|
case "github":
|
|
repos, err = github.GetOrgRepos(targetCloneSource)
|
|
case "gitlab":
|
|
repos, err = gitlab.GetOrgRepos(targetCloneSource)
|
|
case "bitbucket":
|
|
repos, err = bitbucket.GetOrgRepos(targetCloneSource)
|
|
default:
|
|
colorlog.PrintError("GHORG_SCM_TYPE not set or unsupported, also make sure its all lowercase")
|
|
os.Exit(1)
|
|
}
|
|
|
|
return repos, err
|
|
}
|
|
|
|
// TODO: Figure out how to use go channels for this
|
|
func getAllUserCloneUrls() ([]repo.Data, error) {
|
|
asciiTime()
|
|
PrintConfigs()
|
|
var repos []repo.Data
|
|
var err error
|
|
switch os.Getenv("GHORG_SCM_TYPE") {
|
|
case "github":
|
|
repos, err = github.GetUserRepos(targetCloneSource)
|
|
case "gitlab":
|
|
repos, err = gitlab.GetUserRepos(targetCloneSource)
|
|
case "bitbucket":
|
|
repos, err = bitbucket.GetUserRepos(targetCloneSource)
|
|
default:
|
|
colorlog.PrintError("GHORG_SCM_TYPE not set or unsupported, also make sure its all lowercase")
|
|
os.Exit(1)
|
|
}
|
|
|
|
return repos, err
|
|
}
|
|
|
|
func createDirIfNotExist() {
|
|
if _, err := os.Stat(os.Getenv("GHORG_ABSOLUTE_PATH_TO_CLONE_TO") + parentFolder + "_ghorg"); os.IsNotExist(err) {
|
|
err = os.MkdirAll(os.Getenv("GHORG_ABSOLUTE_PATH_TO_CLONE_TO"), 0700)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func repoExistsLocally(path string) bool {
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func getAppNameFromURL(url string) string {
|
|
withGit := strings.Split(url, "/")
|
|
appName := withGit[len(withGit)-1]
|
|
split := strings.Split(appName, ".")
|
|
return strings.Join(split[0:len(split)-1], ".")
|
|
}
|
|
|
|
func printRemainingMessages() {
|
|
if len(cloneInfos) > 0 {
|
|
fmt.Println()
|
|
colorlog.PrintInfo("============ Info ============")
|
|
fmt.Println()
|
|
for _, i := range cloneInfos {
|
|
colorlog.PrintInfo(i)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
if len(cloneErrors) > 0 {
|
|
fmt.Println()
|
|
colorlog.PrintError("============ Issues ============")
|
|
fmt.Println()
|
|
for _, e := range cloneErrors {
|
|
colorlog.PrintError(e)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
}
|
|
|
|
func readGhorgIgnore() ([]string, error) {
|
|
file, err := os.Open(configs.GhorgIgnoreLocation())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
|
|
var lines []string
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
if scanner.Text() != "" {
|
|
lines = append(lines, scanner.Text())
|
|
}
|
|
}
|
|
return lines, scanner.Err()
|
|
}
|
|
|
|
// CloneAllRepos clones all repos
|
|
func CloneAllRepos() {
|
|
// resc, errc, infoc := make(chan string), make(chan error), make(chan error)
|
|
|
|
var cloneTargets []repo.Data
|
|
var err error
|
|
|
|
if os.Getenv("GHORG_CLONE_TYPE") == "org" {
|
|
cloneTargets, err = getAllOrgCloneUrls()
|
|
} else if os.Getenv("GHORG_CLONE_TYPE") == "user" {
|
|
cloneTargets, err = getAllUserCloneUrls()
|
|
} else {
|
|
colorlog.PrintError("GHORG_CLONE_TYPE not set or unsupported")
|
|
os.Exit(1)
|
|
}
|
|
|
|
if err != nil {
|
|
colorlog.PrintError("Encountered an error, aborting")
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if len(cloneTargets) == 0 {
|
|
colorlog.PrintInfo("No repos found for " + os.Getenv("GHORG_SCM_TYPE") + " " + os.Getenv("GHORG_CLONE_TYPE") + ": " + targetCloneSource + ", check spelling and verify clone-type (user/org) is set correctly e.g. -c=user")
|
|
os.Exit(0)
|
|
}
|
|
|
|
// filter repos down based on ghorgignore if one exists
|
|
_, err = os.Stat(configs.GhorgIgnoreLocation())
|
|
if !os.IsNotExist(err) {
|
|
// Open the file parse each line and remove cloneTargets containing
|
|
toIgnore, err := readGhorgIgnore()
|
|
if err != nil {
|
|
colorlog.PrintError("Error parsing your ghorgignore, aborting")
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
colorlog.PrintInfo("Using ghorgignore, filtering repos down...")
|
|
fmt.Println("")
|
|
|
|
filteredCloneTargets := []repo.Data{}
|
|
var flag bool
|
|
for _, cloned := range cloneTargets {
|
|
flag = false
|
|
for _, ignore := range toIgnore {
|
|
if strings.Contains(cloned.URL, ignore) {
|
|
flag = true
|
|
}
|
|
}
|
|
|
|
if flag == false {
|
|
filteredCloneTargets = append(filteredCloneTargets, cloned)
|
|
}
|
|
}
|
|
|
|
cloneTargets = filteredCloneTargets
|
|
|
|
}
|
|
|
|
colorlog.PrintInfo(strconv.Itoa(len(cloneTargets)) + " repos found in " + targetCloneSource)
|
|
fmt.Println()
|
|
|
|
createDirIfNotExist()
|
|
|
|
l, err := strconv.Atoi(os.Getenv("GHORG_CONCURRENCY"))
|
|
|
|
if err != nil {
|
|
log.Fatal("Could not determine GHORG_CONCURRENCY")
|
|
}
|
|
|
|
limit := limiter.NewConcurrencyLimiter(l)
|
|
for _, target := range cloneTargets {
|
|
appName := getAppNameFromURL(target.URL)
|
|
branch := os.Getenv("GHORG_BRANCH")
|
|
repo := target
|
|
|
|
limit.Execute(func() {
|
|
|
|
path := appName
|
|
if repo.Path != "" && os.Getenv("GHORG_PRESERVE_DIRECTORY_STRUCTURE") == "true" {
|
|
path = repo.Path
|
|
}
|
|
|
|
repoDir := os.Getenv("GHORG_ABSOLUTE_PATH_TO_CLONE_TO") + parentFolder + "_ghorg" + "/" + path
|
|
|
|
if os.Getenv("GHORG_BACKUP") == "true" {
|
|
repoDir = os.Getenv("GHORG_ABSOLUTE_PATH_TO_CLONE_TO") + parentFolder + "_ghorg_backup" + "/" + path
|
|
}
|
|
|
|
if repoExistsLocally(repoDir) == true {
|
|
if os.Getenv("GHORG_BACKUP") == "true" {
|
|
cmd := exec.Command("git", "remote", "update")
|
|
cmd.Dir = repoDir
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
e := fmt.Sprintf("Could not update remotes in Repo: %s Error: %v", repo.URL, err)
|
|
cloneErrors = append(cloneErrors, e)
|
|
return
|
|
}
|
|
} else {
|
|
|
|
cmd := exec.Command("git", "checkout", branch)
|
|
cmd.Dir = repoDir
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
e := fmt.Sprintf("Could not checkout out %s, branch may not exist, no changes made Repo: %s Error: %v", branch, repo.URL, err)
|
|
cloneInfos = append(cloneInfos, e)
|
|
return
|
|
}
|
|
|
|
cmd = exec.Command("git", "clean", "-f", "-d")
|
|
cmd.Dir = repoDir
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
e := fmt.Sprintf("Problem running git clean: %s Error: %v", repo.URL, err)
|
|
cloneErrors = append(cloneErrors, e)
|
|
return
|
|
}
|
|
|
|
cmd = exec.Command("git", "reset", "--hard", "origin/"+branch)
|
|
cmd.Dir = repoDir
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
e := fmt.Sprintf("Problem resetting %s Repo: %s Error: %v", branch, repo.URL, err)
|
|
cloneErrors = append(cloneErrors, e)
|
|
return
|
|
}
|
|
|
|
// TODO: handle case where repo was removed, should not give user an error
|
|
cmd = exec.Command("git", "pull", "origin", branch)
|
|
cmd.Dir = repoDir
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
e := fmt.Sprintf("Problem trying to pull %v Repo: %s Error: %v", branch, repo.URL, err)
|
|
cloneErrors = append(cloneErrors, e)
|
|
return
|
|
}
|
|
}
|
|
} else {
|
|
// if https clone and github/gitlab add personal access token to url
|
|
|
|
args := []string{"clone", repo.CloneURL, repoDir}
|
|
if os.Getenv("GHORG_BACKUP") == "true" {
|
|
args = append(args, "--mirror")
|
|
}
|
|
|
|
cmd := exec.Command("git", args...)
|
|
err := cmd.Run()
|
|
|
|
if err != nil {
|
|
e := fmt.Sprintf("Problem trying to clone Repo: %s Error: %v", repo.URL, err)
|
|
cloneErrors = append(cloneErrors, e)
|
|
return
|
|
}
|
|
|
|
// TODO: make configs around remote name
|
|
// we clone with api-key in clone url
|
|
args = []string{"remote", "set-url", "origin", repo.URL}
|
|
cmd = exec.Command("git", args...)
|
|
cmd.Dir = repoDir
|
|
err = cmd.Run()
|
|
|
|
if err != nil {
|
|
e := fmt.Sprintf("Problem trying to set remote on Repo: %s Error: %v", repo.URL, err)
|
|
cloneErrors = append(cloneErrors, e)
|
|
return
|
|
}
|
|
}
|
|
|
|
colorlog.PrintSuccess("Success " + repo.URL)
|
|
})
|
|
|
|
}
|
|
|
|
limit.Wait()
|
|
|
|
printRemainingMessages()
|
|
|
|
// TODO: fix all these if else checks with ghorg_backups
|
|
if os.Getenv("GHORG_BACKUP") == "true" {
|
|
colorlog.PrintSuccess(fmt.Sprintf("Finished! %s%s_ghorg_backup", os.Getenv("GHORG_ABSOLUTE_PATH_TO_CLONE_TO"), parentFolder))
|
|
} else {
|
|
colorlog.PrintSuccess(fmt.Sprintf("Finished! %s%s_ghorg", os.Getenv("GHORG_ABSOLUTE_PATH_TO_CLONE_TO"), parentFolder))
|
|
}
|
|
}
|
|
|
|
func asciiTime() {
|
|
colorlog.PrintInfo(
|
|
`
|
|
+-+-+-+-+ +-+-+ +-+-+-+-+-+
|
|
|T|I|M|E| |T|O| |G|H|O|R|G|
|
|
+-+-+-+-+ +-+-+ +-+-+-+-+-+
|
|
`)
|
|
}
|
|
|
|
// PrintConfigs shows the user what is set before cloning
|
|
func PrintConfigs() {
|
|
colorlog.PrintInfo("*************************************")
|
|
colorlog.PrintInfo("* SCM : " + os.Getenv("GHORG_SCM_TYPE"))
|
|
colorlog.PrintInfo("* Type : " + os.Getenv("GHORG_CLONE_TYPE"))
|
|
colorlog.PrintInfo("* Protocol : " + os.Getenv("GHORG_CLONE_PROTOCOL"))
|
|
colorlog.PrintInfo("* Branch : " + os.Getenv("GHORG_BRANCH"))
|
|
colorlog.PrintInfo("* Location : " + os.Getenv("GHORG_ABSOLUTE_PATH_TO_CLONE_TO"))
|
|
colorlog.PrintInfo("* Concurrency : " + os.Getenv("GHORG_CONCURRENCY"))
|
|
if os.Getenv("GHORG_SCM_BASE_URL") != "" {
|
|
colorlog.PrintInfo("* Base URL : " + os.Getenv("GHORG_SCM_BASE_URL"))
|
|
}
|
|
if os.Getenv("GHORG_SKIP_ARCHIVED") == "true" {
|
|
colorlog.PrintInfo("* Skip Archived : " + os.Getenv("GHORG_SKIP_ARCHIVED"))
|
|
}
|
|
if os.Getenv("GHORG_BACKUP") == "true" {
|
|
colorlog.PrintInfo("* Backup : " + os.Getenv("GHORG_BACKUP"))
|
|
}
|
|
colorlog.PrintInfo("*************************************")
|
|
fmt.Println("")
|
|
}
|
|
|
|
func ensureTrailingSlash(path string) string {
|
|
if string(path[len(path)-1]) == "/" {
|
|
return path
|
|
}
|
|
|
|
return path + "/"
|
|
}
|
|
|
|
func addTokenToHTTPSCloneURL(url string, token string) string {
|
|
splitURL := strings.Split(url, "https://")
|
|
|
|
if os.Getenv("GHORG_SCM_TYPE") == "gitlab" {
|
|
return "https://oauth2:" + token + "@" + splitURL[1]
|
|
}
|
|
|
|
return "https://" + token + "@" + splitURL[1]
|
|
}
|
|
|
|
func parseParentFolder(argz []string) {
|
|
if os.Getenv("GHORG_OUTPUT_DIR") != "" {
|
|
parentFolder = strings.ReplaceAll(os.Getenv("GHORG_OUTPUT_DIR"), "-", "_")
|
|
return
|
|
}
|
|
|
|
pf := strings.ReplaceAll(argz[0], "-", "_")
|
|
parentFolder = strings.ToLower(pf)
|
|
}
|