-
-
Save toidiu/c1e512bb0235e0c5c84b111f769ad2c3 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// github-backups is a program for saving a local copy of all the repositories | |
// you've created or starred on GitHub. | |
// | |
// It expects the "API_TOKEN" environment variable to be set to a valid oauth2 | |
// token so you can login using the github client. For convenience, you can use | |
// a `dotenv` file. | |
package main | |
import ( | |
"context" | |
"errors" | |
"flag" | |
"fmt" | |
"log" | |
"os" | |
"os/exec" | |
"path/filepath" | |
"github.com/fatih/color" | |
"github.com/google/go-github/github" | |
"github.com/joho/godotenv" | |
"golang.org/x/oauth2" | |
) | |
var backupDir = flag.String("backup-dir", ".", "The directory to save the backups to") | |
func main() { | |
flag.Parse() | |
err := godotenv.Load() | |
if err != nil { | |
log.Fatalf("Error loading .env file: %v", err) | |
} | |
token := os.Getenv("API_TOKEN") | |
daemon, err := NewBackupDaemon(token, *backupDir) | |
if err != nil { | |
log.Fatalf("Error creating oauth2 client: %v", err) | |
} | |
ctx := context.Background() | |
repos := daemon.fetchRepositories(ctx) | |
for _, repo := range repos { | |
err := daemon.backupRepo(repo) | |
if err != nil { | |
color.Red("Error backing up %v: %v", repo.GetFullName(), err) | |
} | |
} | |
} | |
type BackupDaemon struct { | |
backupDir string | |
client *github.Client | |
} | |
func NewBackupDaemon(token string, dir string) (*BackupDaemon, error) { | |
backupDir, err := filepath.Abs(dir) | |
if err != nil { | |
return nil, err | |
} | |
ctx := context.Background() | |
client, err := getClient(token, ctx) | |
if err != nil { | |
return nil, err | |
} | |
return &BackupDaemon{backupDir, client}, nil | |
} | |
// getClient will create a new Oauth2 client using the provided API token. The | |
// Context object is used so you can cancel the operation. | |
func getClient(token string, ctx context.Context) (*github.Client, error) { | |
if token == "" { | |
return nil, errors.New("No API_TOKEN found") | |
} | |
ts := oauth2.StaticTokenSource( | |
&oauth2.Token{AccessToken: token}, | |
) | |
tc := oauth2.NewClient(ctx, ts) | |
client := github.NewClient(tc) | |
return client, nil | |
} | |
func (b *BackupDaemon) fetchRepositories(ctx context.Context) []*github.Repository { | |
repos := make([]*github.Repository, 0) | |
color.Magenta("Owned Repositories") | |
owned, err := b.allOwnedRepositories(ctx) | |
if err != nil { | |
log.Println(err) | |
} else { | |
for _, repo := range owned { | |
repos = append(repos, repo) | |
fmt.Printf("\t%v\n", *repo.Name) | |
} | |
} | |
color.Magenta("Starred Repositories") | |
starred, err := b.allStarredRepos(ctx) | |
if err != nil { | |
log.Printf("Error fetching starred repositories: %v", err) | |
} else { | |
for _, repo := range starred { | |
repos = append(repos, repo) | |
fmt.Printf("\t%v\n", *repo.Name) | |
} | |
} | |
return repos | |
} | |
func (b *BackupDaemon) allOwnedRepositories(ctx context.Context) ([]*github.Repository, error) { | |
opt := &github.RepositoryListOptions{ | |
ListOptions: github.ListOptions{PerPage: 50}, | |
} | |
var allRepos []*github.Repository | |
for { | |
repos, resp, err := b.client.Repositories.List(ctx, "", opt) | |
if err != nil { | |
return nil, err | |
} | |
allRepos = append(allRepos, repos...) | |
if resp.NextPage == 0 { | |
break | |
} | |
opt.ListOptions.Page = resp.NextPage | |
} | |
return allRepos, nil | |
} | |
func (b *BackupDaemon) allStarredRepos(ctx context.Context) ([]*github.Repository, error) { | |
opt := &github.ActivityListStarredOptions{ | |
ListOptions: github.ListOptions{PerPage: 50}, | |
} | |
var allRepos []*github.Repository | |
for { | |
starred, resp, err := b.client.Activity.ListStarred(ctx, "", opt) | |
if err != nil { | |
return nil, err | |
} | |
for _, repo := range starred { | |
allRepos = append(allRepos, repo.Repository) | |
} | |
if resp.NextPage == 0 { | |
break | |
} | |
opt.ListOptions.Page = resp.NextPage | |
} | |
return allRepos, nil | |
} | |
// updateRepo will check if the repository currently exists, if so it will do | |
// a `git pull`. Otherwise, it'll `git clone` it. | |
func (b *BackupDaemon) backupRepo(repo *github.Repository) error { | |
repoDir := filepath.Join(b.backupDir, *repo.Name) | |
if _, err := os.Stat(repoDir); os.IsNotExist(err) { | |
return cloneRepo(repoDir, repo) | |
} else { | |
return updateRepo(repoDir, repo) | |
} | |
} | |
func cloneRepo(dir string, repo *github.Repository) error { | |
color.Green(fmt.Sprintf("Cloning New Repo: %v", *repo.FullName)) | |
cloneURL := repo.GetCloneURL() | |
if cloneURL == "" { | |
return errors.New("No cloneURL found") | |
} | |
cmd := exec.Command("git", "clone", "--recursive", cloneURL, dir) | |
output, err := cmd.CombinedOutput() | |
fmt.Print(string(output)) | |
return err | |
} | |
// repoArgs is a hack so that we don't try to pull all branches from "bfc" | |
// because it'll ask for our password... blocking the process. | |
func repoArgs(repo *github.Repository) []string { | |
args := []string{ | |
"--autostash", | |
"--tags", | |
} | |
if *repo.Name != "bfc" { | |
args = append(args, "--all") | |
} | |
return args | |
} | |
func updateRepo(dir string, repo *github.Repository) error { | |
color.Green(fmt.Sprintf("Updating repo: %v (%v)", *repo.FullName, dir)) | |
cmd := exec.Command("git", "pull") | |
for _, arg := range repoArgs(repo) { | |
cmd.Args = append(cmd.Args, arg) | |
} | |
cmd.Dir = dir | |
output, err := cmd.CombinedOutput() | |
fmt.Print(string(output)) | |
if err != nil { | |
return err | |
} | |
return updateSubmodules(dir) | |
} | |
func updateSubmodules(parentRepo string) error { | |
dir := filepath.Join(parentRepo, ".gitmodules") | |
if _, err := os.Stat(dir); os.IsNotExist(err) { | |
return nil | |
} | |
color.Yellow("Updating submodules") | |
cmd := exec.Command("git", "submodule", "update", "--init", "--recursive") | |
cmd.Dir = parentRepo | |
output, err := cmd.CombinedOutput() | |
fmt.Print(string(output)) | |
return err | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment