From 3e9c0e82da30028d214fc7bc6143e3a4f359d5ba Mon Sep 17 00:00:00 2001 From: Simon Cornet Date: Wed, 1 Jan 2025 18:32:53 +0100 Subject: [PATCH] feat: initial commit --- .gitignore | 1 + go.mod | 13 ++++ go.sum | 15 +++++ main.go | 192 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 221 insertions(+) create mode 100644 .gitignore create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6ce0c5b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +gogitlabber diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8182878 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module gogitlabber + +go 1.23.1 + +require ( + github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/schollz/progressbar/v3 v3.17.1 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2850250 --- /dev/null +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg= +github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/schollz/progressbar/v3 v3.17.1 h1:bI1MTaoQO+v5kzklBjYNRQLoVpe0zbyRZNK6DFkVC5U= +github.com/schollz/progressbar/v3 v3.17.1/go.mod h1:RzqpnsPQNjUyIgdglUjRLgD7sVnxN1wpmBMV+UiEbL4= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..fad1630 --- /dev/null +++ b/main.go @@ -0,0 +1,192 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "strings" + "net/http" + "os" + "os/exec" + "github.com/k0kubun/go-ansi" + "github.com/schollz/progressbar/v3" +) + +type Repository struct { + Name string `json:"name"` + PathWithNamespace string `json:"path_with_namespace"` +} + +var repoDestinationPre string +var includeArchived string +var gitlabToken string +var gitlabHost string + +func main() { + + // load environment variables first, they will be overridden + // by argument flags if specified. + if err := loadEnvironmentVariables(); err != nil { + log.Fatalf("Error loading environment variables: %v", err) + } + + // require at least the destination argument + if len(os.Args) <= 1 { + fmt.Println("Usage: gogitlabber --destination=") + fmt.Println("Example: gogitlabber --destination=/tmp/repos") + os.Exit(1) + } + + // parse arguments + for _, arg := range os.Args[1:] { + switch { + + case strings.HasPrefix(arg, "--destination="): + repoDestinationPre = strings.TrimPrefix(arg, "--destination=") + + case strings.HasPrefix(arg, "--gitlab-api-token="): + gitlabToken = strings.TrimPrefix(arg, "--gitlab-api-token=") + + case strings.HasPrefix(arg, "--gitlab-url="): + gitlabHost = strings.TrimPrefix(arg, "--gitlab-url=") + + default: + fmt.Println("Usage: gogitlabber --destination=") + fmt.Println("Example: gogitlabber --destination=/tmp/repos") + os.Exit(1) + } + } + + // fail if destination is unknown + if repoDestinationPre == "" { + fmt.Println("Fatal: No destination found.") + fmt.Println("Example: gogitlabber --destination=/tmp/repos") + fmt.Println("Usage: gogitlabber --destination=/tmp/repos") + os.Exit(1) + } + + // fetch repository information + repositories, err := fetchRepositories() + if err != nil { + log.Fatalf("Error fetching repositories: %v", err) + } + + // manage found repositories + checkoutRepositories(repositories) +} + + +func loadEnvironmentVariables() error { + gitlabToken = os.Getenv("GITLAB_API_KEY") + if gitlabToken == "" { + return fmt.Errorf("GITLAB_API_KEY environment variable is not set") + } + + gitlabHost = os.Getenv("GITLAB_HOSTNAME") + if gitlabHost == "" { + return fmt.Errorf("GITLAB_HOSTNAME environment variable is not set") + } + return nil +} + + +func fetchRepositories() ([]Repository, error) { + + archived := "archived=false" + membership := "membership=true" + perpage := "per_page=100" + order := "order_by=name" + + url := fmt.Sprintf("https://%s/api/v4/projects?%s&%s&%s&%s", + gitlabHost, membership, order, archived, perpage) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + req.Header.Set("PRIVATE-TOKEN", gitlabToken) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("API request failed with status: %d", resp.StatusCode) + } + + var repositories []Repository + if err := json.NewDecoder(resp.Body).Decode(&repositories); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return repositories, nil +} + + +func checkoutRepositories(repositories []Repository) { + repoCount := len(repositories) + + fmt.Printf("Found %d repositories", repoCount) + + // make progressbar using: + // - github.com/k0kubun/go-ansi + // - github.com/schollz/progressbar/v3 + bar := progressbar.NewOptions( + repoCount, + progressbar.OptionSetWriter(ansi.NewAnsiStdout()), + progressbar.OptionEnableColorCodes(true), + progressbar.OptionShowCount(), + progressbar.OptionSetElapsedTime(true), + progressbar.OptionSetPredictTime(false), + progressbar.OptionSetWidth(20), + progressbar.OptionSetDescription("Getting your repositories..."), + progressbar.OptionSetTheme(progressbar.Theme{ + Saucer: "[green]=[reset]", + SaucerHead: "[green]>[reset]", + SaucerPadding: " ", + BarStart: "[", + BarEnd: "]", + }), + ) + + for _, repo := range repositories { + + repoName := string(repo.PathWithNamespace) + gitlabUrl := fmt.Sprintf("https://gitlab-token:%s@%s/%s.git", + gitlabToken, gitlabHost, repoName) + + repoDestination := repoDestinationPre + repoName + + cloneCmd := exec.Command("git", "clone", gitlabUrl, repoDestination) + cloneOutput, err := cloneCmd.CombinedOutput() + + if err != nil { + + // if repo already exists, try to pull the latest changes + if strings.Contains(string(cloneOutput), + "already exists and is not an empty directory") { + pullRepositories(repoDestination) + bar.Add(1) + continue + } + log.Printf("❌ Error cloning %s: %v\n%s", repoName, err, string(cloneOutput)) + bar.Add(1) + continue + } + bar.Add(1) + } +} + + +func pullRepositories(repoDestination string) { + pullCmd := exec.Command("git", "-C", repoDestination, "pull", "origin") + output, err := pullCmd.CombinedOutput() + + if err != nil { + log.Printf("❌ Error pulling %s: %v\n%s", repoDestination, err, string(output)) + } +}