mirror of
https://github.com/Pull-Pal/pull-pal.git
synced 2025-01-17 23:06:37 -05:00
Multirepo support and lots of other stuff (#3)
* Parsing issue into llm request moved to `vc` package * Converted "issue ID" string to "issue number" int * Added config struct to `pullpal/common.go` * Added multi-repo config support * Added support for custom base branches for PRs * Added configurable wait time * Bot should comment on pull requests and threads when running into an error
This commit is contained in:
parent
ab7521477a
commit
9678a1c961
11
README.md
11
README.md
@ -22,17 +22,16 @@ The minimal configuration you need looks like this:
|
|||||||
```
|
```
|
||||||
handle: [username of your bot's Github account]
|
handle: [username of your bot's Github account]
|
||||||
email: [email of your bot's Github account]
|
email: [email of your bot's Github account]
|
||||||
repo-handle: [username of repository owner's Github account]
|
repos: [list of repositories, e.g. "github.com/owner/name" that bot will monitor]
|
||||||
repo-name: [name of repository on Github]
|
users-to-listen-to: [list of Github users who your bot will interact with on Github issues and PRs]
|
||||||
users-to-listen-to: [comma-separated list of Github users who your bot will interact with on Github issues and PRs]
|
required-issue-labels: [list of issue labels that an issue must have in order to be considered by the bot (can be empty)]
|
||||||
required-issue-labels: [comma-separated list of issue labels that an issue must have in order to be considered by the bot (can be empty)]
|
|
||||||
github-token: ghp_xxx
|
github-token: ghp_xxx
|
||||||
open-ai-token: sk-xxx
|
open-ai-token: sk-xxx
|
||||||
```
|
```
|
||||||
|
|
||||||
You can acquire the Github token under "developer settings" in Github, in the "personal access tokens" section. The necessary requirements are:
|
You can acquire the Github token under "developer settings" in Github, in the "personal access tokens" section. The necessary requirements are:
|
||||||
* this token must be created for the same account associated with `handle` and `email` in your config. Important to remember if you are using a separate Github account for your bot
|
* this token must be created for the same account associated with `handle` and `email` in your config. Important to remember if you are using a separate Github account for your bot
|
||||||
* the token must have permissions to interact with the repository at github.com/[repo-handle]/[repo-name]
|
* the token must have permissions to interact with the repositories configured in `repos`
|
||||||
* the token must have read and write permission to commit statuses, repository contents, discussions, issues, and pull requests
|
* the token must have read and write permission to commit statuses, repository contents, discussions, issues, and pull requests
|
||||||
|
|
||||||
You can generate an API key for OpenAI by logging in to platform.openai.com, then going to https://platform.openai.com/account/api-keys
|
You can generate an API key for OpenAI by logging in to platform.openai.com, then going to https://platform.openai.com/account/api-keys
|
||||||
@ -72,6 +71,8 @@ Add an index.html file. It should have a content section populated with a headin
|
|||||||
|
|
||||||
Add a main.go file that serves index.html on port 8080.
|
Add a main.go file that serves index.html on port 8080.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
Files: main.go, index.html
|
Files: main.go, index.html
|
||||||
```
|
```
|
||||||
|
|
||||||
|
45
cmd/root.go
45
cmd/root.go
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mobyvb/pull-pal/pullpal"
|
"github.com/mobyvb/pull-pal/pullpal"
|
||||||
"github.com/mobyvb/pull-pal/vc"
|
"github.com/mobyvb/pull-pal/vc"
|
||||||
@ -23,17 +24,15 @@ type config struct {
|
|||||||
openAIToken string
|
openAIToken string
|
||||||
|
|
||||||
// remote repo info
|
// remote repo info
|
||||||
repoDomain string
|
repos []string
|
||||||
repoHandle string
|
|
||||||
repoName string
|
|
||||||
|
|
||||||
// local paths
|
// local paths
|
||||||
localRepoPath string
|
localRepoPath string
|
||||||
|
|
||||||
// program settings
|
// program settings
|
||||||
promptToClipboard bool
|
|
||||||
usersToListenTo []string
|
usersToListenTo []string
|
||||||
requiredIssueLabels []string
|
requiredIssueLabels []string
|
||||||
|
waitDuration time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConfig() config {
|
func getConfig() config {
|
||||||
@ -43,15 +42,13 @@ func getConfig() config {
|
|||||||
githubToken: viper.GetString("github-token"),
|
githubToken: viper.GetString("github-token"),
|
||||||
openAIToken: viper.GetString("open-ai-token"),
|
openAIToken: viper.GetString("open-ai-token"),
|
||||||
|
|
||||||
repoDomain: viper.GetString("repo-domain"),
|
repos: viper.GetStringSlice("repos"),
|
||||||
repoHandle: viper.GetString("repo-handle"),
|
|
||||||
repoName: viper.GetString("repo-name"),
|
|
||||||
|
|
||||||
localRepoPath: viper.GetString("local-repo-path"),
|
localRepoPath: viper.GetString("local-repo-path"),
|
||||||
|
|
||||||
promptToClipboard: viper.GetBool("prompt-to-clipboard"),
|
|
||||||
usersToListenTo: viper.GetStringSlice("users-to-listen-to"),
|
usersToListenTo: viper.GetStringSlice("users-to-listen-to"),
|
||||||
requiredIssueLabels: viper.GetStringSlice("required-issue-labels"),
|
requiredIssueLabels: viper.GetStringSlice("required-issue-labels"),
|
||||||
|
waitDuration: viper.GetDuration("wait-duration"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,20 +65,22 @@ func getPullPal(ctx context.Context, cfg config) (*pullpal.PullPal, error) {
|
|||||||
Handle: cfg.selfHandle,
|
Handle: cfg.selfHandle,
|
||||||
Token: cfg.githubToken,
|
Token: cfg.githubToken,
|
||||||
}
|
}
|
||||||
repo := vc.Repository{
|
|
||||||
LocalPath: cfg.localRepoPath,
|
|
||||||
HostDomain: cfg.repoDomain,
|
|
||||||
Name: cfg.repoName,
|
|
||||||
Owner: vc.Author{
|
|
||||||
Handle: cfg.repoHandle,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
listIssueOptions := vc.ListIssueOptions{
|
listIssueOptions := vc.ListIssueOptions{
|
||||||
Handles: cfg.usersToListenTo,
|
Handles: cfg.usersToListenTo,
|
||||||
Labels: cfg.requiredIssueLabels,
|
Labels: cfg.requiredIssueLabels,
|
||||||
}
|
}
|
||||||
// TODO make model configurable
|
// TODO make model configurable
|
||||||
p, err := pullpal.NewPullPal(ctx, log.Named("pullpal"), listIssueOptions, author, repo, openai.GPT4, cfg.openAIToken)
|
ppCfg := pullpal.Config{
|
||||||
|
WaitDuration: cfg.waitDuration,
|
||||||
|
LocalRepoPath: cfg.localRepoPath,
|
||||||
|
Repos: cfg.repos,
|
||||||
|
Self: author,
|
||||||
|
ListIssueOptions: listIssueOptions,
|
||||||
|
// TODO configurable model
|
||||||
|
Model: openai.GPT4,
|
||||||
|
OpenAIToken: cfg.openAIToken,
|
||||||
|
}
|
||||||
|
p, err := pullpal.NewPullPal(ctx, log.Named("pullpal"), ppCfg)
|
||||||
|
|
||||||
return p, err
|
return p, err
|
||||||
}
|
}
|
||||||
@ -129,28 +128,26 @@ func init() {
|
|||||||
rootCmd.PersistentFlags().StringP("github-token", "t", "GITHUB TOKEN", "token for authenticating Github actions")
|
rootCmd.PersistentFlags().StringP("github-token", "t", "GITHUB TOKEN", "token for authenticating Github actions")
|
||||||
rootCmd.PersistentFlags().StringP("open-ai-token", "k", "OPENAI TOKEN", "token for authenticating OpenAI")
|
rootCmd.PersistentFlags().StringP("open-ai-token", "k", "OPENAI TOKEN", "token for authenticating OpenAI")
|
||||||
|
|
||||||
rootCmd.PersistentFlags().StringP("repo-domain", "d", "github.com", "domain for version control server")
|
rootCmd.PersistentFlags().StringSliceP("repos", "r", []string{}, "a list of git repositories that Pull Pal will monitor")
|
||||||
rootCmd.PersistentFlags().StringP("repo-handle", "o", "REPO-HANDLE", "handle of repository's owner on version control server")
|
|
||||||
rootCmd.PersistentFlags().StringP("repo-name", "n", "REPO-NAME", "name of repository on version control server")
|
|
||||||
|
|
||||||
rootCmd.PersistentFlags().StringP("local-repo-path", "l", "/tmp/pullpallrepo", "local path to check out ephemeral repository in")
|
rootCmd.PersistentFlags().StringP("local-repo-path", "l", "/tmp/pullpalrepo", "local path to check out ephemeral repository in")
|
||||||
|
|
||||||
rootCmd.PersistentFlags().StringSliceP("users-to-listen-to", "a", []string{}, "a list of Github users that Pull Pal will respond to")
|
rootCmd.PersistentFlags().StringSliceP("users-to-listen-to", "a", []string{}, "a list of Github users that Pull Pal will respond to")
|
||||||
rootCmd.PersistentFlags().StringSliceP("required-issue-labels", "i", []string{}, "a list of labels that are required for Pull Pal to select an issue")
|
rootCmd.PersistentFlags().StringSliceP("required-issue-labels", "i", []string{}, "a list of labels that are required for Pull Pal to select an issue")
|
||||||
|
rootCmd.PersistentFlags().Duration("wait-time", 30*time.Second, "the amount of time Pull Pal should wait when no issues or comments are found to address")
|
||||||
|
|
||||||
viper.BindPFlag("handle", rootCmd.PersistentFlags().Lookup("handle"))
|
viper.BindPFlag("handle", rootCmd.PersistentFlags().Lookup("handle"))
|
||||||
viper.BindPFlag("email", rootCmd.PersistentFlags().Lookup("email"))
|
viper.BindPFlag("email", rootCmd.PersistentFlags().Lookup("email"))
|
||||||
viper.BindPFlag("github-token", rootCmd.PersistentFlags().Lookup("github-token"))
|
viper.BindPFlag("github-token", rootCmd.PersistentFlags().Lookup("github-token"))
|
||||||
viper.BindPFlag("open-ai-token", rootCmd.PersistentFlags().Lookup("open-ai-token"))
|
viper.BindPFlag("open-ai-token", rootCmd.PersistentFlags().Lookup("open-ai-token"))
|
||||||
|
|
||||||
viper.BindPFlag("repo-domain", rootCmd.PersistentFlags().Lookup("repo-domain"))
|
viper.BindPFlag("repos", rootCmd.PersistentFlags().Lookup("repos"))
|
||||||
viper.BindPFlag("repo-handle", rootCmd.PersistentFlags().Lookup("repo-handle"))
|
|
||||||
viper.BindPFlag("repo-name", rootCmd.PersistentFlags().Lookup("repo-name"))
|
|
||||||
|
|
||||||
viper.BindPFlag("local-repo-path", rootCmd.PersistentFlags().Lookup("local-repo-path"))
|
viper.BindPFlag("local-repo-path", rootCmd.PersistentFlags().Lookup("local-repo-path"))
|
||||||
|
|
||||||
viper.BindPFlag("users-to-listen-to", rootCmd.PersistentFlags().Lookup("users-to-listen-to"))
|
viper.BindPFlag("users-to-listen-to", rootCmd.PersistentFlags().Lookup("users-to-listen-to"))
|
||||||
viper.BindPFlag("required-issue-labels", rootCmd.PersistentFlags().Lookup("required-issue-labels"))
|
viper.BindPFlag("required-issue-labels", rootCmd.PersistentFlags().Lookup("required-issue-labels"))
|
||||||
|
viper.BindPFlag("wait-time", rootCmd.PersistentFlags().Lookup("wait-time"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func initConfig() {
|
func initConfig() {
|
||||||
|
3
go.mod
3
go.mod
@ -8,6 +8,7 @@ require (
|
|||||||
github.com/sashabaranov/go-openai v1.9.0
|
github.com/sashabaranov/go-openai v1.9.0
|
||||||
github.com/spf13/cobra v1.7.0
|
github.com/spf13/cobra v1.7.0
|
||||||
github.com/spf13/viper v1.15.0
|
github.com/spf13/viper v1.15.0
|
||||||
|
github.com/stretchr/testify v1.8.1
|
||||||
go.uber.org/zap v1.24.0
|
go.uber.org/zap v1.24.0
|
||||||
golang.org/x/oauth2 v0.7.0
|
golang.org/x/oauth2 v0.7.0
|
||||||
)
|
)
|
||||||
@ -17,6 +18,7 @@ require (
|
|||||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
|
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
|
||||||
github.com/acomagu/bufpipe v1.0.4 // indirect
|
github.com/acomagu/bufpipe v1.0.4 // indirect
|
||||||
github.com/cloudflare/circl v1.1.0 // indirect
|
github.com/cloudflare/circl v1.1.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||||
github.com/go-git/gcfg v1.5.0 // indirect
|
github.com/go-git/gcfg v1.5.0 // indirect
|
||||||
@ -32,6 +34,7 @@ require (
|
|||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/sergi/go-diff v1.1.0 // indirect
|
github.com/sergi/go-diff v1.1.0 // indirect
|
||||||
github.com/skeema/knownhosts v1.1.0 // indirect
|
github.com/skeema/knownhosts v1.1.0 // indirect
|
||||||
github.com/spf13/afero v1.9.3 // indirect
|
github.com/spf13/afero v1.9.3 // indirect
|
||||||
|
@ -19,10 +19,11 @@ const (
|
|||||||
|
|
||||||
// CodeChangeRequest contains all necessary information for generating a prompt for a LLM.
|
// CodeChangeRequest contains all necessary information for generating a prompt for a LLM.
|
||||||
type CodeChangeRequest struct {
|
type CodeChangeRequest struct {
|
||||||
Files []File
|
Files []File
|
||||||
Subject string
|
Subject string
|
||||||
Body string
|
Body string
|
||||||
IssueID string
|
IssueNumber int
|
||||||
|
BaseBranch string
|
||||||
}
|
}
|
||||||
|
|
||||||
// CodeChangeResponse contains data derived from an LLM response to a prompt generated via a CodeChangeRequest.
|
// CodeChangeResponse contains data derived from an LLM response to a prompt generated via a CodeChangeRequest.
|
||||||
|
@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -17,40 +17,96 @@ import (
|
|||||||
// IssueNotFound is returned when no issue can be found to generate a prompt for.
|
// IssueNotFound is returned when no issue can be found to generate a prompt for.
|
||||||
var IssueNotFound = errors.New("no issue found")
|
var IssueNotFound = errors.New("no issue found")
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
WaitDuration time.Duration
|
||||||
|
LocalRepoPath string
|
||||||
|
Repos []string
|
||||||
|
Self vc.Author
|
||||||
|
ListIssueOptions vc.ListIssueOptions
|
||||||
|
Model string
|
||||||
|
OpenAIToken string
|
||||||
|
}
|
||||||
|
|
||||||
// PullPal is the service responsible for:
|
// PullPal is the service responsible for:
|
||||||
// - Interacting with git server (e.g. reading issues and making PRs on Github)
|
// - Interacting with git server (e.g. reading issues and making PRs on Github)
|
||||||
// - Generating LLM prompts
|
// - Generating LLM prompts
|
||||||
// - Parsing LLM responses
|
// - Parsing LLM responses
|
||||||
// - Interacting with LLM (e.g. with GPT via OpenAI API)
|
// - Interacting with LLM (e.g. with GPT via OpenAI API)
|
||||||
type PullPal struct {
|
type PullPal struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
log *zap.Logger
|
log *zap.Logger
|
||||||
listIssueOptions vc.ListIssueOptions
|
cfg Config
|
||||||
|
|
||||||
ghClient *vc.GithubClient
|
repos []pullPalRepo
|
||||||
localGitClient *vc.LocalGitClient
|
openAIClient *llm.OpenAIClient
|
||||||
openAIClient *llm.OpenAIClient
|
}
|
||||||
|
|
||||||
|
type pullPalRepo struct {
|
||||||
|
ctx context.Context
|
||||||
|
log *zap.Logger
|
||||||
|
|
||||||
|
listIssueOptions vc.ListIssueOptions
|
||||||
|
ghClient *vc.GithubClient
|
||||||
|
localGitClient *vc.LocalGitClient
|
||||||
|
openAIClient *llm.OpenAIClient
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPullPal creates a new "pull pal service", including setting up local version control and LLM integrations.
|
// NewPullPal creates a new "pull pal service", including setting up local version control and LLM integrations.
|
||||||
func NewPullPal(ctx context.Context, log *zap.Logger, listIssueOptions vc.ListIssueOptions, self vc.Author, repo vc.Repository, model string, openAIToken string) (*PullPal, error) {
|
func NewPullPal(ctx context.Context, log *zap.Logger, cfg Config) (*PullPal, error) {
|
||||||
ghClient, err := vc.NewGithubClient(ctx, log, self, repo)
|
openAIClient := llm.NewOpenAIClient(log.Named("openaiClient"), cfg.Model, cfg.OpenAIToken)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
ppRepos := []pullPalRepo{}
|
||||||
|
fmt.Println("asdfasfdasfasdfasdf")
|
||||||
|
for _, r := range cfg.Repos {
|
||||||
|
fmt.Println(r)
|
||||||
|
parts := strings.Split(r, "/")
|
||||||
|
if len(parts) < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
host := parts[0]
|
||||||
|
owner := parts[1]
|
||||||
|
name := parts[2]
|
||||||
|
fmt.Println(host)
|
||||||
|
fmt.Println(owner)
|
||||||
|
fmt.Println(name)
|
||||||
|
newRepo := vc.Repository{
|
||||||
|
LocalPath: filepath.Join(cfg.LocalRepoPath, owner, name),
|
||||||
|
HostDomain: host,
|
||||||
|
Name: name,
|
||||||
|
Owner: vc.Author{
|
||||||
|
Handle: owner,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ghClient, err := vc.NewGithubClient(ctx, log.Named("ghclient-"+r), cfg.Self, newRepo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
localGitClient, err := vc.NewLocalGitClient(log.Named("gitclient-"+r), cfg.Self, newRepo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ppRepos = append(ppRepos, pullPalRepo{
|
||||||
|
ctx: ctx,
|
||||||
|
log: log,
|
||||||
|
|
||||||
|
ghClient: ghClient,
|
||||||
|
localGitClient: localGitClient,
|
||||||
|
openAIClient: openAIClient,
|
||||||
|
|
||||||
|
listIssueOptions: cfg.ListIssueOptions,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
localGitClient, err := vc.NewLocalGitClient(log, self, repo)
|
if len(ppRepos) == 0 {
|
||||||
if err != nil {
|
return nil, errors.New("no repos set up")
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &PullPal{
|
return &PullPal{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
log: log,
|
log: log,
|
||||||
listIssueOptions: listIssueOptions,
|
|
||||||
|
|
||||||
ghClient: ghClient,
|
repos: ppRepos,
|
||||||
localGitClient: localGitClient,
|
openAIClient: openAIClient,
|
||||||
openAIClient: llm.NewOpenAIClient(log.Named("openaiClient"), model, openAIToken),
|
cfg: cfg,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,120 +115,102 @@ func (p *PullPal) Run() error {
|
|||||||
p.log.Info("Starting Pull Pal")
|
p.log.Info("Starting Pull Pal")
|
||||||
// TODO gracefully handle context cancelation
|
// TODO gracefully handle context cancelation
|
||||||
for {
|
for {
|
||||||
p.log.Info("checking github issues...")
|
for _, r := range p.repos {
|
||||||
issues, err := p.ghClient.ListOpenIssues(p.listIssueOptions)
|
err := r.checkIssuesAndComments()
|
||||||
if err != nil {
|
|
||||||
p.log.Error("error listing issues", zap.Error(err))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(issues) == 0 {
|
|
||||||
p.log.Info("no issues found")
|
|
||||||
} else {
|
|
||||||
p.log.Info("picked issue to process")
|
|
||||||
|
|
||||||
issue := issues[0]
|
|
||||||
err = p.handleIssue(issue)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.log.Error("error handling issue", zap.Error(err))
|
p.log.Error("issue checking repo for issues and comments", zap.Error(err))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p.log.Info("checking pr comments...")
|
|
||||||
comments, err := p.ghClient.ListOpenComments(vc.ListCommentOptions{
|
|
||||||
Handles: p.listIssueOptions.Handles,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
p.log.Error("error listing comments", zap.Error(err))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(comments) == 0 {
|
|
||||||
p.log.Info("no comments found")
|
|
||||||
} else {
|
|
||||||
p.log.Info("picked comment to process")
|
|
||||||
|
|
||||||
comment := comments[0]
|
|
||||||
err = p.handleComment(comment)
|
|
||||||
if err != nil {
|
|
||||||
p.log.Error("error handling comment", zap.Error(err))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO remove sleep
|
// TODO remove sleep
|
||||||
p.log.Info("sleeping 30s")
|
p.log.Info("sleeping", zap.Duration("wait duration", p.cfg.WaitDuration))
|
||||||
time.Sleep(30 * time.Second)
|
time.Sleep(p.cfg.WaitDuration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PullPal) handleIssue(issue vc.Issue) error {
|
// checkIssuesAndComments will attempt to find and solve one issue and one comment, and then return.
|
||||||
issueNumber, err := strconv.Atoi(issue.ID)
|
func (p pullPalRepo) checkIssuesAndComments() error {
|
||||||
|
p.log.Info("checking github issues...")
|
||||||
|
issues, err := p.ghClient.ListOpenIssues(p.listIssueOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.log.Error("error converting issue ID to int", zap.Error(err))
|
p.log.Error("error listing issues", zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = p.ghClient.CommentOnIssue(issueNumber, "on it")
|
if len(issues) == 0 {
|
||||||
|
p.log.Info("no issues found")
|
||||||
|
} else {
|
||||||
|
p.log.Info("picked issue to process")
|
||||||
|
|
||||||
|
issue := issues[0]
|
||||||
|
err = p.handleIssue(issue)
|
||||||
|
if err != nil {
|
||||||
|
// TODO leave comment if error (make configurable)
|
||||||
|
p.log.Error("error handling issue", zap.Error(err))
|
||||||
|
commentText := fmt.Sprintf("I ran into a problem working on this:\n```\n%w\n```", err)
|
||||||
|
err = p.ghClient.CommentOnIssue(issue.Number, commentText)
|
||||||
|
if err != nil {
|
||||||
|
p.log.Error("error commenting on issue with error", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.log.Info("checking pr comments...")
|
||||||
|
comments, err := p.ghClient.ListOpenComments(vc.ListCommentOptions{
|
||||||
|
Handles: p.listIssueOptions.Handles,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
p.log.Error("error listing comments", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(comments) == 0 {
|
||||||
|
p.log.Info("no comments found")
|
||||||
|
} else {
|
||||||
|
p.log.Info("picked comment to process")
|
||||||
|
|
||||||
|
comment := comments[0]
|
||||||
|
err = p.handleComment(comment)
|
||||||
|
if err != nil {
|
||||||
|
// TODO leave comment if error (make configurable)
|
||||||
|
p.log.Error("error handling comment", zap.Error(err))
|
||||||
|
commentText := fmt.Sprintf("I ran into a problem working on this:\n```\n%w\n```", err)
|
||||||
|
err = p.ghClient.RespondToComment(comment.PRNumber, comment.ID, commentText)
|
||||||
|
if err != nil {
|
||||||
|
p.log.Error("error commenting on thread with error", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pullPalRepo) handleIssue(issue vc.Issue) error {
|
||||||
|
err := p.ghClient.CommentOnIssue(issue.Number, "working on it")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.log.Error("error commenting on issue", zap.Error(err))
|
p.log.Error("error commenting on issue", zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, label := range p.listIssueOptions.Labels {
|
for _, label := range p.listIssueOptions.Labels {
|
||||||
err = p.ghClient.RemoveLabelFromIssue(issueNumber, label)
|
err = p.ghClient.RemoveLabelFromIssue(issue.Number, label)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.log.Error("error removing labels from issue", zap.Error(err))
|
p.log.Error("error removing labels from issue", zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove file list from issue body
|
changeRequest, err := p.localGitClient.ParseIssueAndStartCommit(issue)
|
||||||
// TODO do this better and probably somewhere else
|
if err != nil {
|
||||||
parts := strings.Split(issue.Body, "Files:")
|
return err
|
||||||
issue.Body = parts[0]
|
|
||||||
|
|
||||||
fileList := []string{}
|
|
||||||
if len(parts) > 1 {
|
|
||||||
fileList = strings.Split(parts[1], ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
// get file contents from local git repository
|
|
||||||
files := []llm.File{}
|
|
||||||
for _, path := range fileList {
|
|
||||||
path = strings.TrimSpace(path)
|
|
||||||
nextFile, err := p.localGitClient.GetLocalFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
files = append(files, nextFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
changeRequest := llm.CodeChangeRequest{
|
|
||||||
Subject: issue.Subject,
|
|
||||||
Body: issue.Body,
|
|
||||||
IssueID: issue.ID,
|
|
||||||
Files: files,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
changeResponse, err := p.openAIClient.EvaluateCCR(p.ctx, "", changeRequest)
|
changeResponse, err := p.openAIClient.EvaluateCCR(p.ctx, "", changeRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// create commit with file changes
|
newBranchName := fmt.Sprintf("fix-%d", issue.Number)
|
||||||
err = p.localGitClient.StartCommit()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// todo remove hardcoded main
|
|
||||||
p.log.Info("checking out main branch")
|
|
||||||
err = p.localGitClient.CheckoutRemoteBranch("main")
|
|
||||||
if err != nil {
|
|
||||||
p.log.Info("error checking out main branch", zap.Error(err))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
newBranchName := fmt.Sprintf("fix-%s", changeRequest.IssueID)
|
|
||||||
for _, f := range changeResponse.Files {
|
for _, f := range changeResponse.Files {
|
||||||
p.log.Info("replacing or adding file", zap.String("path", f.Path), zap.String("contents", f.Contents))
|
p.log.Info("replacing or adding file", zap.String("path", f.Path), zap.String("contents", f.Contents))
|
||||||
err = p.localGitClient.ReplaceOrAddLocalFile(f)
|
err = p.localGitClient.ReplaceOrAddLocalFile(f)
|
||||||
@ -181,7 +219,7 @@ func (p *PullPal) handleIssue(issue vc.Issue) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commitMessage := changeRequest.Subject + "\n\n" + changeResponse.Notes + "\n\nResolves: #" + changeRequest.IssueID
|
commitMessage := fmt.Sprintf("%s\n\n%s\n\nResolves #%d", changeRequest.Subject, changeResponse.Notes, changeRequest.IssueNumber)
|
||||||
p.log.Info("about to create commit", zap.String("message", commitMessage))
|
p.log.Info("about to create commit", zap.String("message", commitMessage))
|
||||||
err = p.localGitClient.FinishCommit(commitMessage)
|
err = p.localGitClient.FinishCommit(commitMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -197,7 +235,7 @@ func (p *PullPal) handleIssue(issue vc.Issue) error {
|
|||||||
|
|
||||||
// open code change request
|
// open code change request
|
||||||
// TODO don't hardcode main branch, make configurable
|
// TODO don't hardcode main branch, make configurable
|
||||||
_, url, err := p.ghClient.OpenCodeChangeRequest(changeRequest, changeResponse, newBranchName, "main")
|
_, url, err := p.ghClient.OpenCodeChangeRequest(changeRequest, changeResponse, newBranchName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -206,7 +244,7 @@ func (p *PullPal) handleIssue(issue vc.Issue) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PullPal) handleComment(comment vc.Comment) error {
|
func (p *pullPalRepo) handleComment(comment vc.Comment) error {
|
||||||
if comment.Branch == "" {
|
if comment.Branch == "" {
|
||||||
return errors.New("no branch provided in comment")
|
return errors.New("no branch provided in comment")
|
||||||
}
|
}
|
||||||
|
@ -9,37 +9,38 @@ import (
|
|||||||
|
|
||||||
func (p *PullPal) DebugGit() error {
|
func (p *PullPal) DebugGit() error {
|
||||||
p.log.Info("Starting Pull Pal git debug")
|
p.log.Info("Starting Pull Pal git debug")
|
||||||
|
r := p.repos[0]
|
||||||
|
|
||||||
// create commit with file changes
|
// create commit with file changes
|
||||||
err := p.localGitClient.StartCommit()
|
err := r.localGitClient.StartCommit()
|
||||||
//err = p.ghClient.StartCommit()
|
//err = p.ghClient.StartCommit()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.log.Error("error starting commit", zap.Error(err))
|
r.log.Error("error starting commit", zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
newBranchName := "debug-branch"
|
newBranchName := "debug-branch"
|
||||||
|
|
||||||
for _, f := range []string{"a", "b"} {
|
for _, f := range []string{"a", "b"} {
|
||||||
err = p.localGitClient.ReplaceOrAddLocalFile(llm.File{
|
err = r.localGitClient.ReplaceOrAddLocalFile(llm.File{
|
||||||
Path: f,
|
Path: f,
|
||||||
Contents: "hello",
|
Contents: "hello",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.log.Error("error replacing or adding file", zap.Error(err))
|
r.log.Error("error replacing or adding file", zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commitMessage := "debug commit message"
|
commitMessage := "debug commit message"
|
||||||
err = p.localGitClient.FinishCommit(commitMessage)
|
err = r.localGitClient.FinishCommit(commitMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.log.Error("error finishing commit", zap.Error(err))
|
r.log.Error("error finishing commit", zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = p.localGitClient.PushBranch(newBranchName)
|
err = r.localGitClient.PushBranch(newBranchName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.log.Error("error pushing branch", zap.Error(err))
|
r.log.Error("error pushing branch", zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,25 +50,26 @@ func (p *PullPal) DebugGit() error {
|
|||||||
// todo dont require args for listing comments
|
// todo dont require args for listing comments
|
||||||
func (p *PullPal) DebugGithub(handles []string) error {
|
func (p *PullPal) DebugGithub(handles []string) error {
|
||||||
p.log.Info("Starting Pull Pal Github debug")
|
p.log.Info("Starting Pull Pal Github debug")
|
||||||
|
r := p.repos[0]
|
||||||
|
|
||||||
issues, err := p.ghClient.ListOpenIssues(p.listIssueOptions)
|
issues, err := r.ghClient.ListOpenIssues(r.listIssueOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.log.Error("error listing issues", zap.Error(err))
|
r.log.Error("error listing issues", zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, i := range issues {
|
for _, i := range issues {
|
||||||
p.log.Info("got issue", zap.String("issue", i.String()))
|
r.log.Info("got issue", zap.String("issue", i.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
comments, err := p.ghClient.ListOpenComments(vc.ListCommentOptions{
|
comments, err := r.ghClient.ListOpenComments(vc.ListCommentOptions{
|
||||||
Handles: handles,
|
Handles: handles,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.log.Error("error listing comments", zap.Error(err))
|
r.log.Error("error listing comments", zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, c := range comments {
|
for _, c := range comments {
|
||||||
p.log.Info("got comment", zap.String("comment", c.String()))
|
r.log.Info("got comment", zap.String("comment", c.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -82,10 +84,10 @@ func (p *PullPal) DebugLLM() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
codeChangeRequest := llm.CodeChangeRequest{
|
codeChangeRequest := llm.CodeChangeRequest{
|
||||||
Files: []llm.File{file},
|
Files: []llm.File{file},
|
||||||
Subject: "update port and add endpoint",
|
Subject: "update port and add endpoint",
|
||||||
Body: "use port 8080 for the server in main.go. Also add an endpoint at GET /api/numbers that returns a random integer between 2 and 10",
|
Body: "use port 8080 for the server in main.go. Also add an endpoint at GET /api/numbers that returns a random integer between 2 and 10",
|
||||||
IssueID: "1234",
|
IssueNumber: 1234,
|
||||||
}
|
}
|
||||||
|
|
||||||
p.log.Info("CODE CHANGE REQUEST", zap.String("request", codeChangeRequest.String()))
|
p.log.Info("CODE CHANGE REQUEST", zap.String("request", codeChangeRequest.String()))
|
||||||
|
48
vc/common.go
48
vc/common.go
@ -2,13 +2,14 @@ package vc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Issue represents an issue on a version control server.
|
// Issue represents an issue on a version control server.
|
||||||
type Issue struct {
|
type Issue struct {
|
||||||
ID string
|
Number int
|
||||||
Subject string
|
Subject string
|
||||||
Body string
|
Body string
|
||||||
URL string
|
URL string
|
||||||
@ -16,7 +17,7 @@ type Issue struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i Issue) String() string {
|
func (i Issue) String() string {
|
||||||
return fmt.Sprintf("Issue ID: %s\nAuthor: %s\nSubject: %s\nBody:\n%s\nURL: %s\n", i.ID, i.Author.Handle, i.Subject, i.Body, i.URL)
|
return fmt.Sprintf("Issue #: %d\nAuthor: %s\nSubject: %s\nBody:\n%s\nURL: %s\n", i.Number, i.Author.Handle, i.Subject, i.Body, i.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListIssueOptions defines options for listing issues.
|
// ListIssueOptions defines options for listing issues.
|
||||||
@ -81,3 +82,46 @@ func (repo Repository) SSH() string {
|
|||||||
func (repo Repository) HTTPS() string {
|
func (repo Repository) HTTPS() string {
|
||||||
return fmt.Sprintf("https://%s/%s/%s.git", repo.HostDomain, repo.Owner.Handle, repo.Name)
|
return fmt.Sprintf("https://%s/%s/%s.git", repo.HostDomain, repo.Owner.Handle, repo.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IssueBody struct {
|
||||||
|
PromptBody string
|
||||||
|
FilePaths []string
|
||||||
|
BaseBranch string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseIssueBody(body string) IssueBody {
|
||||||
|
issueBody := IssueBody{
|
||||||
|
BaseBranch: "main",
|
||||||
|
}
|
||||||
|
divider := "---"
|
||||||
|
|
||||||
|
parts := strings.Split(body, divider)
|
||||||
|
issueBody.PromptBody = strings.TrimSpace(parts[0])
|
||||||
|
// if there was nothing to split, no additional configuration was provided
|
||||||
|
if len(parts) <= 1 {
|
||||||
|
return issueBody
|
||||||
|
}
|
||||||
|
|
||||||
|
configStr := parts[1]
|
||||||
|
configLines := strings.Split(configStr, "\n")
|
||||||
|
for _, line := range configLines {
|
||||||
|
lineParts := strings.Split(line, ":")
|
||||||
|
if len(lineParts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.ToLower(strings.TrimSpace(lineParts[0]))
|
||||||
|
if key == "base" {
|
||||||
|
issueBody.BaseBranch = strings.TrimSpace(lineParts[1])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if key == "files" {
|
||||||
|
filePaths := strings.Split(lineParts[1], ",")
|
||||||
|
for _, p := range filePaths {
|
||||||
|
issueBody.FilePaths = append(issueBody.FilePaths, strings.TrimSpace(p))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return issueBody
|
||||||
|
}
|
||||||
|
99
vc/common_test.go
Normal file
99
vc/common_test.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package vc_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mobyvb/pull-pal/vc"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseIssueBody(t *testing.T) {
|
||||||
|
var testCases = []struct {
|
||||||
|
testcase string
|
||||||
|
body string
|
||||||
|
parsed vc.IssueBody
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"simple issue",
|
||||||
|
`
|
||||||
|
add an html file
|
||||||
|
`,
|
||||||
|
vc.IssueBody{
|
||||||
|
PromptBody: "add an html file",
|
||||||
|
BaseBranch: "main",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"issue with explicit file list",
|
||||||
|
`
|
||||||
|
add an html file
|
||||||
|
and also a go file
|
||||||
|
read a readme file too
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
FiLeS: index.html, README.md ,main.go
|
||||||
|
`,
|
||||||
|
vc.IssueBody{
|
||||||
|
PromptBody: "add an html file\nand also a go file\nread a readme file too",
|
||||||
|
BaseBranch: "main",
|
||||||
|
FilePaths: []string{"index.html", "README.md", "main.go"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"issue with a custom base branch",
|
||||||
|
`
|
||||||
|
add an html file
|
||||||
|
---
|
||||||
|
base: some-base-branch
|
||||||
|
`,
|
||||||
|
vc.IssueBody{
|
||||||
|
PromptBody: "add an html file",
|
||||||
|
BaseBranch: "some-base-branch",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"issue with an explicit base branch and file list",
|
||||||
|
`
|
||||||
|
add an html file
|
||||||
|
---
|
||||||
|
base: some-base-branch
|
||||||
|
files: index.html, main.go
|
||||||
|
`,
|
||||||
|
vc.IssueBody{
|
||||||
|
PromptBody: "add an html file",
|
||||||
|
BaseBranch: "some-base-branch",
|
||||||
|
FilePaths: []string{"index.html", "main.go"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"issue with garbage in config section",
|
||||||
|
`
|
||||||
|
add an html file
|
||||||
|
---
|
||||||
|
asdf:
|
||||||
|
files: index.html, main.go
|
||||||
|
: asdfsadf
|
||||||
|
base: some-base-branch
|
||||||
|
asdfjljldsfj
|
||||||
|
nonexistentoption: asdf
|
||||||
|
`,
|
||||||
|
vc.IssueBody{
|
||||||
|
PromptBody: "add an html file",
|
||||||
|
BaseBranch: "some-base-branch",
|
||||||
|
FilePaths: []string{"index.html", "main.go"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range testCases {
|
||||||
|
t.Log("testing case:", tt.testcase)
|
||||||
|
parsed := vc.ParseIssueBody(tt.body)
|
||||||
|
require.Equal(t, tt.parsed.PromptBody, parsed.PromptBody)
|
||||||
|
require.Equal(t, tt.parsed.BaseBranch, parsed.BaseBranch)
|
||||||
|
require.Equal(t, len(tt.parsed.FilePaths), len(parsed.FilePaths))
|
||||||
|
for i, p := range tt.parsed.FilePaths {
|
||||||
|
require.Equal(t, p, parsed.FilePaths[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
vc/git.go
41
vc/git.go
@ -219,3 +219,44 @@ func (gc *LocalGitClient) FinishCommit(message string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseIssueAndStartCommit parses the information provided in the issue to check out the appropriate branch,
|
||||||
|
// get the contents of the files mentioned in the issue, and initialize the worktree.
|
||||||
|
func (gc *LocalGitClient) ParseIssueAndStartCommit(issue Issue) (llm.CodeChangeRequest, error) {
|
||||||
|
var changeRequest llm.CodeChangeRequest
|
||||||
|
|
||||||
|
if gc.worktree != nil {
|
||||||
|
return changeRequest, errors.New("worktree is active - some other work is incomplete")
|
||||||
|
}
|
||||||
|
|
||||||
|
issueBody := ParseIssueBody(issue.Body)
|
||||||
|
|
||||||
|
// start a worktree
|
||||||
|
err := gc.StartCommit()
|
||||||
|
if err != nil {
|
||||||
|
return changeRequest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = gc.CheckoutRemoteBranch(issueBody.BaseBranch)
|
||||||
|
if err != nil {
|
||||||
|
return changeRequest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get file contents from local git repository
|
||||||
|
files := []llm.File{}
|
||||||
|
for _, path := range issueBody.FilePaths {
|
||||||
|
nextFile, err := gc.GetLocalFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return changeRequest, err
|
||||||
|
}
|
||||||
|
files = append(files, nextFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return llm.CodeChangeRequest{
|
||||||
|
Subject: issue.Subject,
|
||||||
|
Body: issueBody.PromptBody,
|
||||||
|
IssueNumber: issue.Number,
|
||||||
|
Files: files,
|
||||||
|
BaseBranch: issueBody.BaseBranch,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
@ -48,7 +48,7 @@ func NewGithubClient(ctx context.Context, log *zap.Logger, self Author, repo Rep
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OpenCodeChangeRequest pushes to a new remote branch and opens a PR on Github.
|
// OpenCodeChangeRequest pushes to a new remote branch and opens a PR on Github.
|
||||||
func (gc *GithubClient) OpenCodeChangeRequest(req llm.CodeChangeRequest, res llm.CodeChangeResponse, fromBranch, toBranch string) (id, url string, err error) {
|
func (gc *GithubClient) OpenCodeChangeRequest(req llm.CodeChangeRequest, res llm.CodeChangeResponse, fromBranch string) (id, url string, err error) {
|
||||||
// TODO handle gc.ctx canceled
|
// TODO handle gc.ctx canceled
|
||||||
|
|
||||||
title := req.Subject
|
title := req.Subject
|
||||||
@ -57,13 +57,13 @@ func (gc *GithubClient) OpenCodeChangeRequest(req llm.CodeChangeRequest, res llm
|
|||||||
}
|
}
|
||||||
|
|
||||||
body := res.Notes
|
body := res.Notes
|
||||||
body += fmt.Sprintf("\n\nResolves #%s", req.IssueID)
|
body += fmt.Sprintf("\n\nResolves #%d", req.IssueNumber)
|
||||||
|
|
||||||
// Finally, open a pull request from the new branch.
|
// Finally, open a pull request from the new branch.
|
||||||
pr, _, err := gc.client.PullRequests.Create(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, &github.NewPullRequest{
|
pr, _, err := gc.client.PullRequests.Create(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, &github.NewPullRequest{
|
||||||
Title: &title,
|
Title: &title,
|
||||||
Head: &fromBranch,
|
Head: &fromBranch,
|
||||||
Base: &toBranch,
|
Base: &req.BaseBranch,
|
||||||
Body: &body,
|
Body: &body,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -102,7 +102,7 @@ func (gc *GithubClient) ListOpenIssues(options ListIssueOptions) ([]Issue, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
nextIssue := Issue{
|
nextIssue := Issue{
|
||||||
ID: strconv.Itoa(issue.GetNumber()),
|
Number: issue.GetNumber(),
|
||||||
Subject: issue.GetTitle(),
|
Subject: issue.GetTitle(),
|
||||||
Body: issue.GetBody(),
|
Body: issue.GetBody(),
|
||||||
URL: issue.GetHTMLURL(),
|
URL: issue.GetHTMLURL(),
|
||||||
|
Loading…
Reference in New Issue
Block a user