Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Pull Request template support for create-pr command #101

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/create_pull_request/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type createPullRequestFlags struct {
NoDraft bool
NoCloseIssue bool
UseDefaultValues bool
FromTemplate bool
}

var flags createPullRequestFlags
Expand All @@ -43,6 +44,7 @@ func init() {
Command.PersistentFlags().BoolVar(&flags.NoFetch, "no-fetch", false, "does not fetch the base branch")
Command.PersistentFlags().BoolVar(&flags.NoDraft, "no-draft", false, "create the pull request in ready for review mode")
Command.PersistentFlags().BoolVarP(&flags.NoCloseIssue, "no-close-issue", "n", false, "do not close the GitHub issue after merging the pull request")
Command.PersistentFlags().BoolVarP(&flags.FromTemplate, "from-template", "t", false, "create the pull request using the repository's pull request template")
}

func runCommand(cmd *cobra.Command, _ []string) error {
Expand Down Expand Up @@ -83,6 +85,7 @@ func runCommand(cmd *cobra.Command, _ []string) error {
IsInteractive: isInteractive,
DraftPR: !flags.NoDraft,
CloseIssue: !flags.NoCloseIssue,
FromTemplate: flags.FromTemplate,
}
createPullRequestUseCase := use_cases.CreatePullRequest{
Cfg: createPullRequestConfig,
Expand Down
7 changes: 7 additions & 0 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ gh sherpa create-pr, cpr [flags]
* `--yes, -y`: The pull request will be created without confirmation.
* `--no-draft`: The pull request will be created in ready for review mode. By default is in draft mode.
* `--no-close-issue`: The GitHub issue will not be closed when the pull request is merged. By default is closed.
* `--from-template`: Use a pull request template from the repository.

### Possible scenarios

Expand Down Expand Up @@ -121,3 +122,9 @@ gh sherpa create-pr --issue SHERPA-81 --base main
```sh
gh sherpa create-pr --issue 750 --no-close-issue
```

#### Create a branch and pull request using pull request template

```sh
gh sherpa create-pr --issue 750 --from-template
```
1 change: 1 addition & 0 deletions internal/domain/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package domain

type RepositoryProvider interface {
GetRepository() (repo *Repository, err error)
GetPullRequestTemplate() (template string, err error)
}

type PullRequestProvider interface {
Expand Down
40 changes: 23 additions & 17 deletions internal/fakes/domain/fake_pull_request_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ import (
type FakePullRequestProvider struct {
PullRequests map[string]*domain.PullRequest
PullRequestsWithErrors []string
CreatedPRs []CreatedPR
}

type CreatedPR struct {
Title string
Body string
BaseBranch string
HeadBranch string
Draft bool
Labels []string
}

var _ domain.PullRequestProvider = (*FakePullRequestProvider)(nil)
Expand All @@ -19,6 +29,7 @@ func NewFakePullRequestProvider() *FakePullRequestProvider {
return &FakePullRequestProvider{
PullRequests: map[string]*domain.PullRequest{},
PullRequestsWithErrors: []string{},
CreatedPRs: []CreatedPR{},
}
}

Expand Down Expand Up @@ -49,27 +60,22 @@ func (f *FakePullRequestProvider) CreatePullRequest(title string, body string, b
return "", ErrPullRequestWithError
}

pr := f.PullRequests[headBranch]
if pr != nil && !pr.Closed {
return "", ErrPrAlreadyExists(headBranch)
}
f.CreatedPRs = append(f.CreatedPRs, CreatedPR{
Title: title,
Body: body,
BaseBranch: baseBranch,
HeadBranch: headBranch,
Draft: draft,
Labels: labels,
})

prLabels := make([]domain.Label, len(labels))
for i, label := range labels {
prLabels[i] = domain.Label{
Id: label,
Name: label,
}
}
pr = &domain.PullRequest{
pr := &domain.PullRequest{
Title: title,
Number: 5,
State: "OPEN",
Closed: false,
Url: "https://github.com/inditextech/gh-sherpa-test-repo/pulls/5",
HeadRefName: headBranch,
BaseRefName: baseBranch,
Labels: prLabels,
State: "OPEN",
Closed: false,
Url: fmt.Sprintf("https://github.com/owner/repo/pull/%d", len(f.PullRequests)+1),
}

f.PullRequests[headBranch] = pr
Expand Down
11 changes: 10 additions & 1 deletion internal/fakes/domain/fake_repository_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (
)

type FakeRepositoryProvider struct {
Repository *domain.Repository
Repository *domain.Repository
Template string
TemplateError error
}

var _ domain.RepositoryProvider = (*FakeRepositoryProvider)(nil)
Expand All @@ -31,3 +33,10 @@ func (f *FakeRepositoryProvider) GetRepository() (repo *domain.Repository, err e
}
return nil, ErrRepositoryNotFound
}

func (f *FakeRepositoryProvider) GetPullRequestTemplate() (template string, err error) {
if f.TemplateError != nil {
return "", f.TemplateError
}
return f.Template, nil
}
56 changes: 56 additions & 0 deletions internal/gh/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gh

import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -144,3 +145,58 @@ func (c *Cli) GetPullRequestForBranch(branchName string) (*domain.PullRequest, e

return &pr, nil
}

func (c *Cli) GetPullRequestTemplate() (template string, err error) {
templatePaths := []string{
".github/pull_request_template.md",
".github/PULL_REQUEST_TEMPLATE.md",
"docs/pull_request_template.md",
"docs/PULL_REQUEST_TEMPLATE.md",
"pull_request_template.md",
"PULL_REQUEST_TEMPLATE.md",
}

args := []string{"api", "/repos/{owner}/{repo}/contents/.github/PULL_REQUEST_TEMPLATE"}
stdout, stderr, err := gh.Exec(args...)
if err == nil && stderr.String() == "" {
var contents []struct {
Name string `json:"name"`
Path string `json:"path"`
Content string `json:"content"`
Encoding string `json:"encoding"`
}
if err := json.Unmarshal(stdout.Bytes(), &contents); err == nil {
for _, content := range contents {
if strings.HasSuffix(content.Name, ".md") {
templatePaths = append(templatePaths, content.Path)
}
}
}
}

for _, path := range templatePaths {
args := []string{"api", fmt.Sprintf("/repos/{owner}/{repo}/contents/%s", path)}
stdout, stderr, err := gh.Exec(args...)
if err == nil && stderr.String() == "" {
var response struct {
Content string `json:"content"`
Encoding string `json:"encoding"`
}

if err := json.Unmarshal(stdout.Bytes(), &response); err != nil {
continue
}

if response.Encoding == "base64" {
decoded, err := base64.StdEncoding.DecodeString(response.Content)
if err != nil {
continue
}
return string(decoded), nil
}
return response.Content, nil
}
}

return "", nil
}
51 changes: 51 additions & 0 deletions internal/mocks/domain/mock_RepositoryProvider.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 30 additions & 4 deletions internal/use_cases/create_pull_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type CreatePullRequestConfiguration struct {
DraftPR bool
IsInteractive bool
CloseIssue bool
FromTemplate bool
}

type CreatePullRequest struct {
Expand Down Expand Up @@ -213,11 +214,36 @@ func (cpr *CreatePullRequest) getPullRequestTitleAndBody(issue domain.Issue) (ti
case domain.IssueTrackerTypeGithub:
title = issue.Title()

keyword := "Related to"
if cpr.Cfg.CloseIssue {
keyword = "Closes"
if cpr.Cfg.FromTemplate {
// Get PR template if requested
template, err := cpr.RepositoryProvider.GetPullRequestTemplate()
if err != nil {
return "", "", fmt.Errorf("error getting PR template: %w", err)
}

// If template exists, use it as base and append issue reference
if template != "" {
keyword := "Related to"
if cpr.Cfg.CloseIssue {
keyword = "Closes"
}
body = fmt.Sprintf("%s\n\n%s #%s", template, keyword, issue.ID())
} else {
// If no template found, use default format
keyword := "Related to"
if cpr.Cfg.CloseIssue {
keyword = "Closes"
}
body = fmt.Sprintf("%s #%s", keyword, issue.ID())
}
} else {
// Use default format if template not requested
keyword := "Related to"
if cpr.Cfg.CloseIssue {
keyword = "Closes"
}
body = fmt.Sprintf("%s #%s", keyword, issue.ID())
}
body = fmt.Sprintf("%s #%s", keyword, issue.ID())

case domain.IssueTrackerTypeJira:
title = fmt.Sprintf("[%s] %s", issue.ID(), issue.Title())
Expand Down
Loading
Loading