Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@toidiu
Forked from Michael-F-Bryan/github-backups.go
Created September 13, 2017 14:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save toidiu/c1e512bb0235e0c5c84b111f769ad2c3 to your computer and use it in GitHub Desktop.
Save toidiu/c1e512bb0235e0c5c84b111f769ad2c3 to your computer and use it in GitHub Desktop.
// 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