├── .gitignore ├── go.mod ├── config.env ├── makefile ├── README.md └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.DS_Store 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sagarishere/cloneAllGitea 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /config.env: -------------------------------------------------------------------------------- 1 | # replace below line this with your own gitea host 2 | # Examples https://01.kood.tech/git , https://01.alem.school/git , https://learn.01founders.co/git , https://ytrack.learn.ynov.com/git , https://zero.academie.one/git , https://talent.uniworkhub.com/git , etc.. 3 | GITEA_HOST=https://01.gritlab.ax/git 4 | 5 | # to generate access token, go to setting, applications, generate new token. This has to be your own token and confidential to you 6 | GITEA_ACCESS_TOKEN=b4c1c82e1a7d3e8a2f0b4c9e5d2a7f8b3c9a6d5 7 | 8 | # this is the directory where you want to clone the repos 9 | TARGET_DIR=./gritlab 10 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # Makefile for building and running a Go project 2 | 3 | # Project-specific settings 4 | BINARY_NAME=cloneAllGitea 5 | BUILD_DIR=./bin 6 | SOURCE_DIR=. 7 | 8 | # Go build commands 9 | GO_BUILD=go build -ldflags "-s -w" 10 | GO_CLEAN=go clean 11 | 12 | # Makefile targets 13 | .PHONY: all build clean run 14 | 15 | all: build 16 | 17 | build: 18 | mkdir -p $(BUILD_DIR) 19 | $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY_NAME) $(SOURCE_DIR) 20 | cp $(BUILD_DIR)/$(BINARY_NAME) . 21 | 22 | run: build 23 | ./$(BINARY_NAME) 24 | 25 | clean: 26 | $(GO_CLEAN) 27 | rm -f $(BINARY_NAME) 28 | rm -rf $(BUILD_DIR) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gitea auto-backup 2 | 3 | This is script to backup all repositories from a Gitea server. 4 | 5 | ## Usage 6 | 7 | **First go to `config.env` and set the following variables:** 8 | >GITEA_HOST => The host of the Gitea server 9 | > 10 | >GITEA_ACCESS_TOKEN => The access token of the Gitea server. To generate access token, go to your profile in gitea, go to setting, applications, generate new token (make sure to note it down, as it will not be shown again) 11 | > 12 | >TARGET_DIR => The directory where the backups will be stored 13 | 14 | Sample Information is provided in the `config.env` file, you must change the values as per your requirement. 15 | Note: if confused, kindly write the issue, I will help you out. 16 | 17 | Then run the following command: 18 | 19 | ```bash 20 | go mod tidy && go run . 21 | ``` 22 | 23 | ### Filtering Repositories by Flags 24 | 25 | The script now supports filtering repositories with the following flags: 26 | 27 | - `--onlyme`: Backs up only the repositories owned by the user associated with the provided access token. 28 | 29 | Example usage: 30 | 31 | ```bash 32 | go mod tidy && go run . --onlyme 33 | ``` 34 | 35 | - `--user`: Backs up repositories owned by a specified username. This allows for backing up repositories of a specific user. 36 | 37 | Example usage: 38 | 39 | ```bash 40 | go mod tidy && go run . --user="username" 41 | ``` 42 | 43 | or 44 | 45 | ```bash 46 | go mod tidy && go run . --user username 47 | ``` 48 | 49 | whereas username is the username of the user whose repositories you want to backup. 50 | 51 | ## Author 52 | 53 | [👤 **Sagar Yadav**](https://www.linkedin.com/in/sagaryadav) 54 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "os" 11 | "os/exec" 12 | "strings" 13 | "sync" 14 | "time" 15 | ) 16 | 17 | const ( 18 | userReposEndpoint = "/api/v1/user/repos" 19 | timeout = 5 * time.Minute 20 | userEndpoint = "/api/v1/user" 21 | ) 22 | 23 | type Repository struct { 24 | Name string `json:"name"` 25 | CloneURL string `json:"clone_url"` 26 | FullName string `json:"full_name"` 27 | } 28 | 29 | type Result struct { 30 | RepoName string 31 | Err error 32 | } 33 | 34 | func main() { 35 | var ( 36 | onlyMe bool 37 | user string 38 | ) 39 | flag.BoolVar(&onlyMe, "onlyme", false, "Fetch repositories owned by the user only") 40 | flag.StringVar(&user, "user", "", "Specify a username to fetch their repositories") 41 | flag.Parse() 42 | 43 | config, err := loadConfig("config.env") 44 | if err != nil { 45 | fmt.Printf("Error loading config: %v\n", err) 46 | return 47 | } 48 | 49 | giteaHost := config["GITEA_HOST"] 50 | giteaAccessToken := config["GITEA_ACCESS_TOKEN"] 51 | targetDir := config["TARGET_DIR"] 52 | 53 | if _, err := os.Stat(targetDir); os.IsNotExist(err) { 54 | fmt.Printf("Creating target directory: %s\n", targetDir) 55 | os.MkdirAll(targetDir, os.ModePerm) 56 | } 57 | 58 | os.Chdir(targetDir) 59 | 60 | var username string 61 | if onlyMe { 62 | username, err = fetchUsername(giteaHost, giteaAccessToken) 63 | if err != nil { 64 | fmt.Printf("Error fetching user details: %v\n", err) 65 | return 66 | } 67 | } else if user != "" { 68 | username = user 69 | } 70 | 71 | repos, err := fetchRepositories(giteaHost, giteaAccessToken, username, onlyMe || user != "") 72 | if err != nil { 73 | fmt.Printf("Error fetching repositories: %v\n", err) 74 | return 75 | } 76 | 77 | fmt.Printf("Found %d repositories\n", len(repos)) 78 | 79 | resultsCh := make(chan Result, len(repos)) 80 | var wg sync.WaitGroup 81 | 82 | for _, repo := range repos { 83 | wg.Add(1) 84 | go func(repo Repository) { 85 | defer wg.Done() 86 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 87 | defer cancel() 88 | 89 | if _, err := os.Stat(repo.FullName); !os.IsNotExist(err) { 90 | fmt.Printf("Repo %s already exists, skipping.\n", repo.FullName) 91 | resultsCh <- Result{RepoName: repo.FullName, Err: nil} 92 | return 93 | } 94 | 95 | fmt.Printf("Cloning %s from %s\n", repo.Name, repo.CloneURL) 96 | err := gitClone(ctx, repo.CloneURL, repo.FullName) 97 | resultsCh <- Result{RepoName: repo.FullName, Err: err} 98 | }(repo) 99 | } 100 | 101 | go func() { 102 | wg.Wait() 103 | close(resultsCh) 104 | }() 105 | 106 | for res := range resultsCh { 107 | if res.Err != nil { 108 | fmt.Printf("Error cloning repository %s: %v\n", res.RepoName, res.Err) 109 | } 110 | } 111 | } 112 | 113 | func fetchRepositories(giteaHost, giteaAccessToken, username string, filterByUsername bool) ([]Repository, error) { 114 | var allRepos []Repository 115 | client := &http.Client{} 116 | page := 1 117 | for { 118 | req, err := http.NewRequest("GET", fmt.Sprintf("%s%s?page=%d", giteaHost, userReposEndpoint, page), nil) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | req.Header.Add("Authorization", "token "+giteaAccessToken) 124 | response, err := client.Do(req) 125 | if err != nil { 126 | return nil, err 127 | } 128 | defer response.Body.Close() 129 | 130 | if response.StatusCode != 200 { 131 | return nil, fmt.Errorf("API request failed with HTTP status code: %d", response.StatusCode) 132 | } 133 | 134 | body, err := io.ReadAll(response.Body) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | var repos []Repository 140 | json.Unmarshal(body, &repos) 141 | 142 | if len(repos) == 0 { 143 | break 144 | } 145 | 146 | if filterByUsername && username != "" { 147 | for _, repo := range repos { 148 | if strings.Split(repo.FullName, "/")[0] == username { 149 | allRepos = append(allRepos, repo) 150 | } 151 | } 152 | } else { 153 | allRepos = append(allRepos, repos...) 154 | } 155 | 156 | page++ 157 | } 158 | return allRepos, nil 159 | } 160 | 161 | func gitClone(ctx context.Context, cloneURL, addrToSave string) error { 162 | cmd := exec.CommandContext(ctx, "git", "clone", cloneURL, addrToSave) 163 | return cmd.Run() 164 | } 165 | 166 | func loadConfig(path string) (map[string]string, error) { 167 | configFile, err := os.ReadFile(path) 168 | if err != nil { 169 | return nil, err 170 | } 171 | 172 | config := make(map[string]string) 173 | lines := strings.Split(string(configFile), "\n") 174 | for _, line := range lines { 175 | if strings.TrimSpace(line) == "" || strings.HasPrefix(line, "#") { 176 | continue 177 | } 178 | 179 | parts := strings.SplitN(line, "=", 2) 180 | if len(parts) != 2 { 181 | return nil, fmt.Errorf("bad line in config file: %s", line) 182 | } 183 | config[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) 184 | } 185 | 186 | return config, nil 187 | } 188 | 189 | func fetchUsername(giteaHost, giteaAccessToken string) (string, error) { 190 | client := &http.Client{} 191 | req, err := http.NewRequest("GET", fmt.Sprintf("%s%s", giteaHost, userEndpoint), nil) 192 | if err != nil { 193 | return "", err 194 | } 195 | 196 | req.Header.Add("Authorization", "token "+giteaAccessToken) 197 | response, err := client.Do(req) 198 | if err != nil { 199 | return "", err 200 | } 201 | defer response.Body.Close() 202 | 203 | if response.StatusCode != 200 { 204 | return "", fmt.Errorf("failed to fetch user details with status code: %d", response.StatusCode) 205 | } 206 | 207 | var userDetails struct { 208 | Username string `json:"login"` 209 | } 210 | err = json.NewDecoder(response.Body).Decode(&userDetails) 211 | if err != nil { 212 | return "", err 213 | } 214 | 215 | return userDetails.Username, nil 216 | } 217 | --------------------------------------------------------------------------------