Skip to content

Instantly share code, notes, and snippets.

@xarantolus
Created June 17, 2019 13:27
Show Gist options
  • Save xarantolus/7cf11f272f48b9dc6a69bcd930a123ab to your computer and use it in GitHub Desktop.
Save xarantolus/7cf11f272f48b9dc6a69bcd930a123ab to your computer and use it in GitHub Desktop.
// Program for cloning repos at a specific place on your computer
package main
import (
"flag"
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
)
/*
MIT License
Copyright (c) 2019 xarantolus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
const (
// Change this to a path on your computer (that already exists)
basePath = "/path/to/repos"
)
// COMPILING / INSTALLING:
// Download a release from https://golang.org (this program was tested using `go version go1.12 windows/amd64`, but should work anywhere) and install Go.
//
// Before compiling, make sure you changed the `basePath` variable above (line 40)
//
// Open a terminal, cd to the directory where this file is located, run `go build`.
// Move the resulting executable somewhere in your $PATH
// USAGE:
// Download/Clone a repo:
// `rep https://github.com/user/repo`
// You can also just give it a sub-directory, e.g.
// `rep https://github.com/user/repo/some/subdir`
// To update all repos, just run `rep up` or `rep update`
func main() {
flag.Parse()
if len(flag.Args()) == 0 {
must(fmt.Errorf("Need at least one repo to clone"))
}
if flag.Arg(0) == "update" || flag.Arg(0) == "up" {
must(updateRepos())
return
}
var counter, failed int
for _, link := range flag.Args() {
if link == "-v" {
continue
}
err := processRepo(link)
if err != nil {
fmt.Printf("Error while processing repo: %s\n", err.Error())
failed++
}
counter++
}
if counter == 1 {
fmt.Printf("Got one repo, %d failed\n", failed)
} else {
fmt.Printf("Got %d repos, %d failed\n", counter, failed)
}
}
func processRepo(link string) (err error) {
pathFragment, cloneURL, err := splitURL(link)
if err != nil {
return
}
log("Processing repo", pathFragment)
path, err := checkPath(pathFragment)
if err != nil {
return
}
return cloneRepo(cloneURL, path)
}
// updateRepos goes through all repos at `basePath` and updates them using git pull
func updateRepos() (err error) {
baseLen := len(strings.Split(basePath, string(os.PathSeparator)))
// basepath + "/hosting/author/name"
// => len must be baseLen + 3 to be in main repo folder
// Could speed this up by using a "leveled" walk with a depth of 3
err = filepath.Walk(basePath, func(path string, fi os.FileInfo, err error) error {
split := strings.Split(path, string(os.PathSeparator))
if len(split) != baseLen+3 {
return nil
}
repoName := strings.Join(split[len(split)-3:], "/")
log("Updating repo", repoName)
err = updateRepo(path)
if err != nil {
fmt.Printf("Error while updating %s: %s\n", repoName, err.Error())
}
return nil
})
return err
}
// updateRepo actually updates a repo using git pull
func updateRepo(path string) (err error) {
cmd := exec.Command("git", "pull")
cmd.Dir = path
return cmd.Run()
}
// checkPath creates a directory at `p` if it doesn't exist yet
func checkPath(p string) (result string, err error) {
result = filepath.Join(basePath, p)
return result, os.MkdirAll(result, os.ModePerm)
}
// cloneRepo clones the repo at `url` to the directory `destination`
func cloneRepo(url, destination string) (err error) {
c := exec.Command("git", "clone", url, destination)
c.Stdout = os.Stdout
c.Stderr = os.Stderr
c.Stdin = os.Stdin
return c.Run()
}
var (
// Websites with the `host/username/reponame` url format
// They have the git clone url at `host/username/reponame.git`
hostsUsernameFormat = map[string]bool{
"github.com": true,
"gitlab.com": true,
"bitbucket.org": true,
"gist.github.com": true,
}
)
// splitURL is the main url resolving function.
// The raw input `link` is converted to serveral strings:
// `cleanPath` should be the path fragment after `basePath`
// `cloneURL` is the direct clone url for the repo
func splitURL(link string) (cleanPath, cloneURL string, err error) {
parsed, err := url.Parse(link)
if err != nil {
return
}
// Websites with the `host/username/reponame` url format
if _, ok := hostsUsernameFormat[parsed.Host]; ok {
split := strings.Split(strings.TrimLeft(parsed.Path, "/"), "/")
if len(split) > 2 {
// we cannot clone "github.com/user/name/folder/in/repo", we need to clone "github.com/user/name"
split = split[:2]
}
cleanPath = parsed.Host + "/" + strings.Join(split, "/")
return strings.TrimSuffix(cleanPath, ".git"), "https://" + cleanPath + ".git", nil
}
// "golang.org/x/something" always redirects to "github.com/golang/something", so we resolve the repo url like that, but not the path
if parsed.Host == "golang.org" {
split := strings.Split(strings.TrimLeft(parsed.Path, "/"), "/")
if len(split) > 1 && split[0] == "x" {
return "golang.org/x/" + split[1], "https://github.com/golang/" + split[1] + ".git", nil
}
}
// This works e.g. for self-hosted gitlab repos - but you cannot give a folder in that repo, like "selfhosted.com/user/repo/folder/in/repo"
cleanPath = parsed.Host + "/" + parsed.Path
return cleanPath, "https://" + cleanPath, nil
}
// must is for fatal error handling
func must(err error) {
if err != nil {
fmt.Printf("Fatal error: %s\n", err.Error())
os.Exit(1)
}
}
func log(s ...interface{}) {
fmt.Fprintln(os.Stderr, s...)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment