├── .github ├── build ├── run-tests.sh └── workflows │ ├── pull_request.yml │ ├── push.yml │ └── release.yml ├── .gitignore ├── README.md ├── go.mod ├── go.sum └── main.go /.github/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The basename of our binary 4 | BASE="github2mr" 5 | 6 | # Setup an output directory - creating if missing 7 | cur=$(pwd) 8 | OUTPUT="${cur}/bin" 9 | if [ ! -d "${OUTPUT}" ]; then 10 | mkdir -p "${OUTPUT}" 11 | fi 12 | 13 | # We build on multiple platforms/archs 14 | BUILD_PLATFORMS="linux darwin freebsd" 15 | BUILD_ARCHS="amd64 386" 16 | 17 | # For each platform. 18 | for OS in ${BUILD_PLATFORMS[@]}; do 19 | 20 | # For each arch 21 | for ARCH in ${BUILD_ARCHS[@]}; do 22 | 23 | # Setup a suffix for the binary 24 | SUFFIX="${OS}" 25 | 26 | # i386 is better than 386 27 | if [ "$ARCH" = "386" ]; then 28 | SUFFIX="${SUFFIX}-i386" 29 | else 30 | SUFFIX="${SUFFIX}-${ARCH}" 31 | fi 32 | 33 | echo "Building for ${OS} [${ARCH}] -> ${BASE}-${SUFFIX}" 34 | 35 | # Run the build 36 | export GOARCH=${ARCH} 37 | export GOOS=${OS} 38 | export CGO_ENABLED=0 39 | 40 | # Build the main-binary 41 | go build -ldflags "-X main.version=$(git describe --tags 2>/dev/null || echo 'master')" -o "${OUTPUT}/${BASE}-${SUFFIX}" 42 | done 43 | done 44 | -------------------------------------------------------------------------------- /.github/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # I don't even .. 4 | go env -w GOFLAGS="-buildvcs=false" 5 | 6 | # Install the tools we use to test our code-quality. 7 | # 8 | # Here we setup the tools to install only if the "CI" environmental variable 9 | # is not empty. This is because locally I have them installed. 10 | # 11 | # NOTE: Github Actions always set CI=true 12 | # 13 | if [ -n "${CI}" ] ; then 14 | go install golang.org/x/lint/golint@latest 15 | go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest 16 | go install honnef.co/go/tools/cmd/staticcheck@latest 17 | fi 18 | 19 | # Run the static-check tool 20 | t=$(mktemp) 21 | staticcheck -checks all ./... | grep -v "is deprecated"> "$t" 22 | if [ -s "$t" ]; then 23 | echo "Found errors via 'staticcheck'" 24 | cat "$t" 25 | rm "$t" 26 | exit 1 27 | fi 28 | rm "$t" 29 | 30 | # At this point failures cause aborts 31 | set -e 32 | 33 | # Run the linter 34 | echo "Launching linter .." 35 | golint -set_exit_status ./... 36 | echo "Completed linter .." 37 | 38 | # Run the shadow-checker 39 | echo "Launching shadowed-variable check .." 40 | go vet -vettool="$(which shadow)" ./... 41 | echo "Completed shadowed-variable check .." 42 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | on: pull_request 2 | name: Pull Request 3 | jobs: 4 | test: 5 | name: Run tests 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@master 9 | - name: Test 10 | uses: skx/github-action-tester@master 11 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | name: Push Event 6 | jobs: 7 | test: 8 | name: Run tests 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Test 13 | uses: skx/github-action-tester@master 14 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [created] 4 | name: Handle Release 5 | jobs: 6 | upload: 7 | name: Upload 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout the repository 11 | uses: actions/checkout@master 12 | - name: Generate the artifacts 13 | uses: skx/github-action-build@master 14 | - name: Upload the artifacts 15 | uses: skx/github-action-publish-binaries@master 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | with: 19 | args: bin/*-* 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Configuration / Usage 2 | 3 | Once installed you'll need to configure your github token, which you can generate from [withing your github settings](https://github.com/settings/tokens). 4 | 5 | you can either pass the token as an argument to the tool (via `github2mr -token=xxxxx`), or store it in the environment in the variable GITHUB_TOKEN: 6 | 7 | $ export GITHUB_TOKEN=xxxxx 8 | $ github2mr [options] 9 | 10 | You can run `github2mr -help` to see available options, but in brief: 11 | 12 | * You can choose a default prefix to clone your repositories to. 13 | * By default all repositories will be located at `~/Repos/${git_host}`. 14 | * You can exclude all-organizational repositories. 15 | * Or the reverse, ignoring all personal-repositories. 16 | * You can exclude repositories by name. 17 | * You can default to cloning repositories via HTTP, instead of SSH. 18 | * By default all _archived_ repositories are excluded. 19 | 20 | 21 | ## Other Git Hosts 22 | 23 | This tool can be configured to point at other systems which use the same 24 | API as the public-facing Github site. 25 | 26 | To use it against a self-hosted Github Enterprise installation, for example, 27 | simply specify the URL: 28 | 29 | $ export GITHUB_TOKEN=xxxxx 30 | $ github2mr -api=https://git.example.com/ [options] 31 | 32 | It has also been tested against an installation of [gitbucket](https://github.com/gitbucket/gitbucket) which can be configured a similar way - however in this case you'll find that you receive an error "401 bad credentials" unless you add the `-auth-header-token` flag: 33 | 34 | $ export GITHUB_TOKEN=xxxxx 35 | $ github2mr -api=https://git.example.com/ -auth-header-token 36 | 37 | This seems to be related to the OAUTH header the library I'm using sends, by default it will send a HTTP request looking like this: 38 | 39 | ``` 40 | GET /api/v3/users/skx/repos HTTP/1.1 41 | Host: localhost:9999 42 | User-Agent: go-github 43 | Accept: application/vnd.github.mercy-preview+json 44 | Authorization: Bearer SECRET-TOKEN 45 | Accept-Encoding: gzip 46 | ``` 47 | 48 | Notice that the value of the `Authorization`-header begins with `Bearer`? Gitbucket prefers to see `Authorization: token SECRET-VALUE-HERE`. 49 | 50 | 51 | 52 | 53 | # Github Setup 54 | 55 | This repository is configured to run tests upon every commit, and when pull-requests are created/updated. The testing is carried out via [.github/run-tests.sh](.github/run-tests.sh) which is used by the [github-action-tester](https://github.com/skx/github-action-tester) action. 56 | 57 | Releases are automated in a similar fashion via [.github/build](.github/build), and the [github-action-publish-binaries](https://github.com/skx/github-action-publish-binaries) action. 58 | 59 | Currently these are reporting failures; but I'm in the process of fixing them. 60 | 61 | 62 | 63 | Steve 64 | -- 65 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/skx/github2mr 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/google/go-github/v29 v29.0.2 7 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 3 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 4 | github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= 5 | github.com/google/go-github/v29 v29.0.2 h1:opYN6Wc7DOz7Ku3Oh4l7prmkOMwEcQxpFtxdU8N8Pts= 6 | github.com/google/go-github/v29 v29.0.2/go.mod h1:CHKiKKPHJ0REzfwc14QMklvtHwCveD0PxlMjLlzAM5E= 7 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 8 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 9 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 10 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 11 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 12 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 13 | golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= 14 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 15 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 16 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= 17 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 18 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 19 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 20 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 21 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 22 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 23 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // This is a trivial application which will output a dump of repositories 2 | // which are hosted upon github, or some other host which uses a 3 | // compatible API. 4 | // 5 | // It relies upon having an access-token for authentication. 6 | 7 | package main 8 | 9 | import ( 10 | "bytes" 11 | "context" 12 | "flag" 13 | "fmt" 14 | "net/url" 15 | "os" 16 | "sort" 17 | "strings" 18 | "text/template" 19 | 20 | "github.com/google/go-github/v29/github" 21 | "golang.org/x/oauth2" 22 | ) 23 | 24 | var ( 25 | // 26 | // Context for all calls 27 | // 28 | ctx context.Context 29 | 30 | // 31 | // The actual github client 32 | // 33 | client *github.Client 34 | 35 | // 36 | // The token to use for accessing the remote host. 37 | // 38 | // This is required because gitbucket prefers to see 39 | // 40 | // Authorization: token SECRET-TOKEN 41 | // 42 | // Instead of: 43 | // 44 | // Authorization: bearer SECRET-TOKEN 45 | // 46 | oauthToken = &oauth2.Token{} 47 | 48 | // 49 | // The number of repos to fetch from the API at a time. 50 | // 51 | pageSize = 50 52 | 53 | // 54 | // Our version number, set for release-builds. 55 | // 56 | version = "unreleased" 57 | ) 58 | 59 | // Login accepts the address of a github endpoint, and a corresponding 60 | // token to authenticate with. 61 | // 62 | // We use the login to get the user-information which confirms 63 | // that the login was correct. 64 | func Login(api string, token string) error { 65 | 66 | // Setup context 67 | ctx = context.Background() 68 | 69 | // Setup token 70 | ts := oauth2.StaticTokenSource(oauthToken) 71 | tc := oauth2.NewClient(ctx, ts) 72 | 73 | // Create the API-client 74 | client = github.NewClient(tc) 75 | 76 | // If the user is using a custom URL which doesn't have the 77 | // versioned API-suffix add it. This appears to be necessary. 78 | if api != "https://api.github.com/" { 79 | if !strings.HasSuffix(api, "/api/v3/") { 80 | if !strings.HasSuffix(api, "/") { 81 | api += "/" 82 | } 83 | api += "api/v3/" 84 | } 85 | } 86 | 87 | // Parse the URL for sanity, and update the client with it 88 | url, err := url.Parse(api) 89 | if err != nil { 90 | return err 91 | } 92 | client.BaseURL = url 93 | 94 | // Fetch user-information about the user who we are logging in as. 95 | user, _, err := client.Users.Get(ctx, "") 96 | if err != nil { 97 | return err 98 | } 99 | 100 | // Ensure we have a login 101 | if *user.Login == "" { 102 | return fmt.Errorf("we failed to find our username, which suggests our login failed") 103 | } 104 | 105 | return nil 106 | } 107 | 108 | // getPersonalRepos returns all the personal repositories which 109 | // belong to our user. 110 | func getPersonalRepos(fetch string) ([]*github.Repository, error) { 111 | 112 | var results []*github.Repository 113 | 114 | // Fetch in pages 115 | opt := &github.RepositoryListOptions{ 116 | ListOptions: github.ListOptions{PerPage: pageSize}, 117 | Type: fetch, 118 | } 119 | 120 | // Loop until we're done. 121 | for { 122 | repos, resp, err := client.Repositories.List(ctx, "", opt) 123 | if err != nil { 124 | return results, err 125 | } 126 | results = append(results, repos...) 127 | if resp.NextPage == 0 { 128 | break 129 | } 130 | opt.Page = resp.NextPage 131 | } 132 | 133 | return results, nil 134 | 135 | } 136 | 137 | // getOrganizationalRepositores finds all the organizations the 138 | // user is a member of, then fetches their repositories 139 | func getOrganizationalRepositores(fetch string) ([]*github.Repository, error) { 140 | 141 | var results []*github.Repository 142 | 143 | // Get the organizations the user is a member of. 144 | orgs, _, err := client.Organizations.List(ctx, "", nil) 145 | if err != nil { 146 | return results, err 147 | } 148 | 149 | // Fetch in pages 150 | opt := &github.RepositoryListByOrgOptions{ 151 | ListOptions: github.ListOptions{PerPage: pageSize}, 152 | Type: fetch, 153 | } 154 | 155 | // For each organization we want to get their repositories. 156 | for _, org := range orgs { 157 | 158 | // Loop forever getting the repositories 159 | for { 160 | 161 | repos, resp, err := client.Repositories.ListByOrg(ctx, *org.Login, opt) 162 | if err != nil { 163 | return results, err 164 | } 165 | results = append(results, repos...) 166 | if resp.NextPage == 0 { 167 | break 168 | } 169 | opt.Page = resp.NextPage 170 | } 171 | } 172 | 173 | return results, nil 174 | } 175 | 176 | // 177 | // Entry-point 178 | // 179 | func main() { 180 | 181 | // 182 | // Parse flags 183 | // 184 | archived := flag.Bool("archived", false, "Include archived repositories in the output?") 185 | api := flag.String("api", "https://api.github.com/", "The API end-point to use for the remote git-host.") 186 | authHeader := flag.Bool("auth-header-token", false, "Use an authorization-header including 'token' rather than 'bearer'.\nThis is required for gitbucket, and perhaps other systems.") 187 | exclude := flag.String("exclude", "", "Comma-separated list of repositories to exclude.") 188 | getOrgs := flag.String("organizations", "all", "Which organizational repositories to fetch.\nValid values are 'public', 'private', 'none', or 'all'.") 189 | getPersonal := flag.String("personal", "all", "Which personal repositories to fetch.\nValid values are 'public', 'private', 'none', or 'all'.") 190 | http := flag.Bool("http", false, "Generate HTTP-based clones rather than SSH-based ones.") 191 | ssh := flag.Bool("ssh", false, "Add 'ssh://'-prefix to the git clone command.") 192 | output := flag.String("output", "", "Write output to the named file, instead of printing to STDOUT.") 193 | prefix := flag.String("prefix", "", "The prefix beneath which to store the repositories upon the current system.") 194 | token := flag.String("token", "", "The API token used to authenticate to the remote API-host.") 195 | versionCmd := flag.Bool("version", false, "Report upon our version, and terminate.") 196 | flag.Parse() 197 | 198 | // 199 | // Showing only the version? 200 | // 201 | if *versionCmd { 202 | fmt.Printf("github2mr %s\n", version) 203 | return 204 | } 205 | 206 | // 207 | // Validate the repository-types 208 | // 209 | if *getPersonal != "all" && 210 | *getPersonal != "none" && 211 | *getPersonal != "public" && 212 | *getPersonal != "private" { 213 | fmt.Fprintf(os.Stderr, "Valid settings are 'public', 'private', 'none', or 'all'\n") 214 | return 215 | } 216 | if *getOrgs != "all" && 217 | *getOrgs != "none" && 218 | *getOrgs != "public" && 219 | *getOrgs != "private" { 220 | fmt.Fprintf(os.Stderr, "Valid settings are 'public', 'private', 'none', or 'all'\n") 221 | return 222 | } 223 | 224 | // 225 | // Get the authentication token supplied via the flag, falling back 226 | // to the environment if nothing has been specified. 227 | // 228 | tok := *token 229 | if tok == "" { 230 | // Fallback 231 | tok = os.Getenv("GITHUB_TOKEN") 232 | 233 | if tok == "" { 234 | fmt.Printf("Please specify your github token!\n") 235 | return 236 | } 237 | } 238 | 239 | // 240 | // Populate our global OAUTH token with the supplied value. 241 | // 242 | oauthToken.AccessToken = tok 243 | 244 | // 245 | // Allow setting the authorization header-type, if required. 246 | // 247 | if *authHeader { 248 | oauthToken.TokenType = "token" 249 | } 250 | 251 | // 252 | // Login and confirm that this worked. 253 | // 254 | err := Login(*api, tok) 255 | if err != nil { 256 | fmt.Fprintf(os.Stderr, "Login error - is your token set/correct? %s\n", err.Error()) 257 | return 258 | } 259 | 260 | // 261 | // Fetch details of all "personal" repositories, unless we're not 262 | // supposed to. 263 | // 264 | var personal []*github.Repository 265 | if *getPersonal != "none" { 266 | personal, err = getPersonalRepos(*getPersonal) 267 | if err != nil { 268 | fmt.Fprintf(os.Stderr, "Failed to fetch personal repository list: %s\n", err.Error()) 269 | return 270 | } 271 | } 272 | 273 | // 274 | // Fetch details of all organizational repositories, unless we're 275 | // not supposed to. 276 | // 277 | var orgs []*github.Repository 278 | if *getOrgs != "none" { 279 | orgs, err = getOrganizationalRepositores(*getOrgs) 280 | if err != nil { 281 | fmt.Fprintf(os.Stderr, "Failed to fetch organizational repositories: %s\n", 282 | err.Error()) 283 | return 284 | } 285 | } 286 | 287 | // 288 | // If the prefix is not set then create a default. 289 | // 290 | // This will be of the form: 291 | // 292 | // ~/Repos/github.com/x/y 293 | // ~/Repos/git.example.com/x/y 294 | // ~/Repos/git.steve.fi/x/y 295 | // 296 | // i.e "~/Repos/${git host}/${owner}/${path} 297 | // 298 | // (${git host} comes from the remote API host.) 299 | // 300 | repoPrefix := *prefix 301 | if repoPrefix == "" { 302 | 303 | // Get the hostname 304 | url, _ := url.Parse(*api) 305 | host := url.Hostname() 306 | 307 | // Handle the obvious case 308 | if host == "api.github.com" { 309 | host = "github.com" 310 | } 311 | 312 | // Generate a prefix 313 | repoPrefix = os.Getenv("HOME") + "/Repos/" + host 314 | } 315 | 316 | // 317 | // Combine the results of the repositories we've found. 318 | // 319 | var all []*github.Repository 320 | all = append(all, personal...) 321 | all = append(all, orgs...) 322 | 323 | // 324 | // Sort the list, based upon the full-name. 325 | // 326 | sort.Slice(all[:], func(i, j int) bool { 327 | 328 | // Case-insensitive sorting. 329 | a := strings.ToLower(*all[i].FullName) 330 | b := strings.ToLower(*all[j].FullName) 331 | 332 | return a < b 333 | }) 334 | 335 | // 336 | // Repos we're excluding 337 | // 338 | excluded := strings.Split(*exclude, ",") 339 | 340 | // 341 | // Structure we use for template expansion 342 | // 343 | type Repo struct { 344 | // Prefix-directory for local clones. 345 | Prefix string 346 | 347 | // Name of the repository "owner/repo-name". 348 | Name string 349 | 350 | // Source to clone from http/ssh-based. 351 | Source string 352 | } 353 | 354 | // 355 | // Repos we will output 356 | // 357 | var repos []*Repo 358 | 359 | // 360 | // Now format the repositories we've discovered. 361 | // 362 | for _, repo := range all { 363 | 364 | // 365 | // If the repository is archived then 366 | // skip it, unless we're supposed to keep 367 | // it. 368 | // 369 | if *repo.Archived && !*archived { 370 | continue 371 | } 372 | 373 | // 374 | // The clone-type is configurable 375 | // 376 | clone := *repo.SSHURL 377 | if *http { 378 | clone = *repo.CloneURL 379 | } 380 | 381 | // 382 | // Sometimes SSH clones need a prefix 383 | // 384 | if *ssh { 385 | clone = "ssh://" + clone 386 | } 387 | 388 | // 389 | // Hack! 390 | // 391 | clone = strings.ReplaceAll(clone, ":4444:", ":4444/") 392 | 393 | // 394 | // Should we exclude this entry? 395 | // 396 | skip := false 397 | for _, exc := range excluded { 398 | 399 | exc = strings.TrimSpace(exc) 400 | 401 | if len(exc) > 0 && strings.Contains(strings.ToLower(clone), strings.ToLower(exc)) { 402 | skip = true 403 | } 404 | } 405 | 406 | // Skipped 407 | if skip { 408 | continue 409 | } 410 | 411 | repos = append(repos, &Repo{Prefix: repoPrefix, 412 | Name: *repo.FullName, 413 | Source: clone}) 414 | } 415 | 416 | // 417 | // Load the template we'll use for formatting the output 418 | // 419 | tmpl := `# Generated by github2mr - {{len .}} repositories 420 | 421 | {{range .}} 422 | [{{.Prefix}}/{{.Name}}] 423 | checkout = git clone {{.Source}} 424 | {{end}} 425 | ` 426 | 427 | // 428 | // Parse the template and execute it. 429 | // 430 | var out bytes.Buffer 431 | t := template.Must(template.New("tmpl").Parse(tmpl)) 432 | err = t.Execute(&out, repos) 433 | 434 | // 435 | // If there were errors we're done. 436 | // 437 | if err != nil { 438 | fmt.Fprintf(os.Stderr, "Error interpolating template:%s\n", err.Error()) 439 | return 440 | } 441 | 442 | // 443 | // Show the results, or write to the specified file as appropriate 444 | // 445 | if *output != "" { 446 | file, err := os.Create(*output) 447 | if err != nil { 448 | fmt.Fprintf(os.Stderr, "failed to open %s:%s\n", *output, err.Error()) 449 | return 450 | } 451 | defer file.Close() 452 | file.Write(out.Bytes()) 453 | } else { 454 | fmt.Println(out.String()) 455 | } 456 | 457 | // 458 | // All done. 459 | // 460 | } 461 | --------------------------------------------------------------------------------