Skip to content

Commit 3f42eb6

Browse files
committed
Option to read list of repos from a text file
1 parent e7402b4 commit 3f42eb6

3 files changed

Lines changed: 59 additions & 22 deletions

File tree

README.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,31 @@ Features:
1313

1414
## Usage
1515

16-
multiclone https://github.com/owner/repo [DIR]
17-
multiclone owner/repo [DIR]
16+
multiclone https://github.com/owner/repo
17+
multiclone owner/repo
1818

19-
Clone forks of owner/repo into DIR (or the current directory).
19+
Clone forks of owner/repo into the current directory.
20+
21+
multiclone repos.txt
22+
23+
`repos.txt` is a file with one repo name per line.
2024

2125
### GitHub Classroom
2226

23-
multiclone https://github.com/owner/repo [DIR] --classroom
24-
multiclone org/repo [DIR] --classroom
27+
multiclone https://github.com/owner/repo --classroom
28+
multiclone org/repo --classroom
2529

26-
Clone org's repos named repo-* into DIR (or the current directory).
30+
Clone org's repos named repo-* into the current directory.
2731

2832
This is intended for use with repos created via [GitHub Classroom](https://classroom.github.com).
2933

3034
### Options
3135

32-
multiclone owner/repo [DIR] --dry-run
36+
multiclone --dir path/to/dir owner/repo
37+
38+
Clone into subdirectories of `path/to/dir`, instead of the current directory.
39+
40+
multiclone owner/repo --dry-run
3341

3442
See the `git` commands that would be run, without actually running them.
3543

TODO.txt

Lines changed: 0 additions & 2 deletions
This file was deleted.

main.go

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,43 +8,71 @@ import (
88
"os/exec"
99
"path/filepath"
1010
"regexp"
11+
"strings"
1112
"text/template"
1213

1314
kingpin "gopkg.in/alecthomas/kingpin.v2"
1415
)
1516

1617
var (
18+
dir = kingpin.Flag("dir", "The name of the directory to clone into").Short('d').Default(".").String()
1719
dryRun = kingpin.Flag("dry-run", "Dry run").Bool()
20+
reposFile = kingpin.Flag("file", "Repo is GitHub classroom repo").Short('f').ExistingFile()
1821
classroom = kingpin.Flag("classroom", "Repo is GitHub classroom repo").Bool()
1922
jobs = kingpin.Flag("jobs", "The number of repos fetched at the same time").Short('j').Default("8").Int()
2023
verbose = kingpin.Flag("verbose", "Verbose").Bool()
2124
mrconfig = kingpin.Flag("mrconfig", "Create a myrepos .mrconfig file in the output directory").Default("true").Bool()
2225

2326
nwo = kingpin.Arg("repo", "GitHub owner/repo").String()
24-
dir = kingpin.Arg("directory", "The name of the directory to clone into").Default(".").String()
2527

2628
repoRE = regexp.MustCompile(`^(?:https://github\.com/)?([^/]+)/([^/]+)$`)
2729
)
2830

2931
func main() {
3032
kingpin.Parse()
31-
if *nwo == "" {
33+
switch {
34+
case *reposFile == "" && *nwo == "":
3235
kingpin.FatalUsage("repo is a required argument")
33-
}
34-
m := repoRE.FindStringSubmatch(*nwo)
35-
if m == nil {
36-
kingpin.FatalUsage("repo must be in the format owner/repo")
37-
}
38-
owner, name := m[1], m[2]
39-
if err := run(owner, name); err != nil {
40-
kingpin.FatalIfError(err, "")
36+
37+
case *reposFile != "" && *nwo != "":
38+
kingpin.FatalUsage("--file and repo are exclusive")
39+
case *nwo != "":
40+
m := repoRE.FindStringSubmatch(*nwo)
41+
if m == nil {
42+
kingpin.FatalUsage("repo must be in the format owner/repo")
43+
}
44+
owner, name := m[1], m[2]
45+
kingpin.FatalIfError(run(owner, name), "")
46+
case *reposFile != "":
47+
kingpin.FatalIfError(runWithFiles(*reposFile), "")
4148
}
4249
}
4350

4451
type repoEntry struct {
4552
Dir, URL string
4653
}
4754

55+
func runWithFiles(reposFile string) error {
56+
dat, err := ioutil.ReadFile(reposFile)
57+
if err != nil {
58+
return err
59+
}
60+
var entries []repoEntry
61+
re := regexp.MustCompile(`(?m)^(?:https://github\.com/)?([^/]+)/(.+?)(?:\.git)?\s*(?:#.*)?$`)
62+
for _, m := range re.FindAllStringSubmatch(string(dat), -1) {
63+
entries = append(entries, repoEntry{Dir: m[1], URL: strings.TrimSpace(m[0])})
64+
}
65+
if err := cloneRepos(entries, *dir); err != nil {
66+
return err
67+
}
68+
if *mrconfig {
69+
if err := writeMrConfig(entries, *dir); err != nil {
70+
return err
71+
}
72+
}
73+
return nil
74+
}
75+
4876
func run(owner, name string) error {
4977
repos, err := queryRepos(owner, name)
5078
if err != nil {
@@ -56,6 +84,7 @@ func run(owner, name string) error {
5684
prep = "without"
5785
}
5886
fmt.Fprintf(os.Stderr, "No entries. Try again %s the --classroom option.\n", prep)
87+
return nil
5988
}
6089
var entries []repoEntry
6190
for _, repo := range repos {
@@ -84,10 +113,12 @@ func queryRepos(owner, name string) ([]repoRecord, error) {
84113
}
85114

86115
func repoAuthor(repo repoRecord, name string) string {
87-
if *classroom {
116+
switch *classroom {
117+
case true:
88118
return repo.Name[len(name)+1:]
119+
default:
120+
return repo.Owner
89121
}
90-
return repo.Owner
91122
}
92123

93124
func cloneRepos(repos []repoEntry, dir string) error {
@@ -105,13 +136,13 @@ func cloneRepos(repos []repoEntry, dir string) error {
105136
go func(repo repoEntry) {
106137
sem <- true
107138
defer func() { <-sem }()
108-
// dst := filepath.Join(dir, repo.owner)
109139
args := []string{"git", "clone", repo.URL, repo.Dir}
110140
if *dryRun {
111141
args = append([]string{"echo"}, args...)
112142
// time.Sleep(time.Second)
113143
}
114144
cmd := exec.Command(args[0], args[1:]...)
145+
cmd.Dir = dir
115146
stdoutStderr, err := cmd.CombinedOutput()
116147
if err != nil {
117148
errors <- fmt.Errorf("%s: %s while trying to clone %s", err, stdoutStderr, repo.URL)

0 commit comments

Comments
 (0)