Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 16 additions & 80 deletions cmd/src/batch_new.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
package main

import (
"context"
"flag"
"fmt"
"os"
"os/exec"
"strings"

"text/template"

"github.com/pkg/errors"
"github.com/sourcegraph/src-cli/internal/batches"
"github.com/sourcegraph/src-cli/internal/api"
"github.com/sourcegraph/src-cli/internal/batches/service"
"github.com/sourcegraph/src-cli/internal/cmderrors"
)

func init() {
Expand All @@ -30,51 +27,33 @@ Examples:
`

flagSet := flag.NewFlagSet("new", flag.ExitOnError)
apiFlags := api.NewFlags(flagSet)

var (
fileFlag = flagSet.String("f", "batch.yaml", "The name of the batch spec file to create.")
)

handler := func(args []string) error {
ctx := context.Background()

if err := flagSet.Parse(args); err != nil {
return err
}

f, err := os.OpenFile(*fileFlag, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
if err != nil {
if os.IsExist(err) {
return fmt.Errorf("file %s already exists", *fileFlag)
}
return errors.Wrapf(err, "failed to create file %s", *fileFlag)
if len(flagSet.Args()) != 0 {
return cmderrors.Usage("additional arguments not allowed")
}
defer f.Close()

tmpl, err := template.New("").Parse(batchSpecTmpl)
if err != nil {
return err
}
svc := service.New(&service.Opts{
Client: cfg.apiClient(apiFlags, flagSet.Output()),
})

author := batches.GitCommitAuthor{
Name: "Sourcegraph",
Email: "batch-changes@sourcegraph.com",
}

// Try to get better default values from git, ignore any errors.
if err := checkExecutable("git", "version"); err == nil {
var gitAuthorName, gitAuthorEmail string
var err1, err2 error
gitAuthorName, err1 = getGitConfig("user.name")
gitAuthorEmail, err2 = getGitConfig("user.email")

if err1 == nil && err2 == nil && gitAuthorName != "" && gitAuthorEmail != "" {
author.Name = gitAuthorName
author.Email = gitAuthorEmail
}
if err := svc.DetermineFeatureFlags(ctx); err != nil {
return err
}

err = tmpl.Execute(f, map[string]interface{}{"Author": author})
if err != nil {
return errors.Wrap(err, "failed to write batch spec to file")
if err := svc.GenerateExampleSpec(ctx, *fileFlag); err != nil {
return err
}

fmt.Printf("%s created.\n", *fileFlag)
Expand All @@ -92,46 +71,3 @@ Examples:
},
})
}

func getGitConfig(attribute string) (string, error) {
cmd := exec.Command("git", "config", "--get", attribute)
out, err := cmd.CombinedOutput()
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}

const batchSpecTmpl = `name: NAME-OF-YOUR-BATCH-CHANGE
description: DESCRIPTION-OF-YOUR-BATCH-CHANGE

# "on" specifies on which repositories to execute the "steps".
on:
# Example: find all repositories that contain a README.md file.
- repositoriesMatchingQuery: file:README.md

# "steps" are run in each repository. Each step is run in a Docker container
# with the repository as the working directory. Once complete, each
# repository's resulting diff is captured.
steps:
# Example: append "Hello World" to every README.md
- run: echo "Hello World" | tee -a $(find -name README.md)
container: alpine:3

# "changesetTemplate" describes the changeset (e.g., GitHub pull request) that
# will be created for each repository.
changesetTemplate:
title: Hello World
body: This adds Hello World to the README

branch: BRANCH-NAME-IN-EACH-REPOSITORY # Push the commit to this branch.

commit:
author:
name: {{ .Author.Name }}
email: {{ .Author.Email }}
message: Append Hello World to all README.md files

# Change published to true once you're ready to create changesets on the code host.
published: false
`
88 changes: 88 additions & 0 deletions internal/batches/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import (
"context"
"encoding/json"
"fmt"
"html/template"
"io"
"io/ioutil"
"os"
"os/exec"
"path"
"regexp"
"strings"
Expand Down Expand Up @@ -273,6 +276,82 @@ func (svc *Service) ParseBatchSpec(in io.Reader) (*batches.BatchSpec, string, er
return spec, string(data), nil
}

const exampleSpecTmpl = `name: NAME-OF-YOUR-BATCH-CHANGE
description: DESCRIPTION-OF-YOUR-BATCH-CHANGE

# "on" specifies on which repositories to execute the "steps".
on:
# Example: find all repositories that contain a README.md file.
- repositoriesMatchingQuery: file:README.md

# "steps" are run in each repository. Each step is run in a Docker container
# with the repository as the working directory. Once complete, each
# repository's resulting diff is captured.
steps:
# Example: append "Hello World" to every README.md
- run: echo "Hello World" | tee -a $(find -name README.md)
container: alpine:3

# "changesetTemplate" describes the changeset (e.g., GitHub pull request) that
# will be created for each repository.
changesetTemplate:
title: Hello World
body: This adds Hello World to the README

branch: BRANCH-NAME-IN-EACH-REPOSITORY # Push the commit to this branch.

commit:
author:
name: {{ .Author.Name }}
email: {{ .Author.Email }}
message: Append Hello World to all README.md files
`

const exampleSpecPublishFlagTmpl = `
# Change published to true once you're ready to create changesets on the code host.
published: false
`

func (svc *Service) GenerateExampleSpec(ctx context.Context, fileName string) error {
// Try to create file. Bail out, if it already exists.
f, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
if err != nil {
if os.IsExist(err) {
return fmt.Errorf("file %s already exists", fileName)
}
return errors.Wrapf(err, "failed to create file %s", fileName)
}
defer f.Close()

t := exampleSpecTmpl
if !svc.features.AllowOptionalPublished {
t += exampleSpecPublishFlagTmpl
}
tmpl, err := template.New("").Parse(t)
if err != nil {
return err
}

author := batches.GitCommitAuthor{
Name: "Sourcegraph",
Email: "batch-changes@sourcegraph.com",
}
// Try to get better default values from git, ignore any errors.
gitAuthorName, err1 := getGitConfig("user.name")
gitAuthorEmail, err2 := getGitConfig("user.email")
if err1 == nil && err2 == nil && gitAuthorName != "" && gitAuthorEmail != "" {
author.Name = gitAuthorName
author.Email = gitAuthorEmail
}

err = tmpl.Execute(f, map[string]interface{}{"Author": author})
if err != nil {
return errors.Wrap(err, "failed to write batch spec to file")
}

return nil
}

const namespaceQuery = `
query NamespaceQuery($name: String!) {
user(username: $name) {
Expand Down Expand Up @@ -702,3 +781,12 @@ func (sr *searchResult) UnmarshalJSON(data []byte) error {
return errors.Errorf("unknown GraphQL type %q", tn.Typename)
}
}

func getGitConfig(attribute string) (string, error) {
cmd := exec.Command("git", "config", "--get", attribute)
out, err := cmd.CombinedOutput()
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}