feat: rewrite git.go and gitea.go
This commit is contained in:
parent
19b3442376
commit
02d022da54
4 changed files with 440 additions and 213 deletions
|
|
@ -3,198 +3,291 @@ package main
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/scornet256/go-logger"
|
"github.com/scornet256/go-logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// add a mutex to safely increment shared counters
|
// collect git operations results
|
||||||
var mu sync.Mutex
|
type GitOperationResult struct {
|
||||||
|
RepoName string
|
||||||
|
Operation string
|
||||||
|
Error error
|
||||||
|
ErrorType string
|
||||||
|
}
|
||||||
|
|
||||||
func checkoutRepositories(repositories []Repository) {
|
// collect git stats
|
||||||
|
type GitStats struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
clonedCount int
|
||||||
|
pulledCount int
|
||||||
|
errorCount int
|
||||||
|
pullErrorMsgUnstaged []string
|
||||||
|
pullErrorMsgUncommitted []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// increment counters
|
||||||
|
func (stats *GitStats) IncrementCounter(operation string, repoPath string) {
|
||||||
|
|
||||||
|
stats.mu.Lock()
|
||||||
|
defer stats.mu.Unlock()
|
||||||
|
|
||||||
|
switch operation {
|
||||||
|
case "cloned":
|
||||||
|
stats.clonedCount++
|
||||||
|
case "pulled":
|
||||||
|
stats.pulledCount++
|
||||||
|
case "error":
|
||||||
|
stats.errorCount++
|
||||||
|
case "unstaged":
|
||||||
|
stats.errorCount++
|
||||||
|
stats.pullErrorMsgUnstaged = append(stats.pullErrorMsgUnstaged, repoPath)
|
||||||
|
case "uncommitted":
|
||||||
|
stats.errorCount++
|
||||||
|
stats.pullErrorMsgUncommitted = append(stats.pullErrorMsgUncommitted, repoPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// concurrent git operations
|
||||||
|
func CheckoutRepositories(repositories []Repository, stats *GitStats) {
|
||||||
|
|
||||||
// create a waitgroup + semaphore channel
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
semaphore := make(chan struct{}, config.Concurrency)
|
semaphore := make(chan struct{}, config.Concurrency)
|
||||||
|
|
||||||
// manage all repositories found
|
|
||||||
for _, repo := range repositories {
|
for _, repo := range repositories {
|
||||||
|
|
||||||
// increment waitgroup counter + acquire semaphore slot
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
semaphore <- struct{}{}
|
semaphore <- struct{}{}
|
||||||
|
|
||||||
// start go routine per repo
|
|
||||||
go func(repo Repository) {
|
go func(repo Repository) {
|
||||||
|
|
||||||
// ensure we release the semaphore and close the goroutine
|
|
||||||
defer func() {
|
defer func() {
|
||||||
<-semaphore
|
<-semaphore
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// get repository name + create repo destination
|
result := processRepository(repo)
|
||||||
repoName := string(repo.PathWithNamespace)
|
handleResult(result, stats)
|
||||||
repoDestination := config.Destination + repoName
|
|
||||||
|
|
||||||
// log activity
|
|
||||||
logger.Print("Starting on repository: "+repoName, nil)
|
|
||||||
|
|
||||||
// make git url
|
|
||||||
url := fmt.Sprintf("https://%s-token:%s@%s/%s.git", config.GitBackend, config.GitToken, config.GitHost, repoName)
|
|
||||||
|
|
||||||
// check current status of repoDestination
|
|
||||||
checkRepo := func(repoDestination string) string {
|
|
||||||
checkCmd := exec.Command("git", "-C", repoDestination, "remote", "-v")
|
|
||||||
checkOutput, _ := checkCmd.CombinedOutput()
|
|
||||||
logger.Print("Checking status for repository: "+repoName, nil)
|
|
||||||
|
|
||||||
return string(checkOutput)
|
|
||||||
}
|
|
||||||
repoStatus := checkRepo(repoDestination)
|
|
||||||
|
|
||||||
// report error if not cloned or pulled repository
|
|
||||||
// clone repository if it does not exist
|
|
||||||
switch {
|
|
||||||
case strings.Contains(string(repoStatus), "No such file or directory"):
|
|
||||||
|
|
||||||
// log activity
|
|
||||||
logger.Print("Decided to clone repository: "+repoName, nil)
|
|
||||||
|
|
||||||
// clone the repo
|
|
||||||
cloneRepository := func(repoDestination string, url string) (string, error) {
|
|
||||||
cloneCmd := exec.Command("git", "clone", url, repoDestination)
|
|
||||||
cloneOutput, err := cloneCmd.CombinedOutput()
|
|
||||||
|
|
||||||
// set username and email
|
|
||||||
setGitUserName(repoName, repoDestination)
|
|
||||||
setGitUserMail(repoName, repoDestination)
|
|
||||||
|
|
||||||
logger.Print("Cloning repository: "+repoName+" to "+repoDestination, nil)
|
|
||||||
|
|
||||||
return string(cloneOutput), err
|
|
||||||
}
|
|
||||||
_, err := cloneRepository(repoDestination, url)
|
|
||||||
if err != nil {
|
|
||||||
logger.Print("ERROR: %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// set a lock, increment counters, update progressbar and unlock
|
|
||||||
mu.Lock()
|
|
||||||
clonedCount++
|
|
||||||
if !config.Debug {
|
|
||||||
_ = bar.Add(1)
|
|
||||||
}
|
|
||||||
mu.Unlock()
|
|
||||||
|
|
||||||
// pull the latest
|
|
||||||
case strings.Contains(string(repoStatus), url):
|
|
||||||
logger.Print("Decided to pull repository: "+repoName, nil)
|
|
||||||
pullRepository(repoName, repoDestination)
|
|
||||||
|
|
||||||
// set username and email
|
|
||||||
setGitUserName(repoName, repoDestination)
|
|
||||||
setGitUserMail(repoName, repoDestination)
|
|
||||||
|
|
||||||
if !config.Debug {
|
|
||||||
_ = bar.Add(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
logger.Print("ERROR: decided not to clone or pull repository: "+repoName, nil)
|
|
||||||
logger.Print("ERROR: this is why: "+repoStatus, nil)
|
|
||||||
|
|
||||||
// set a lock, increment counters and unlock
|
|
||||||
mu.Lock()
|
|
||||||
errorCount++
|
|
||||||
if !config.Debug {
|
|
||||||
_ = bar.Add(1)
|
|
||||||
}
|
|
||||||
mu.Unlock()
|
|
||||||
}
|
|
||||||
}(repo)
|
}(repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait for goroutines
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func pullRepository(repoName string, repoDestination string) {
|
// manage single repo
|
||||||
|
func processRepository(repo Repository) GitOperationResult {
|
||||||
|
repoName := string(repo.PathWithNamespace)
|
||||||
|
repoDestination := filepath.Join(config.Destination, repoName)
|
||||||
|
|
||||||
// log activity
|
logger.Print("Starting on repository: "+repoName, nil)
|
||||||
logger.Print("Pulling repository: "+repoName+" at "+repoDestination, nil)
|
|
||||||
|
|
||||||
// find remote
|
// check repo status
|
||||||
findRemote := func(repoDestination string) (string, error) {
|
status, err := checkRepositoryStatus(repoDestination)
|
||||||
remoteCmd := exec.Command("git", "-C", repoDestination, "remote", "show")
|
|
||||||
remoteOutput, err := remoteCmd.CombinedOutput()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("finding remote: %v", err)
|
return GitOperationResult{
|
||||||
|
RepoName: repoName,
|
||||||
|
Operation: "error",
|
||||||
|
Error: fmt.Errorf("checking repository status: %w", err),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Print("Finding remote for repository: "+repoName+" at "+repoDestination, nil)
|
gitURL := buildGitURL(repoName)
|
||||||
remote := strings.Split(strings.TrimSpace(string(remoteOutput)), "\n")[0]
|
|
||||||
logger.Print("Found remote; "+remote+" for repository: "+repoName+" at "+repoDestination, nil)
|
|
||||||
return remote, nil
|
|
||||||
}
|
|
||||||
remote, _ := findRemote(repoDestination)
|
|
||||||
|
|
||||||
// pull repository
|
|
||||||
pullCmd := exec.Command("git", "-C", repoDestination, "pull", remote)
|
|
||||||
pullOutput, err := pullCmd.CombinedOutput()
|
|
||||||
|
|
||||||
// set a lock, increment counters and unlock
|
|
||||||
mu.Lock()
|
|
||||||
pulledCount++
|
|
||||||
mu.Unlock()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
|
|
||||||
// set a lock, increment counters and unlock
|
|
||||||
mu.Lock()
|
|
||||||
errorCount++
|
|
||||||
pulledCount--
|
|
||||||
mu.Unlock()
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case strings.Contains(string(pullOutput), "You have unstaged changes"):
|
case strings.Contains(status, "No such file or directory"):
|
||||||
pullErrorMsgUnstaged = append(pullErrorMsgUnstaged, repoDestination)
|
return cloneRepository(repoName, repoDestination, gitURL)
|
||||||
logger.Print("Found unstaged changes in repository: "+repoName+" at "+repoDestination, nil)
|
case strings.Contains(status, gitURL):
|
||||||
|
return pullRepository(repoName, repoDestination)
|
||||||
case strings.Contains(string(pullOutput), "Your index contains uncommitted changes"):
|
|
||||||
pullErrorMsgUncommitted = append(pullErrorMsgUncommitted, repoDestination)
|
|
||||||
logger.Print("Found uncommitted changes in repository: "+repoName+" at "+repoDestination, nil)
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.Print("ERROR: pulling "+repoName, nil)
|
return GitOperationResult{
|
||||||
|
RepoName: repoName,
|
||||||
|
Operation: "error",
|
||||||
|
Error: fmt.Errorf("unexpected repository status: %s", status),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// log activity
|
|
||||||
logger.Print("Pulled repository: "+repoName+" at "+repoDestination, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// function to set the git user name
|
// check repo status
|
||||||
func setGitUserName(repoName string, repoDestination string) {
|
func checkRepositoryStatus(repoDestination string) (string, error) {
|
||||||
|
cmd := exec.Command("git", "-C", repoDestination, "remote", "-v")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
|
// If directory doesn't exist, that's expected for new clones
|
||||||
|
if err != nil && strings.Contains(string(output), "No such file or directory") {
|
||||||
|
return string(output), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(output), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// craft git url with auth
|
||||||
|
func buildGitURL(repoName string) string {
|
||||||
|
return fmt.Sprintf("https://%s-token:%s@%s/%s.git",
|
||||||
|
config.GitBackend, config.GitToken, config.GitHost, repoName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clone new repository
|
||||||
|
func cloneRepository(repoName, repoDestination, gitURL string) GitOperationResult {
|
||||||
|
logger.Print("Cloning repository: "+repoName, nil)
|
||||||
|
|
||||||
|
cmd := exec.Command("git", "clone", gitURL, repoDestination)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
gitUserNameCmd := exec.Command("git", "-C", repoDestination, "config", "user.name", config.GitUserName)
|
|
||||||
_, err := gitUserNameCmd.CombinedOutput()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Print("ERROR: %v\n", err)
|
return GitOperationResult{
|
||||||
|
RepoName: repoName,
|
||||||
|
Operation: "error",
|
||||||
|
Error: fmt.Errorf("cloning repository: %w, output: %s", err, string(output)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Print("Setting git username for: "+repoName, nil)
|
// set git user config
|
||||||
|
if err := setGitUserConfig(repoName, repoDestination); err != nil {
|
||||||
|
logger.Print("WARNING: failed to set git user config: "+err.Error(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return GitOperationResult{
|
||||||
|
RepoName: repoName,
|
||||||
|
Operation: "cloned",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// function to set the git user mail
|
// pull repo
|
||||||
func setGitUserMail(repoName string, repoDestination string) {
|
func pullRepository(repoName, repoDestination string) GitOperationResult {
|
||||||
|
logger.Print("Pulling repository: "+repoName, nil)
|
||||||
|
|
||||||
gitUserMailCmd := exec.Command("git", "-C", repoDestination, "config", "user.mail", config.GitUserMail)
|
// Find remote
|
||||||
_, err := gitUserMailCmd.CombinedOutput()
|
remote, err := findRemote(repoDestination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Print("ERROR: %v\n", err)
|
return GitOperationResult{
|
||||||
|
RepoName: repoName,
|
||||||
|
Operation: "error",
|
||||||
|
Error: fmt.Errorf("finding remote: %w", err),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Print("Setting git email for: "+repoName, nil)
|
// pull changes
|
||||||
|
cmd := exec.Command("git", "-C", repoDestination, "pull", remote)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
errorType := classifyPullError(string(output))
|
||||||
|
return GitOperationResult{
|
||||||
|
RepoName: repoName,
|
||||||
|
Operation: "error",
|
||||||
|
Error: fmt.Errorf("pulling repository: %w, output: %s", err, string(output)),
|
||||||
|
ErrorType: errorType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set git user configuration
|
||||||
|
if err := setGitUserConfig(repoName, repoDestination); err != nil {
|
||||||
|
logger.Print("WARNING: failed to set git user config: "+err.Error(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return GitOperationResult{
|
||||||
|
RepoName: repoName,
|
||||||
|
Operation: "pulled",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// find remote for repo
|
||||||
|
func findRemote(repoDestination string) (string, error) {
|
||||||
|
cmd := exec.Command("git", "-C", repoDestination, "remote", "show")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("getting remote: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
remotes := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||||
|
if len(remotes) == 0 {
|
||||||
|
return "", fmt.Errorf("no remotes found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return remotes[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// manage pull error
|
||||||
|
func classifyPullError(output string) string {
|
||||||
|
switch {
|
||||||
|
case strings.Contains(output, "You have unstaged changes"):
|
||||||
|
return "unstaged"
|
||||||
|
case strings.Contains(output, "Your index contains uncommitted changes"):
|
||||||
|
return "uncommitted"
|
||||||
|
default:
|
||||||
|
return "other"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set git user config
|
||||||
|
func setGitUserConfig(repoName, repoDestination string) error {
|
||||||
|
|
||||||
|
// git user name
|
||||||
|
if err := setGitUserName(repoName, repoDestination); err != nil {
|
||||||
|
return fmt.Errorf("setting username: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// git user mail
|
||||||
|
if err := setGitUserEmail(repoName, repoDestination); err != nil {
|
||||||
|
return fmt.Errorf("setting email: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// set git user name
|
||||||
|
func setGitUserName(repoName, repoDestination string) error {
|
||||||
|
|
||||||
|
cmd := exec.Command("git", "-C", repoDestination, "config", "user.name", config.GitUserName)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("setting git username: %w, output: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Print("Set git username for: "+repoName, nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// set git user mail
|
||||||
|
func setGitUserEmail(repoName, repoDestination string) error {
|
||||||
|
|
||||||
|
cmd := exec.Command("git", "-C", repoDestination, "config", "user.email", config.GitUserMail)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("setting git email: %w, output: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Print("Set git email for: "+repoName, nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// manage results
|
||||||
|
func handleResult(result GitOperationResult, stats *GitStats) {
|
||||||
|
|
||||||
|
switch result.Operation {
|
||||||
|
case "cloned":
|
||||||
|
stats.IncrementCounter("cloned", "")
|
||||||
|
logger.Print("Successfully cloned: "+result.RepoName, nil)
|
||||||
|
|
||||||
|
case "pulled":
|
||||||
|
stats.IncrementCounter("pulled", "")
|
||||||
|
logger.Print("Successfully pulled: "+result.RepoName, nil)
|
||||||
|
|
||||||
|
case "error":
|
||||||
|
if result.ErrorType == "unstaged" {
|
||||||
|
stats.IncrementCounter("unstaged", result.RepoName)
|
||||||
|
logger.Print("Found unstaged changes in: "+result.RepoName, nil)
|
||||||
|
} else if result.ErrorType == "uncommitted" {
|
||||||
|
stats.IncrementCounter("uncommitted", result.RepoName)
|
||||||
|
logger.Print("Found uncommitted changes in: "+result.RepoName, nil)
|
||||||
|
} else {
|
||||||
|
stats.IncrementCounter("error", "")
|
||||||
|
logger.Print("ERROR processing "+result.RepoName+": "+result.Error.Error(), nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update progress bar
|
||||||
|
if !config.Debug {
|
||||||
|
_ = bar.Add(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,99 +1,238 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/scornet256/go-logger"
|
"github.com/scornet256/go-logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func fetchRepositoriesGitea() ([]Repository, error) {
|
// giteaClient struct
|
||||||
|
type GiteaClient struct {
|
||||||
|
httpClient *http.Client
|
||||||
|
baseURL string
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
type GiteaRepository struct {
|
// gitea repo information
|
||||||
|
type GiteaRepository struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
FullName string `json:"full_name"`
|
FullName string `json:"full_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// gitea api options
|
||||||
|
type GiteaAPIOptions struct {
|
||||||
|
Visibility string
|
||||||
|
IncludeArchived string
|
||||||
|
Sort string
|
||||||
|
Limit int
|
||||||
|
Page int
|
||||||
|
}
|
||||||
|
|
||||||
|
// gitea api clien
|
||||||
|
func NewGiteaClient(baseURL, token string) *GiteaClient {
|
||||||
|
return &GiteaClient{
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
baseURL: baseURL,
|
||||||
|
token: token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch gitea repos
|
||||||
|
func FetchRepositoriesGitea() ([]Repository, error) {
|
||||||
|
client := NewGiteaClient(config.GitHost, config.GitToken)
|
||||||
|
|
||||||
|
options := GiteaAPIOptions{
|
||||||
|
Visibility: "all",
|
||||||
|
IncludeArchived: config.IncludeArchived,
|
||||||
|
Sort: "alpha",
|
||||||
|
Limit: 100,
|
||||||
|
Page: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
// default options
|
repositories, err := client.fetchAllRepositories(context.Background(), options)
|
||||||
visibility := "visibility=all"
|
|
||||||
perpage := "limit=100"
|
|
||||||
sort := "sort=alpha"
|
|
||||||
|
|
||||||
// configure archived options
|
|
||||||
var archived string
|
|
||||||
switch config.IncludeArchived {
|
|
||||||
case "excluded":
|
|
||||||
archived = "&archived=false"
|
|
||||||
case "only":
|
|
||||||
archived = "&archived=true"
|
|
||||||
default:
|
|
||||||
archived = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
url := fmt.Sprintf("https://%s/api/v1/user/repos?%s&%s&%s%s",
|
|
||||||
config.GitHost, visibility, sort, perpage, archived)
|
|
||||||
|
|
||||||
logger.Print("HTTP: Creating API request", nil)
|
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ERROR: creating request: %v", err)
|
return nil, fmt.Errorf("fetching repositories: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Print("HTTP: Adding Authorization header to API request", nil)
|
if len(repositories) == 0 {
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("token %s", config.GitToken))
|
return repositories, fmt.Errorf("no repositories found")
|
||||||
|
}
|
||||||
|
|
||||||
logger.Print("HTTP: Making request", nil)
|
// update progress bar
|
||||||
client := &http.Client{}
|
if err := updateProgressBar(len(repositories)); err != nil {
|
||||||
resp, err := client.Do(req)
|
logger.Print("WARNING: failed to update progress bar: "+err.Error(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Print(fmt.Sprintf("Successfully fetched %d repositories", len(repositories)), nil)
|
||||||
|
return repositories, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch all repos with pagination
|
||||||
|
func (c *GiteaClient) fetchAllRepositories(ctx context.Context, options GiteaAPIOptions) ([]Repository, error) {
|
||||||
|
var allRepositories []Repository
|
||||||
|
|
||||||
|
for {
|
||||||
|
giteaRepos, hasMore, err := c.fetchRepositoryPage(ctx, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ERROR: making request: %v", err)
|
return nil, fmt.Errorf("fetching page %d: %w", options.Page, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convert gitea repositories to repo type
|
||||||
|
repositories := convertGiteaRepositories(giteaRepos)
|
||||||
|
allRepositories = append(allRepositories, repositories...)
|
||||||
|
|
||||||
|
if !hasMore {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
options.Page++
|
||||||
|
}
|
||||||
|
|
||||||
|
return allRepositories, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch single page of repo
|
||||||
|
func (c *GiteaClient) fetchRepositoryPage(ctx context.Context, options GiteaAPIOptions) ([]GiteaRepository, bool, error) {
|
||||||
|
apiURL, err := c.buildAPIURL(options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, fmt.Errorf("building API URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("token %s", c.token))
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
logger.Print("Making API request to: "+apiURL, nil)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, fmt.Errorf("making request: %w", err)
|
||||||
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := resp.Body.Close(); err != nil {
|
if closeErr := resp.Body.Close(); closeErr != nil {
|
||||||
logger.Fatal("HTTP: Error closing response body", err)
|
logger.Print("WARNING: failed to close response body: "+closeErr.Error(), nil)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("ERROR: API request failed with status: %d", resp.StatusCode)
|
return nil, false, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, resp.Status)
|
||||||
}
|
}
|
||||||
logger.Print("HTTP: Decoding JSON response", nil)
|
|
||||||
|
|
||||||
// first decode into gitearepository slice
|
|
||||||
var giteaRepos []GiteaRepository
|
var giteaRepos []GiteaRepository
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&giteaRepos); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&giteaRepos); err != nil {
|
||||||
return nil, fmt.Errorf("ERROR: decoding response: %v", err)
|
return nil, false, fmt.Errorf("decoding JSON response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert to repository slice
|
// check for more pages
|
||||||
|
hasMore := len(giteaRepos) == options.Limit
|
||||||
|
|
||||||
|
return giteaRepos, hasMore, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// build final api url
|
||||||
|
func (c *GiteaClient) buildAPIURL(options GiteaAPIOptions) (string, error) {
|
||||||
|
baseURL := fmt.Sprintf("https://%s/api/v1/user/repos", c.baseURL)
|
||||||
|
|
||||||
|
u, err := url.Parse(baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parsing base URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := u.Query()
|
||||||
|
query.Set("visibility", options.Visibility)
|
||||||
|
query.Set("sort", options.Sort)
|
||||||
|
query.Set("limit", strconv.Itoa(options.Limit))
|
||||||
|
query.Set("page", strconv.Itoa(options.Page))
|
||||||
|
|
||||||
|
// handle archived
|
||||||
|
switch options.IncludeArchived {
|
||||||
|
case "excluded":
|
||||||
|
query.Set("archived", "false")
|
||||||
|
case "only":
|
||||||
|
query.Set("archived", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
u.RawQuery = query.Encode()
|
||||||
|
return u.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert gitea repos to repo type
|
||||||
|
func convertGiteaRepositories(giteaRepos []GiteaRepository) []Repository {
|
||||||
repositories := make([]Repository, len(giteaRepos))
|
repositories := make([]Repository, len(giteaRepos))
|
||||||
for repo, giteaRepo := range giteaRepos {
|
for i, giteaRepo := range giteaRepos {
|
||||||
repositories[repo] = Repository{
|
repositories[i] = Repository{
|
||||||
Name: giteaRepo.Name,
|
Name: giteaRepo.Name,
|
||||||
PathWithNamespace: giteaRepo.FullName,
|
PathWithNamespace: giteaRepo.FullName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return repositories
|
||||||
if len(repositories) < 1 {
|
}
|
||||||
return repositories, fmt.Errorf("ERROR: no repositories found")
|
|
||||||
}
|
// update progressbar
|
||||||
repoCount := len(repositories)
|
func updateProgressBar(repoCount int) error {
|
||||||
|
if config.Debug {
|
||||||
logger.Print("BAR: Resetting the progressbar", nil)
|
return nil // Skip progress bar in debug mode
|
||||||
if !config.Debug {
|
}
|
||||||
err = bar.Set(0)
|
|
||||||
if err != nil {
|
logger.Print("Resetting progress bar", nil)
|
||||||
logger.Fatal("Could not reset the progressbar", err)
|
if err := bar.Set(0); err != nil {
|
||||||
}
|
return fmt.Errorf("resetting progress bar: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Print("BAR: Increasing the max value of the progressbar", nil)
|
logger.Print("Setting progress bar maximum", nil)
|
||||||
if !config.Debug {
|
bar.ChangeMax(repoCount)
|
||||||
bar.ChangeMax(repoCount)
|
|
||||||
}
|
return nil
|
||||||
|
}
|
||||||
logger.Print("HTTP: Returning repositories found", nil)
|
|
||||||
return repositories, nil
|
// connection validation
|
||||||
|
func (c *GiteaClient) ValidateConnection(ctx context.Context) error {
|
||||||
|
apiURL := fmt.Sprintf("https://%s/api/v1/user", c.baseURL)
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating validation request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("token %s", c.token))
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("making validation request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusUnauthorized {
|
||||||
|
return fmt.Errorf("invalid or expired token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("API validation failed with status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// simply count git repos only
|
||||||
|
func (c *GiteaClient) GetRepositoryCount(ctx context.Context, options GiteaAPIOptions) (int, error) {
|
||||||
|
|
||||||
|
repos, err := c.fetchAllRepositories(ctx, options)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("counting repositories: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(repos), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,6 @@ import (
|
||||||
var version string
|
var version string
|
||||||
var config *Config
|
var config *Config
|
||||||
|
|
||||||
// keep count 🧛
|
|
||||||
var clonedCount int
|
|
||||||
var errorCount int
|
|
||||||
var pulledCount int
|
|
||||||
var pullErrorMsgUnstaged []string
|
|
||||||
var pullErrorMsgUncommitted []string
|
|
||||||
|
|
||||||
// repository data
|
// repository data
|
||||||
type Repository struct {
|
type Repository struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|
@ -58,7 +51,7 @@ func main() {
|
||||||
var repositories []Repository
|
var repositories []Repository
|
||||||
switch config.GitBackend {
|
switch config.GitBackend {
|
||||||
case "gitea":
|
case "gitea":
|
||||||
repositories, err = fetchRepositoriesGitea()
|
repositories, err = FetchRepositoriesGitea()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal("Fetching repositories failed", err)
|
logger.Fatal("Fetching repositories failed", err)
|
||||||
}
|
}
|
||||||
|
|
@ -72,8 +65,9 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// manage found repositories
|
// manage found repositories
|
||||||
checkoutRepositories(repositories)
|
stats := &GitStats{}
|
||||||
printSummary()
|
CheckoutRepositories(repositories, stats)
|
||||||
printPullErrorUnstaged(pullErrorMsgUnstaged)
|
printSummary(stats)
|
||||||
printPullErrorUncommitted(pullErrorMsgUncommitted)
|
printPullErrorUnstaged(stats)
|
||||||
|
printPullErrorUncommitted(stats)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,31 +35,32 @@ func progressBar() {
|
||||||
logger.Print("Initialize progressbar", nil)
|
logger.Print("Initialize progressbar", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func printSummary() {
|
func printSummary(stats *GitStats) {
|
||||||
|
|
||||||
|
// print stats
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
fmt.Printf(
|
fmt.Printf(
|
||||||
"Summary:\n"+
|
"Summary:\n"+
|
||||||
" Cloned repositories: %v\n"+
|
" Cloned repositories: %v\n"+
|
||||||
" Pulled repositories: %v\n"+
|
" Pulled repositories: %v\n"+
|
||||||
" Errors: %v\n",
|
" Errors: %v\n",
|
||||||
clonedCount,
|
stats.clonedCount,
|
||||||
pulledCount,
|
stats.pulledCount,
|
||||||
errorCount,
|
stats.errorCount,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func printPullErrorUnstaged(pullErrorMsgUnstaged []string) {
|
func printPullErrorUnstaged(stats *GitStats) {
|
||||||
if len(pullErrorMsgUnstaged) > 0 {
|
if len(stats.pullErrorMsgUnstaged) > 0 {
|
||||||
for _, repo := range pullErrorMsgUnstaged {
|
for _, repo := range stats.pullErrorMsgUnstaged {
|
||||||
fmt.Printf("❕%s has unstaged changes.\n", repo)
|
fmt.Printf("❕%s has unstaged changes.\n", repo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func printPullErrorUncommitted(pullErrorMsgUncommitted []string) {
|
func printPullErrorUncommitted(stats *GitStats) {
|
||||||
if len(pullErrorMsgUncommitted) > 0 {
|
if len(stats.pullErrorMsgUncommitted) > 0 {
|
||||||
for _, repo := range pullErrorMsgUncommitted {
|
for _, repo := range stats.pullErrorMsgUncommitted {
|
||||||
fmt.Printf("❕%s has uncommitted changes.\n", repo)
|
fmt.Printf("❕%s has uncommitted changes.\n", repo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue