github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/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/ungtb10d/cli/v2/api" 11 "github.com/ungtb10d/cli/v2/git" 12 fd "github.com/ungtb10d/cli/v2/internal/featuredetection" 13 "github.com/ungtb10d/cli/v2/internal/ghrepo" 14 "github.com/ungtb10d/cli/v2/pkg/githubtemplate" 15 "github.com/ungtb10d/cli/v2/pkg/prompt" 16 "github.com/shurcooL/githubv4" 17 ) 18 19 type issueTemplate struct { 20 Gname string `graphql:"name"` 21 Gbody string `graphql:"body"` 22 } 23 24 type pullRequestTemplate struct { 25 Gname string `graphql:"filename"` 26 Gbody string `graphql:"body"` 27 } 28 29 func (t *issueTemplate) Name() string { 30 return t.Gname 31 } 32 33 func (t *issueTemplate) NameForSubmit() string { 34 return t.Gname 35 } 36 37 func (t *issueTemplate) Body() []byte { 38 return []byte(t.Gbody) 39 } 40 41 func (t *pullRequestTemplate) Name() string { 42 return t.Gname 43 } 44 45 func (t *pullRequestTemplate) NameForSubmit() string { 46 return "" 47 } 48 49 func (t *pullRequestTemplate) Body() []byte { 50 return []byte(t.Gbody) 51 } 52 53 func listIssueTemplates(httpClient *http.Client, repo ghrepo.Interface) ([]Template, error) { 54 var query struct { 55 Repository struct { 56 IssueTemplates []issueTemplate 57 } `graphql:"repository(owner: $owner, name: $name)"` 58 } 59 60 variables := map[string]interface{}{ 61 "owner": githubv4.String(repo.RepoOwner()), 62 "name": githubv4.String(repo.RepoName()), 63 } 64 65 gql := api.NewClientFromHTTP(httpClient) 66 67 err := gql.Query(repo.RepoHost(), "IssueTemplates", &query, variables) 68 if err != nil { 69 return nil, err 70 } 71 72 ts := query.Repository.IssueTemplates 73 templates := make([]Template, len(ts)) 74 for i := range templates { 75 templates[i] = &ts[i] 76 } 77 78 return templates, nil 79 } 80 81 func listPullRequestTemplates(httpClient *http.Client, repo ghrepo.Interface) ([]Template, error) { 82 var query struct { 83 Repository struct { 84 PullRequestTemplates []pullRequestTemplate 85 } `graphql:"repository(owner: $owner, name: $name)"` 86 } 87 88 variables := map[string]interface{}{ 89 "owner": githubv4.String(repo.RepoOwner()), 90 "name": githubv4.String(repo.RepoName()), 91 } 92 93 gql := api.NewClientFromHTTP(httpClient) 94 95 err := gql.Query(repo.RepoHost(), "PullRequestTemplates", &query, variables) 96 if err != nil { 97 return nil, err 98 } 99 100 ts := query.Repository.PullRequestTemplates 101 templates := make([]Template, len(ts)) 102 for i := range templates { 103 templates[i] = &ts[i] 104 } 105 106 return templates, nil 107 } 108 109 type Template interface { 110 Name() string 111 NameForSubmit() string 112 Body() []byte 113 } 114 115 type templateManager struct { 116 repo ghrepo.Interface 117 rootDir string 118 allowFS bool 119 isPR bool 120 httpClient *http.Client 121 detector fd.Detector 122 123 templates []Template 124 legacyTemplate Template 125 126 didFetch bool 127 fetchError error 128 } 129 130 func NewTemplateManager(httpClient *http.Client, repo ghrepo.Interface, dir string, allowFS bool, isPR bool) *templateManager { 131 cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) 132 return &templateManager{ 133 repo: repo, 134 rootDir: dir, 135 allowFS: allowFS, 136 isPR: isPR, 137 httpClient: httpClient, 138 detector: fd.NewDetector(cachedClient, repo.RepoHost()), 139 } 140 } 141 142 func (m *templateManager) hasAPI() (bool, error) { 143 if !m.isPR { 144 return true, nil 145 } 146 147 features, err := m.detector.RepositoryFeatures() 148 if err != nil { 149 return false, err 150 } 151 152 return features.PullRequestTemplateQuery, nil 153 } 154 155 func (m *templateManager) HasTemplates() (bool, error) { 156 if err := m.memoizedFetch(); err != nil { 157 return false, err 158 } 159 return len(m.templates) > 0, nil 160 } 161 162 func (m *templateManager) LegacyBody() []byte { 163 if m.legacyTemplate == nil { 164 return nil 165 } 166 return m.legacyTemplate.Body() 167 } 168 169 func (m *templateManager) Choose() (Template, error) { 170 if err := m.memoizedFetch(); err != nil { 171 return nil, err 172 } 173 if len(m.templates) == 0 { 174 return nil, nil 175 } 176 177 names := make([]string, len(m.templates)) 178 for i, t := range m.templates { 179 names[i] = t.Name() 180 } 181 182 blankOption := "Open a blank issue" 183 if m.isPR { 184 blankOption = "Open a blank pull request" 185 } 186 187 var selectedOption int 188 //nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter 189 err := prompt.SurveyAskOne(&survey.Select{ 190 Message: "Choose a template", 191 Options: append(names, blankOption), 192 }, &selectedOption) 193 if err != nil { 194 return nil, fmt.Errorf("could not prompt: %w", err) 195 } 196 197 if selectedOption == len(names) { 198 return nil, nil 199 } 200 return m.templates[selectedOption], nil 201 } 202 203 func (m *templateManager) memoizedFetch() error { 204 if m.didFetch { 205 return m.fetchError 206 } 207 m.fetchError = m.fetch() 208 m.didFetch = true 209 return m.fetchError 210 } 211 212 func (m *templateManager) fetch() error { 213 hasAPI, err := m.hasAPI() 214 if err != nil { 215 return err 216 } 217 218 if hasAPI { 219 lister := listIssueTemplates 220 if m.isPR { 221 lister = listPullRequestTemplates 222 } 223 templates, err := lister(m.httpClient, m.repo) 224 if err != nil { 225 return err 226 } 227 m.templates = templates 228 } 229 230 if !m.allowFS { 231 return nil 232 } 233 234 dir := m.rootDir 235 if dir == "" { 236 var err error 237 gitClient := &git.Client{} 238 dir, err = gitClient.ToplevelDir(context.Background()) 239 if err != nil { 240 return nil // abort silently 241 } 242 } 243 244 filePattern := "ISSUE_TEMPLATE" 245 if m.isPR { 246 filePattern = "PULL_REQUEST_TEMPLATE" 247 } 248 249 if !hasAPI { 250 issueTemplates := githubtemplate.FindNonLegacy(dir, filePattern) 251 m.templates = make([]Template, len(issueTemplates)) 252 for i, t := range issueTemplates { 253 m.templates[i] = &filesystemTemplate{path: t} 254 } 255 } 256 257 if legacyTemplate := githubtemplate.FindLegacy(dir, filePattern); legacyTemplate != "" { 258 m.legacyTemplate = &filesystemTemplate{path: legacyTemplate} 259 } 260 261 return nil 262 } 263 264 type filesystemTemplate struct { 265 path string 266 } 267 268 func (t *filesystemTemplate) Name() string { 269 return githubtemplate.ExtractName(t.path) 270 } 271 272 func (t *filesystemTemplate) NameForSubmit() string { 273 return "" 274 } 275 276 func (t *filesystemTemplate) Body() []byte { 277 return githubtemplate.ExtractContents(t.path) 278 }