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  }