Merge pull request #950 from AliabbasMerchant/blank-issue

Allow choosing a blank issue/pr template
This commit is contained in:
Mislav Marohnić 2020-06-10 12:47:06 +02:00 committed by GitHub
commit 1c9c5e5616
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 236 additions and 78 deletions

View file

@ -1,3 +1,9 @@
---
name: "\U0001F41B Bug fix"
about: Fix a bug in GitHub CLI
---
<!--
Please make sure you read our contributing guidelines at
https://github.com/cli/cli/blob/trunk/.github/CONTRIBUTING.md

View file

@ -351,11 +351,11 @@ func issueCreate(cmd *cobra.Command, args []string) error {
return err
}
var templateFiles []string
var nonLegacyTemplateFiles []string
if baseOverride == "" {
if rootDir, err := git.ToplevelDir(); err == nil {
// TODO: figure out how to stub this in tests
templateFiles = githubtemplate.Find(rootDir, "ISSUE_TEMPLATE")
nonLegacyTemplateFiles = githubtemplate.FindNonLegacy(rootDir, "ISSUE_TEMPLATE")
}
}
@ -399,7 +399,7 @@ func issueCreate(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
} else if len(templateFiles) > 1 {
} else if len(nonLegacyTemplateFiles) > 1 {
openURL += "/choose"
}
cmd.Printf("Opening %s in your browser.\n", displayURL(openURL))
@ -418,6 +418,7 @@ func issueCreate(cmd *cobra.Command, args []string) error {
action := SubmitAction
tb := issueMetadataState{
Type: issueMetadata,
Assignees: assignees,
Labels: labelNames,
Projects: projectNames,
@ -427,7 +428,14 @@ func issueCreate(cmd *cobra.Command, args []string) error {
interactive := !(cmd.Flags().Changed("title") && cmd.Flags().Changed("body"))
if interactive {
err := titleBodySurvey(cmd, &tb, apiClient, baseRepo, title, body, defaults{}, templateFiles, false, repo.ViewerCanTriage())
var legacyTemplateFile *string
if baseOverride == "" {
if rootDir, err := git.ToplevelDir(); err == nil {
// TODO: figure out how to stub this in tests
legacyTemplateFile = githubtemplate.FindLegacy(rootDir, "ISSUE_TEMPLATE")
}
}
err := titleBodySurvey(cmd, &tb, apiClient, baseRepo, title, body, defaults{}, nonLegacyTemplateFiles, legacyTemplateFile, false, repo.ViewerCanTriage())
if err != nil {
return fmt.Errorf("could not collect title and/or body: %w", err)
}

View file

@ -203,6 +203,7 @@ func prCreate(cmd *cobra.Command, _ []string) error {
}
tb := issueMetadataState{
Type: prMetadata,
Reviewers: reviewers,
Assignees: assignees,
Labels: labelNames,
@ -213,13 +214,14 @@ func prCreate(cmd *cobra.Command, _ []string) error {
interactive := !(cmd.Flags().Changed("title") && cmd.Flags().Changed("body"))
if !isWeb && !autofill && interactive {
var templateFiles []string
var nonLegacyTemplateFiles []string
var legacyTemplateFile *string
if rootDir, err := git.ToplevelDir(); err == nil {
// TODO: figure out how to stub this in tests
templateFiles = githubtemplate.Find(rootDir, "PULL_REQUEST_TEMPLATE")
nonLegacyTemplateFiles = githubtemplate.FindNonLegacy(rootDir, "PULL_REQUEST_TEMPLATE")
legacyTemplateFile = githubtemplate.FindLegacy(rootDir, "PULL_REQUEST_TEMPLATE")
}
err := titleBodySurvey(cmd, &tb, client, baseRepo, title, body, defs, templateFiles, true, baseRepo.ViewerCanTriage())
err := titleBodySurvey(cmd, &tb, client, baseRepo, title, body, defs, nonLegacyTemplateFiles, legacyTemplateFile, true, baseRepo.ViewerCanTriage())
if err != nil {
return fmt.Errorf("could not collect title and/or body: %w", err)
}

View file

@ -13,8 +13,16 @@ import (
)
type Action int
type metadataStateType int
const (
issueMetadata metadataStateType = iota
prMetadata
)
type issueMetadataState struct {
Type metadataStateType
Body string
Title string
Action Action
@ -99,35 +107,46 @@ func confirmSubmission(allowPreview bool, allowMetadata bool) (Action, error) {
}
}
func selectTemplate(templatePaths []string) (string, error) {
func selectTemplate(nonLegacyTemplatePaths []string, legacyTemplatePath *string, metadataType metadataStateType) (string, error) {
templateResponse := struct {
Index int
}{}
if len(templatePaths) > 1 {
templateNames := make([]string, 0, len(templatePaths))
for _, p := range templatePaths {
templateNames = append(templateNames, githubtemplate.ExtractName(p))
}
selectQs := []*survey.Question{
{
Name: "index",
Prompt: &survey.Select{
Message: "Choose a template",
Options: templateNames,
},
},
}
if err := SurveyAsk(selectQs, &templateResponse); err != nil {
return "", fmt.Errorf("could not prompt: %w", err)
}
templateNames := make([]string, 0, len(nonLegacyTemplatePaths))
for _, p := range nonLegacyTemplatePaths {
templateNames = append(templateNames, githubtemplate.ExtractName(p))
}
if metadataType == issueMetadata {
templateNames = append(templateNames, "Open a blank issue")
} else if metadataType == prMetadata {
templateNames = append(templateNames, "Open a blank pull request")
}
templateContents := githubtemplate.ExtractContents(templatePaths[templateResponse.Index])
selectQs := []*survey.Question{
{
Name: "index",
Prompt: &survey.Select{
Message: "Choose a template",
Options: templateNames,
},
},
}
if err := SurveyAsk(selectQs, &templateResponse); err != nil {
return "", fmt.Errorf("could not prompt: %w", err)
}
if templateResponse.Index == len(nonLegacyTemplatePaths) { // the user has selected the blank template
if legacyTemplatePath != nil {
templateContents := githubtemplate.ExtractContents(*legacyTemplatePath)
return string(templateContents), nil
} else {
return "", nil
}
}
templateContents := githubtemplate.ExtractContents(nonLegacyTemplatePaths[templateResponse.Index])
return string(templateContents), nil
}
func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClient *api.Client, repo ghrepo.Interface, providedTitle, providedBody string, defs defaults, templatePaths []string, allowReviewers, allowMetadata bool) error {
func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClient *api.Client, repo ghrepo.Interface, providedTitle, providedBody string, defs defaults, nonLegacyTemplatePaths []string, legacyTemplatePath *string, allowReviewers, allowMetadata bool) error {
editorCommand, err := determineEditor(cmd)
if err != nil {
return err
@ -137,13 +156,15 @@ func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClie
templateContents := ""
if providedBody == "" {
if len(templatePaths) > 0 {
if len(nonLegacyTemplatePaths) > 0 {
var err error
templateContents, err = selectTemplate(templatePaths)
templateContents, err = selectTemplate(nonLegacyTemplatePaths, legacyTemplatePath, issueState.Type)
if err != nil {
return err
}
issueState.Body = templateContents
} else if legacyTemplatePath != nil {
issueState.Body = string(githubtemplate.ExtractContents(*legacyTemplatePath))
} else {
issueState.Body = defs.Body
}

View file

@ -10,8 +10,8 @@ import (
"gopkg.in/yaml.v3"
)
// Find returns the list of template file paths
func Find(rootDir string, name string) []string {
// FindNonLegacy returns the list of template file paths from the template folder (according to the "upgraded multiple template builder")
func FindNonLegacy(rootDir string, name string) []string {
results := []string{}
// https://help.github.com/en/github/building-a-strong-community/creating-a-pull-request-template-for-your-repository
@ -46,21 +46,34 @@ mainLoop:
break
}
}
}
sort.Strings(results)
return results
}
// FindLegacy returns the file path of the default(legacy) template
func FindLegacy(rootDir string, name string) *string {
// https://help.github.com/en/github/building-a-strong-community/creating-a-pull-request-template-for-your-repository
candidateDirs := []string{
path.Join(rootDir, ".github"),
rootDir,
path.Join(rootDir, "docs"),
}
for _, dir := range candidateDirs {
files, err := ioutil.ReadDir(dir)
if err != nil {
continue
}
// detect a single template file
for _, file := range files {
if strings.EqualFold(file.Name(), name+".md") {
results = append(results, path.Join(dir, file.Name()))
break
result := path.Join(dir, file.Name())
return &result
}
}
if len(results) > 0 {
break
}
}
sort.Strings(results)
return results
return nil
}
// ExtractName returns the name of the template from YAML front-matter

View file

@ -8,7 +8,7 @@ import (
"testing"
)
func TestFind(t *testing.T) {
func TestFindNonLegacy(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "gh-cli")
if err != nil {
t.Fatal(err)
@ -25,52 +25,69 @@ func TestFind(t *testing.T) {
want []string
}{
{
name: "Template in root",
name: "Legacy templates ignored",
prepare: []string{
"README.md",
"ISSUE_TEMPLATE",
"issue_template.md",
"issue_template.txt",
"pull_request_template.md",
},
args: args{
rootDir: tmpdir,
name: "ISSUE_TEMPLATE",
},
want: []string{
path.Join(tmpdir, "issue_template.md"),
},
},
{
name: "Template in .github takes precedence",
prepare: []string{
"ISSUE_TEMPLATE.md",
".github/issue_template.md",
},
args: args{
rootDir: tmpdir,
name: "ISSUE_TEMPLATE",
},
want: []string{
path.Join(tmpdir, ".github/issue_template.md"),
},
},
{
name: "Template in docs",
prepare: []string{
"README.md",
"docs/issue_template.md",
},
args: args{
rootDir: tmpdir,
name: "ISSUE_TEMPLATE",
},
want: []string{},
},
{
name: "Template folder in .github takes precedence",
prepare: []string{
"ISSUE_TEMPLATE.md",
"docs/ISSUE_TEMPLATE/abc.md",
"ISSUE_TEMPLATE/abc.md",
".github/ISSUE_TEMPLATE/abc.md",
},
args: args{
rootDir: tmpdir,
name: "ISSUE_TEMPLATE",
},
want: []string{
path.Join(tmpdir, "docs/issue_template.md"),
path.Join(tmpdir, ".github/ISSUE_TEMPLATE/abc.md"),
},
},
{
name: "Multiple templates",
name: "Template folder in root",
prepare: []string{
"ISSUE_TEMPLATE.md",
"docs/ISSUE_TEMPLATE/abc.md",
"ISSUE_TEMPLATE/abc.md",
},
args: args{
rootDir: tmpdir,
name: "ISSUE_TEMPLATE",
},
want: []string{
path.Join(tmpdir, "ISSUE_TEMPLATE/abc.md"),
},
},
{
name: "Template folder in docs",
prepare: []string{
"ISSUE_TEMPLATE.md",
"docs/ISSUE_TEMPLATE/abc.md",
},
args: args{
rootDir: tmpdir,
name: "ISSUE_TEMPLATE",
},
want: []string{
path.Join(tmpdir, "docs/ISSUE_TEMPLATE/abc.md"),
},
},
{
name: "Multiple templates in template folder",
prepare: []string{
".github/ISSUE_TEMPLATE/nope.md",
".github/PULL_REQUEST_TEMPLATE.md",
@ -90,18 +107,17 @@ func TestFind(t *testing.T) {
},
},
{
name: "Empty multiple templates directory",
name: "Empty template directories",
prepare: []string{
".github/issue_template.md",
".github/issue_template/.keep",
".github/ISSUE_TEMPLATE/.keep",
".docs/ISSUE_TEMPLATE/.keep",
"ISSUE_TEMPLATE/.keep",
},
args: args{
rootDir: tmpdir,
name: "ISSUE_TEMPLATE",
},
want: []string{
path.Join(tmpdir, ".github/issue_template.md"),
},
want: []string{},
},
}
for _, tt := range tests {
@ -116,7 +132,99 @@ func TestFind(t *testing.T) {
file.Close()
}
if got := Find(tt.args.rootDir, tt.args.name); !reflect.DeepEqual(got, tt.want) {
if got := FindNonLegacy(tt.args.rootDir, tt.args.name); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Find() = %v, want %v", got, tt.want)
}
})
os.RemoveAll(tmpdir)
}
}
func TestFindLegacy(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "gh-cli")
if err != nil {
t.Fatal(err)
}
type args struct {
rootDir string
name string
}
tests := []struct {
name string
prepare []string
args args
want string
}{
{
name: "Template in root",
prepare: []string{
"README.md",
"ISSUE_TEMPLATE",
"issue_template.md",
"issue_template.txt",
"pull_request_template.md",
"docs/issue_template.md",
},
args: args{
rootDir: tmpdir,
name: "ISSUE_TEMPLATE",
},
want: path.Join(tmpdir, "issue_template.md"),
},
{
name: "Template in .github takes precedence",
prepare: []string{
"ISSUE_TEMPLATE.md",
".github/issue_template.md",
"docs/issue_template.md",
},
args: args{
rootDir: tmpdir,
name: "ISSUE_TEMPLATE",
},
want: path.Join(tmpdir, ".github/issue_template.md"),
},
{
name: "Template in docs",
prepare: []string{
"README.md",
"docs/issue_template.md",
},
args: args{
rootDir: tmpdir,
name: "ISSUE_TEMPLATE",
},
want: path.Join(tmpdir, "docs/issue_template.md"),
},
{
name: "Non legacy templates ignored",
prepare: []string{
".github/PULL_REQUEST_TEMPLATE/abc.md",
"PULL_REQUEST_TEMPLATE/abc.md",
"docs/PULL_REQUEST_TEMPLATE/abc.md",
".github/PULL_REQUEST_TEMPLATE.md",
},
args: args{
rootDir: tmpdir,
name: "PuLl_ReQuEsT_TeMpLaTe",
},
want: path.Join(tmpdir, ".github/PULL_REQUEST_TEMPLATE.md"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, p := range tt.prepare {
fp := path.Join(tmpdir, p)
_ = os.MkdirAll(path.Dir(fp), 0700)
file, err := os.Create(fp)
if err != nil {
t.Fatal(err)
}
file.Close()
}
if got := FindLegacy(tt.args.rootDir, tt.args.name); *got != tt.want {
t.Errorf("Find() = %v, want %v", got, tt.want)
}
})