github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/pr/shared/templates.go (about)

     1  package shared
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"time"
     8  
     9  	"github.com/AlecAivazis/survey/v2"
    10  	"github.com/cli/cli/api"
    11  	"github.com/cli/cli/git"
    12  	"github.com/cli/cli/internal/ghinstance"
    13  	"github.com/cli/cli/internal/ghrepo"
    14  	"github.com/cli/cli/pkg/githubtemplate"
    15  	"github.com/cli/cli/pkg/prompt"
    16  	"github.com/shurcooL/githubv4"
    17  	"github.com/shurcooL/graphql"
    18  )
    19  
    20  type issueTemplate struct {
    21  	// I would have un-exported these fields, except `shurcool/graphql` then cannot unmarshal them :/
    22  	Gname string `graphql:"name"`
    23  	Gbody string `graphql:"body"`
    24  }
    25  
    26  func (t *issueTemplate) Name() string {
    27  	return t.Gname
    28  }
    29  
    30  func (t *issueTemplate) NameForSubmit() string {
    31  	return t.Gname
    32  }
    33  
    34  func (t *issueTemplate) Body() []byte {
    35  	return []byte(t.Gbody)
    36  }
    37  
    38  func listIssueTemplates(httpClient *http.Client, repo ghrepo.Interface) ([]issueTemplate, error) {
    39  	var query struct {
    40  		Repository struct {
    41  			IssueTemplates []issueTemplate
    42  		} `graphql:"repository(owner: $owner, name: $name)"`
    43  	}
    44  
    45  	variables := map[string]interface{}{
    46  		"owner": githubv4.String(repo.RepoOwner()),
    47  		"name":  githubv4.String(repo.RepoName()),
    48  	}
    49  
    50  	gql := graphql.NewClient(ghinstance.GraphQLEndpoint(repo.RepoHost()), httpClient)
    51  
    52  	err := gql.QueryNamed(context.Background(), "IssueTemplates", &query, variables)
    53  	if err != nil {
    54  		return nil, err
    55  	}
    56  
    57  	return query.Repository.IssueTemplates, nil
    58  }
    59  
    60  func hasIssueTemplateSupport(httpClient *http.Client, hostname string) (bool, error) {
    61  	if !ghinstance.IsEnterprise(hostname) {
    62  		return true, nil
    63  	}
    64  
    65  	var featureDetection struct {
    66  		Repository struct {
    67  			Fields []struct {
    68  				Name string
    69  			} `graphql:"fields(includeDeprecated: true)"`
    70  		} `graphql:"Repository: __type(name: \"Repository\")"`
    71  		CreateIssueInput struct {
    72  			InputFields []struct {
    73  				Name string
    74  			}
    75  		} `graphql:"CreateIssueInput: __type(name: \"CreateIssueInput\")"`
    76  	}
    77  
    78  	gql := graphql.NewClient(ghinstance.GraphQLEndpoint(hostname), httpClient)
    79  	err := gql.QueryNamed(context.Background(), "IssueTemplates_fields", &featureDetection, nil)
    80  	if err != nil {
    81  		return false, err
    82  	}
    83  
    84  	var hasQuerySupport bool
    85  	var hasMutationSupport bool
    86  	for _, field := range featureDetection.Repository.Fields {
    87  		if field.Name == "issueTemplates" {
    88  			hasQuerySupport = true
    89  		}
    90  	}
    91  	for _, field := range featureDetection.CreateIssueInput.InputFields {
    92  		if field.Name == "issueTemplate" {
    93  			hasMutationSupport = true
    94  		}
    95  	}
    96  
    97  	return hasQuerySupport && hasMutationSupport, nil
    98  }
    99  
   100  type Template interface {
   101  	Name() string
   102  	NameForSubmit() string
   103  	Body() []byte
   104  }
   105  
   106  type templateManager struct {
   107  	repo       ghrepo.Interface
   108  	rootDir    string
   109  	allowFS    bool
   110  	isPR       bool
   111  	httpClient *http.Client
   112  
   113  	cachedClient   *http.Client
   114  	templates      []Template
   115  	legacyTemplate Template
   116  
   117  	didFetch   bool
   118  	fetchError error
   119  }
   120  
   121  func NewTemplateManager(httpClient *http.Client, repo ghrepo.Interface, dir string, allowFS bool, isPR bool) *templateManager {
   122  	return &templateManager{
   123  		repo:       repo,
   124  		rootDir:    dir,
   125  		allowFS:    allowFS,
   126  		isPR:       isPR,
   127  		httpClient: httpClient,
   128  	}
   129  }
   130  
   131  func (m *templateManager) hasAPI() (bool, error) {
   132  	if m.isPR {
   133  		return false, nil
   134  	}
   135  	if m.cachedClient == nil {
   136  		m.cachedClient = api.NewCachedClient(m.httpClient, time.Hour*24)
   137  	}
   138  	return hasIssueTemplateSupport(m.cachedClient, m.repo.RepoHost())
   139  }
   140  
   141  func (m *templateManager) HasTemplates() (bool, error) {
   142  	if err := m.memoizedFetch(); err != nil {
   143  		return false, err
   144  	}
   145  	return len(m.templates) > 0, nil
   146  }
   147  
   148  func (m *templateManager) LegacyBody() []byte {
   149  	if m.legacyTemplate == nil {
   150  		return nil
   151  	}
   152  	return m.legacyTemplate.Body()
   153  }
   154  
   155  func (m *templateManager) Choose() (Template, error) {
   156  	if err := m.memoizedFetch(); err != nil {
   157  		return nil, err
   158  	}
   159  	if len(m.templates) == 0 {
   160  		return nil, nil
   161  	}
   162  
   163  	names := make([]string, len(m.templates))
   164  	for i, t := range m.templates {
   165  		names[i] = t.Name()
   166  	}
   167  
   168  	blankOption := "Open a blank issue"
   169  	if m.isPR {
   170  		blankOption = "Open a blank pull request"
   171  	}
   172  
   173  	var selectedOption int
   174  	err := prompt.SurveyAskOne(&survey.Select{
   175  		Message: "Choose a template",
   176  		Options: append(names, blankOption),
   177  	}, &selectedOption)
   178  	if err != nil {
   179  		return nil, fmt.Errorf("could not prompt: %w", err)
   180  	}
   181  
   182  	if selectedOption == len(names) {
   183  		return nil, nil
   184  	}
   185  	return m.templates[selectedOption], nil
   186  }
   187  
   188  func (m *templateManager) memoizedFetch() error {
   189  	if m.didFetch {
   190  		return m.fetchError
   191  	}
   192  	m.fetchError = m.fetch()
   193  	m.didFetch = true
   194  	return m.fetchError
   195  }
   196  
   197  func (m *templateManager) fetch() error {
   198  	hasAPI, err := m.hasAPI()
   199  	if err != nil {
   200  		return err
   201  	}
   202  
   203  	if hasAPI {
   204  		issueTemplates, err := listIssueTemplates(m.httpClient, m.repo)
   205  		if err != nil {
   206  			return err
   207  		}
   208  		m.templates = make([]Template, len(issueTemplates))
   209  		for i := range issueTemplates {
   210  			m.templates[i] = &issueTemplates[i]
   211  		}
   212  	}
   213  
   214  	if !m.allowFS {
   215  		return nil
   216  	}
   217  
   218  	dir := m.rootDir
   219  	if dir == "" {
   220  		var err error
   221  		dir, err = git.ToplevelDir()
   222  		if err != nil {
   223  			return nil // abort silently
   224  		}
   225  	}
   226  
   227  	filePattern := "ISSUE_TEMPLATE"
   228  	if m.isPR {
   229  		filePattern = "PULL_REQUEST_TEMPLATE"
   230  	}
   231  
   232  	if !hasAPI {
   233  		issueTemplates := githubtemplate.FindNonLegacy(dir, filePattern)
   234  		m.templates = make([]Template, len(issueTemplates))
   235  		for i, t := range issueTemplates {
   236  			m.templates[i] = &filesystemTemplate{path: t}
   237  		}
   238  	}
   239  
   240  	if legacyTemplate := githubtemplate.FindLegacy(dir, filePattern); legacyTemplate != "" {
   241  		m.legacyTemplate = &filesystemTemplate{path: legacyTemplate}
   242  	}
   243  
   244  	return nil
   245  }
   246  
   247  type filesystemTemplate struct {
   248  	path string
   249  }
   250  
   251  func (t *filesystemTemplate) Name() string {
   252  	return githubtemplate.ExtractName(t.path)
   253  }
   254  
   255  func (t *filesystemTemplate) NameForSubmit() string {
   256  	return ""
   257  }
   258  
   259  func (t *filesystemTemplate) Body() []byte {
   260  	return githubtemplate.ExtractContents(t.path)
   261  }