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 }