mirror of
https://github.com/Pull-Pal/pull-pal.git
synced 2024-12-21 17:46:24 -05:00
cleanup/refactor (#1)
focus on fully automated as basic/default functionality. remove unnecessary commands except for local git debug. remove unnecessary interfaces and code. remove local git functionality from github client. probably some other stuff too
This commit is contained in:
parent
3a312b2b9b
commit
cbec4c1be9
@ -6,9 +6,9 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var runCmd = &cobra.Command{
|
||||
Use: "run",
|
||||
Short: "Runs a fully automated pull pal service",
|
||||
var debugGitCmd = &cobra.Command{
|
||||
Use: "debug-git",
|
||||
Short: "debug git functionality",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cfg := getConfig()
|
||||
|
||||
@ -19,14 +19,14 @@ var runCmd = &cobra.Command{
|
||||
}
|
||||
fmt.Println("Successfully initialized pull pal")
|
||||
|
||||
err = p.Run()
|
||||
err = p.DebugGit()
|
||||
if err != nil {
|
||||
fmt.Println("error running", err)
|
||||
fmt.Println("err debugging git", err)
|
||||
return
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(runCmd)
|
||||
rootCmd.AddCommand(debugGitCmd)
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var listCommentsCmd = &cobra.Command{
|
||||
Use: "list-comments",
|
||||
Short: "Lists comments on a Github PR meeting the configured criteria",
|
||||
Long: "Lists comments on a Github PR meeting the configured criteria",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cfg := getConfig()
|
||||
|
||||
p, err := getPullPal(cmd.Context(), cfg)
|
||||
if err != nil {
|
||||
fmt.Println("error creating new pull pal", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("Successfully initialized pull pal")
|
||||
|
||||
prID := args[0]
|
||||
issueList, err := p.ListComments(prID, cfg.usersToListenTo)
|
||||
if err != nil {
|
||||
fmt.Println("error listing issues", err)
|
||||
return
|
||||
}
|
||||
fmt.Println(issueList)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(listCommentsCmd)
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var listIssuesCmd = &cobra.Command{
|
||||
Use: "list-issues",
|
||||
Short: "Lists github issues meeting the configured criteria",
|
||||
Long: "Lists github issues meeting the configured criteria",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cfg := getConfig()
|
||||
|
||||
p, err := getPullPal(cmd.Context(), cfg)
|
||||
if err != nil {
|
||||
fmt.Println("error creating new pull pal", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("Successfully initialized pull pal")
|
||||
|
||||
issueList, err := p.ListIssues(cfg.usersToListenTo, cfg.requiredIssueLabels)
|
||||
if err != nil {
|
||||
fmt.Println("error listing issues", err)
|
||||
return
|
||||
}
|
||||
fmt.Println(issueList)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(listIssuesCmd)
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mobyvb/pull-pal/vc"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var localIssueCmd = &cobra.Command{
|
||||
Use: "local-issue",
|
||||
Short: "Processes a locally-defined/provided issue rather than remotely reading one from the Github repo",
|
||||
// TODO csv filepath as arg?
|
||||
// Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cfg := getConfig()
|
||||
|
||||
p, err := getPullPal(cmd.Context(), cfg)
|
||||
if err != nil {
|
||||
fmt.Println("error creating new pull pal", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("Successfully initialized pull pal")
|
||||
|
||||
newIssue := vc.Issue{
|
||||
Subject: "a few updates",
|
||||
Body: "Add a quote from Frodo to the README.md and index.html files.\nSwitch main.go to port 7777.\nFiles:index.html,README.md,main.go",
|
||||
Author: vc.Author{
|
||||
Handle: "mobyvb",
|
||||
},
|
||||
}
|
||||
err = p.MakeLocalChange(newIssue)
|
||||
if err != nil {
|
||||
fmt.Println("err making local change", err)
|
||||
return
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(localIssueCmd)
|
||||
}
|
87
cmd/root.go
87
cmd/root.go
@ -2,11 +2,9 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mobyvb/pull-pal/llm"
|
||||
"github.com/mobyvb/pull-pal/pullpal"
|
||||
"github.com/mobyvb/pull-pal/vc"
|
||||
"go.uber.org/zap"
|
||||
@ -30,8 +28,6 @@ type config struct {
|
||||
|
||||
// local paths
|
||||
localRepoPath string
|
||||
promptPath string
|
||||
responsePath string
|
||||
|
||||
// program settings
|
||||
promptToClipboard bool
|
||||
@ -51,8 +47,6 @@ func getConfig() config {
|
||||
repoName: viper.GetString("repo-name"),
|
||||
|
||||
localRepoPath: viper.GetString("local-repo-path"),
|
||||
promptPath: viper.GetString("prompt-path"),
|
||||
responsePath: viper.GetString("response-path"),
|
||||
|
||||
promptToClipboard: viper.GetBool("prompt-to-clipboard"),
|
||||
usersToListenTo: viper.GetStringSlice("users-to-listen-to"),
|
||||
@ -61,6 +55,7 @@ func getConfig() config {
|
||||
}
|
||||
|
||||
func getPullPal(ctx context.Context, cfg config) (*pullpal.PullPal, error) {
|
||||
// TODO figure out debug logging
|
||||
log, err := zap.NewProduction()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -91,16 +86,7 @@ func getPullPal(ctx context.Context, cfg config) (*pullpal.PullPal, error) {
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "pull-pal",
|
||||
Short: "A bot that uses large language models to act as a collaborator on a git project",
|
||||
Long: `A bot that uses large language models to act as a collaborator on a git project.
|
||||
|
||||
It can be used to:
|
||||
* Monitor a repository for open issues, and generate LLM prompts according to the issue details
|
||||
* Read an LLM response and process it into a new git commit and code change request on the version control server
|
||||
`,
|
||||
// Uncomment the following line if your bare application
|
||||
// has an action associated with it:
|
||||
Short: "run an automated digital assitant to monitor and make code changes to a github repository",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cfg := getConfig()
|
||||
|
||||
@ -111,64 +97,11 @@ It can be used to:
|
||||
}
|
||||
fmt.Println("Successfully initialized pull pal")
|
||||
|
||||
// TODO this loop breaks on the second iteration due to a weird git state or something
|
||||
for {
|
||||
var input string
|
||||
fmt.Println("Press 'enter' when ready to select issue. Type 'exit' to exit.")
|
||||
fmt.Scanln(&input)
|
||||
if input == "exit" {
|
||||
break
|
||||
}
|
||||
|
||||
var issue vc.Issue
|
||||
var changeRequest llm.CodeChangeRequest
|
||||
if cfg.promptToClipboard {
|
||||
issue, changeRequest, err = p.PickIssueToClipboard()
|
||||
if err != nil {
|
||||
if !errors.Is(err, pullpal.IssueNotFound) {
|
||||
fmt.Println("error selecting issue and/or generating prompt", err)
|
||||
return
|
||||
} else {
|
||||
fmt.Println("No issues found. Proceeding to parse prompt")
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Picked issue and copied prompt to clipboard. Issue #%s\n", issue.ID)
|
||||
}
|
||||
} else {
|
||||
issue, changeRequest, err = p.PickIssueToFile(cfg.promptPath)
|
||||
if err != nil {
|
||||
if !errors.Is(err, pullpal.IssueNotFound) {
|
||||
fmt.Println("error selecting issue and/or generating prompt", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("No issues found. Proceeding to parse prompt")
|
||||
} else {
|
||||
fmt.Printf("Picked issue and copied prompt to clipboard. Issue #%s. Prompt location %s\n", issue.ID, cfg.promptPath)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\nInsert LLM response into response file: %s", cfg.responsePath)
|
||||
|
||||
fmt.Println("Press 'enter' when ready to parse response. Enter 'skip' to skip response parsing. Enter 'exit' to exit.")
|
||||
fmt.Scanln(&input)
|
||||
if input == "exit" {
|
||||
break
|
||||
}
|
||||
if input == "skip" {
|
||||
fmt.Println()
|
||||
continue
|
||||
}
|
||||
|
||||
prURL, err := p.ProcessResponseFromFile(changeRequest, cfg.responsePath)
|
||||
if err != nil {
|
||||
fmt.Println("error parsing LLM response and/or making version control changes", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully opened a code change request. Link: %s\n", prURL)
|
||||
err = p.Run()
|
||||
if err != nil {
|
||||
fmt.Println("error running", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Done. Thank you!")
|
||||
},
|
||||
}
|
||||
|
||||
@ -198,11 +131,8 @@ func init() {
|
||||
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/pullpalrepo/", "path where pull pal will check out a local copy of the repository")
|
||||
rootCmd.PersistentFlags().StringP("prompt-path", "p", "./path/to/prompt.txt", "path where pull pal will write the llm prompt")
|
||||
rootCmd.PersistentFlags().StringP("response-path", "r", "./path/to/response.txt", "path where pull pal will read the llm response from")
|
||||
rootCmd.PersistentFlags().StringP("local-repo-path", "l", "/tmp/pullpallrepo", "local path to check out ephemeral repository in")
|
||||
|
||||
rootCmd.PersistentFlags().BoolP("prompt-to-clipboard", "c", false, "whether to copy LLM prompt to clipboard rather than using a file")
|
||||
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")
|
||||
|
||||
@ -216,10 +146,7 @@ func init() {
|
||||
viper.BindPFlag("repo-name", rootCmd.PersistentFlags().Lookup("repo-name"))
|
||||
|
||||
viper.BindPFlag("local-repo-path", rootCmd.PersistentFlags().Lookup("local-repo-path"))
|
||||
viper.BindPFlag("prompt-path", rootCmd.PersistentFlags().Lookup("prompt-path"))
|
||||
viper.BindPFlag("response-path", rootCmd.PersistentFlags().Lookup("response-path"))
|
||||
|
||||
viper.BindPFlag("prompt-to-clipboard", rootCmd.PersistentFlags().Lookup("prompt-to-clipboard"))
|
||||
viper.BindPFlag("users-to-listen-to", rootCmd.PersistentFlags().Lookup("users-to-listen-to"))
|
||||
viper.BindPFlag("required-issue-labels", rootCmd.PersistentFlags().Lookup("required-issue-labels"))
|
||||
}
|
||||
|
1
go.mod
1
go.mod
@ -3,7 +3,6 @@ module github.com/mobyvb/pull-pal
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
github.com/go-git/go-git/v5 v5.6.1
|
||||
github.com/google/go-github v17.0.0+incompatible
|
||||
github.com/sashabaranov/go-openai v1.9.0
|
||||
|
2
go.sum
2
go.sum
@ -48,8 +48,6 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
|
@ -37,7 +37,7 @@ func (req CodeChangeRequest) MustGetPrompt() string {
|
||||
|
||||
// GetPrompt converts the information in the request to a prompt for an LLM.
|
||||
func (req CodeChangeRequest) GetPrompt() (string, error) {
|
||||
tmpl, err := template.ParseFiles("./llm/code-change-request.tmpl")
|
||||
tmpl, err := template.ParseFiles("./llm/prompts/code-change-request.tmpl")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -72,10 +72,16 @@ func (res CodeChangeResponse) String() string {
|
||||
|
||||
// ParseCodeChangeResponse parses the LLM's response to CodeChangeRequest (string) into a CodeChangeResponse.
|
||||
func ParseCodeChangeResponse(llmResponse string) CodeChangeResponse {
|
||||
sections := strings.Split(llmResponse, "Notes:")
|
||||
sections := strings.Split(llmResponse, "ppnotes:")
|
||||
|
||||
filesSection := sections[0]
|
||||
notes := strings.TrimSpace(sections[1])
|
||||
filesSection := ""
|
||||
if len(sections) > 0 {
|
||||
filesSection = sections[0]
|
||||
}
|
||||
notes := ""
|
||||
if len(sections) > 1 {
|
||||
notes = strings.TrimSpace(sections[1])
|
||||
}
|
||||
|
||||
files := parseFiles(filesSection)
|
||||
|
||||
@ -87,7 +93,10 @@ func ParseCodeChangeResponse(llmResponse string) CodeChangeResponse {
|
||||
|
||||
// parseFiles process the "files" subsection of the LLM's response. It is a helper for GetCodeChangeResponse.
|
||||
func parseFiles(filesSection string) []File {
|
||||
fileStringList := strings.Split(filesSection, "name:")
|
||||
fileStringList := strings.Split(filesSection, "ppname:")
|
||||
if len(fileStringList) < 2 {
|
||||
return []File{}
|
||||
}
|
||||
// first item in the list is just gonna be "Files:"
|
||||
fileStringList = fileStringList[1:]
|
||||
|
||||
@ -98,7 +107,10 @@ func parseFiles(filesSection string) []File {
|
||||
)
|
||||
fileList := make([]File, len(fileStringList))
|
||||
for i, f := range fileStringList {
|
||||
fileParts := strings.Split(f, "contents:")
|
||||
fileParts := strings.Split(f, "ppcontents:")
|
||||
if len(fileParts) < 2 {
|
||||
continue
|
||||
}
|
||||
path := replacer.Replace(fileParts[0])
|
||||
path = strings.TrimSpace(path)
|
||||
|
||||
|
@ -23,10 +23,11 @@ func (oc *OpenAIClient) EvaluateCCR(ctx context.Context, req CodeChangeRequest)
|
||||
resp, err := oc.client.CreateChatCompletion(
|
||||
ctx,
|
||||
openai.ChatCompletionRequest{
|
||||
Model: openai.GPT3Dot5Turbo,
|
||||
// TODO make model configurable
|
||||
Model: openai.GPT4,
|
||||
//Model: openai.GPT3Dot5Turbo,
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{
|
||||
// TODO is this the correct role for my prompts?
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: req.String(),
|
||||
},
|
||||
@ -38,10 +39,10 @@ func (oc *OpenAIClient) EvaluateCCR(ctx context.Context, req CodeChangeRequest)
|
||||
return res, err
|
||||
}
|
||||
|
||||
// TODO use different choices/different options in different branches/worktrees?
|
||||
choice := resp.Choices[0].Message.Content
|
||||
|
||||
oc.log.Debug("got response from llm", zap.String("output", choice))
|
||||
// TODO make debug log when I figure out how to config that
|
||||
oc.log.Info("got response from llm", zap.String("output", choice))
|
||||
|
||||
return ParseCodeChangeResponse(choice), nil
|
||||
}
|
||||
|
@ -15,10 +15,10 @@ Body:
|
||||
Respond in the exact format:
|
||||
Files:
|
||||
{{ range $index, $file := .Files }}
|
||||
- name: {{ $file.Path }}
|
||||
contents:
|
||||
ppname: {{ $file.Path }}
|
||||
ppcontents:
|
||||
[new {{ $file.Path }} contents]
|
||||
{{ end }}
|
||||
|
||||
Notes:
|
||||
ppnotes:
|
||||
[additional context about your changes]
|
@ -24,9 +24,9 @@ Q
|
||||
Response Template B:
|
||||
R
|
||||
Files:
|
||||
- name: {{ .Path }}
|
||||
contents:
|
||||
ppname: {{ .Path }}
|
||||
ppcontents:
|
||||
[new {{ .Path }} contents]
|
||||
|
||||
Response:
|
||||
ppresponse:
|
||||
[additional context about your changes]
|
@ -4,14 +4,13 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mobyvb/pull-pal/llm"
|
||||
"github.com/mobyvb/pull-pal/vc"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@ -19,16 +18,16 @@ import (
|
||||
var IssueNotFound = errors.New("no issue found")
|
||||
|
||||
// PullPal is the service responsible for:
|
||||
// * Interacting with git server (e.g. reading issues and making PRs on Github)
|
||||
// * Generating LLM prompts
|
||||
// * Parsing LLM responses
|
||||
// * Interacting with LLM (e.g. with GPT via OpenAI API)
|
||||
// - Interacting with git server (e.g. reading issues and making PRs on Github)
|
||||
// - Generating LLM prompts
|
||||
// - Parsing LLM responses
|
||||
// - Interacting with LLM (e.g. with GPT via OpenAI API)
|
||||
type PullPal struct {
|
||||
ctx context.Context
|
||||
log *zap.Logger
|
||||
listIssueOptions vc.ListIssueOptions
|
||||
|
||||
vcClient vc.VCClient
|
||||
ghClient *vc.GithubClient
|
||||
localGitClient *vc.LocalGitClient
|
||||
openAIClient *llm.OpenAIClient
|
||||
}
|
||||
@ -39,7 +38,7 @@ func NewPullPal(ctx context.Context, log *zap.Logger, listIssueOptions vc.ListIs
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
localGitClient, err := vc.NewLocalGitClient(self, repo)
|
||||
localGitClient, err := vc.NewLocalGitClient(log, self, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -49,7 +48,7 @@ func NewPullPal(ctx context.Context, log *zap.Logger, listIssueOptions vc.ListIs
|
||||
log: log,
|
||||
listIssueOptions: listIssueOptions,
|
||||
|
||||
vcClient: ghClient,
|
||||
ghClient: ghClient,
|
||||
localGitClient: localGitClient,
|
||||
openAIClient: llm.NewOpenAIClient(log.Named("openaiClient"), openAIToken),
|
||||
}, nil
|
||||
@ -60,20 +59,38 @@ func (p *PullPal) Run() error {
|
||||
p.log.Info("Starting Pull Pal")
|
||||
// TODO gracefully handle context cancelation
|
||||
for {
|
||||
issues, err := p.vcClient.ListOpenIssues(p.listIssueOptions)
|
||||
issues, err := p.ghClient.ListOpenIssues(p.listIssueOptions)
|
||||
if err != nil {
|
||||
p.log.Error("error listing issues", zap.Error(err))
|
||||
continue
|
||||
return err
|
||||
}
|
||||
|
||||
if len(issues) == 0 {
|
||||
// todo don't sleep
|
||||
p.log.Info("no issues found. sleeping for 5 mins")
|
||||
time.Sleep(5 * time.Minute)
|
||||
p.log.Info("no issues found. sleeping for 30 seconds")
|
||||
time.Sleep(30 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
issue := issues[0]
|
||||
issueNumber, err := strconv.Atoi(issue.ID)
|
||||
if err != nil {
|
||||
p.log.Error("error converting issue ID to int", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
err = p.ghClient.CommentOnIssue(issueNumber, "on it")
|
||||
if err != nil {
|
||||
p.log.Error("error commenting on issue", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
for _, label := range p.listIssueOptions.Labels {
|
||||
err = p.ghClient.RemoveLabelFromIssue(issueNumber, label)
|
||||
if err != nil {
|
||||
p.log.Error("error removing labels from issue", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// remove file list from issue body
|
||||
// TODO do this better and probably somewhere else
|
||||
@ -89,7 +106,7 @@ func (p *PullPal) Run() error {
|
||||
files := []llm.File{}
|
||||
for _, path := range fileList {
|
||||
path = strings.TrimSpace(path)
|
||||
nextFile, err := p.vcClient.GetLocalFile(path)
|
||||
nextFile, err := p.localGitClient.GetLocalFile(path)
|
||||
if err != nil {
|
||||
p.log.Error("error getting file from vcclient", zap.Error(err))
|
||||
continue
|
||||
@ -115,40 +132,63 @@ func (p *PullPal) Run() error {
|
||||
//codeChangeResponse := llm.ParseCodeChangeResponse(llmResponse)
|
||||
|
||||
// create commit with file changes
|
||||
err = p.vcClient.StartCommit()
|
||||
err = p.localGitClient.StartCommit()
|
||||
//err = p.ghClient.StartCommit()
|
||||
if err != nil {
|
||||
p.log.Error("error starting commit", zap.Error(err))
|
||||
continue
|
||||
return err
|
||||
}
|
||||
newBranchName := fmt.Sprintf("fix-%s", changeRequest.IssueID)
|
||||
/*
|
||||
err = p.localGitClient.SwitchBranch(newBranchName)
|
||||
if err != nil {
|
||||
p.log.Error("error switching branch", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
*/
|
||||
for _, f := range changeResponse.Files {
|
||||
err = p.vcClient.ReplaceOrAddLocalFile(f)
|
||||
p.log.Info("replacing or adding file", zap.String("path", f.Path), zap.String("contents", f.Contents))
|
||||
err = p.localGitClient.ReplaceOrAddLocalFile(f)
|
||||
// err = p.ghClient.ReplaceOrAddLocalFile(f)
|
||||
if err != nil {
|
||||
p.log.Error("error replacing or adding file", zap.Error(err))
|
||||
continue
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
commitMessage := changeRequest.Subject + "\n\n" + changeResponse.Notes + "\n\nResolves: #" + changeRequest.IssueID
|
||||
err = p.vcClient.FinishCommit(commitMessage)
|
||||
p.log.Info("about to create commit", zap.String("message", commitMessage))
|
||||
err = p.localGitClient.FinishCommit(commitMessage)
|
||||
if err != nil {
|
||||
p.log.Error("error finshing commit", zap.Error(err))
|
||||
continue
|
||||
p.log.Error("error finishing commit", zap.Error(err))
|
||||
// TODO figure out why sometimes finish commit returns "already up-to-date error"
|
||||
// return err
|
||||
}
|
||||
|
||||
err = p.localGitClient.PushBranch(newBranchName)
|
||||
if err != nil {
|
||||
p.log.Error("error pushing branch", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// open code change request
|
||||
_, url, err := p.vcClient.OpenCodeChangeRequest(changeRequest, changeResponse)
|
||||
// TODO don't hardcode main branch, make configurable
|
||||
_, url, err := p.ghClient.OpenCodeChangeRequest(changeRequest, changeResponse, newBranchName, "main")
|
||||
if err != nil {
|
||||
p.log.Error("error opening PR", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
p.log.Info("successfully created PR", zap.String("URL", url))
|
||||
|
||||
p.log.Info("going to sleep for five mins")
|
||||
time.Sleep(5 * time.Minute)
|
||||
p.log.Info("going to sleep for thirty seconds")
|
||||
time.Sleep(30 * time.Second)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
// PickIssueToFile is the same as PickIssue, but the changeRequest is converted to a string and written to a file.
|
||||
func (p *PullPal) PickIssueToFile(promptPath string) (issue vc.Issue, changeRequest llm.CodeChangeRequest, err error) {
|
||||
issue, changeRequest, err = p.PickIssue()
|
||||
@ -180,11 +220,12 @@ func (p *PullPal) PickIssueToClipboard() (issue vc.Issue, changeRequest llm.Code
|
||||
err = clipboard.WriteAll(prompt)
|
||||
return issue, changeRequest, err
|
||||
}
|
||||
|
||||
*/
|
||||
/*
|
||||
// PickIssue selects an issue from the version control server and returns the selected issue, as well as the LLM prompt needed to fulfill the request.
|
||||
func (p *PullPal) PickIssue() (issue vc.Issue, changeRequest llm.CodeChangeRequest, err error) {
|
||||
// TODO I should be able to pass in settings for listing issues from here
|
||||
issues, err := p.vcClient.ListOpenIssues(p.listIssueOptions)
|
||||
issues, err := p.ghClient.ListOpenIssues(p.listIssueOptions)
|
||||
if err != nil {
|
||||
return issue, changeRequest, err
|
||||
}
|
||||
@ -209,7 +250,7 @@ func (p *PullPal) PickIssue() (issue vc.Issue, changeRequest llm.CodeChangeReque
|
||||
files := []llm.File{}
|
||||
for _, path := range fileList {
|
||||
path = strings.TrimSpace(path)
|
||||
nextFile, err := p.vcClient.GetLocalFile(path)
|
||||
nextFile, err := p.ghClient.GetLocalFile(path)
|
||||
if err != nil {
|
||||
return issue, changeRequest, err
|
||||
}
|
||||
@ -223,7 +264,8 @@ func (p *PullPal) PickIssue() (issue vc.Issue, changeRequest llm.CodeChangeReque
|
||||
|
||||
return issue, changeRequest, nil
|
||||
}
|
||||
|
||||
*/
|
||||
/*
|
||||
// ProcessResponseFromFile is the same as ProcessResponse, but the response is inputted into a file rather than passed directly as an argument.
|
||||
func (p *PullPal) ProcessResponseFromFile(codeChangeRequest llm.CodeChangeRequest, llmResponsePath string) (url string, err error) {
|
||||
data, err := ioutil.ReadFile(llmResponsePath)
|
||||
@ -239,31 +281,33 @@ func (p *PullPal) ProcessResponse(codeChangeRequest llm.CodeChangeRequest, llmRe
|
||||
codeChangeResponse := llm.ParseCodeChangeResponse(llmResponse)
|
||||
|
||||
// 2. create commit with file changes
|
||||
err = p.vcClient.StartCommit()
|
||||
err = p.ghClient.StartCommit()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, f := range codeChangeResponse.Files {
|
||||
err = p.vcClient.ReplaceOrAddLocalFile(f)
|
||||
err = p.ghClient.ReplaceOrAddLocalFile(f)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
commitMessage := codeChangeRequest.Subject + "\n\n" + codeChangeResponse.Notes + "\n\nResolves: #" + codeChangeRequest.IssueID
|
||||
err = p.vcClient.FinishCommit(commitMessage)
|
||||
err = p.ghClient.FinishCommit(commitMessage)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 3. open code change request
|
||||
_, url, err = p.vcClient.OpenCodeChangeRequest(codeChangeRequest, codeChangeResponse)
|
||||
_, url, err = p.ghClient.OpenCodeChangeRequest(codeChangeRequest, codeChangeResponse)
|
||||
return url, err
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
// ListIssues gets a list of all issues meeting the provided criteria.
|
||||
func (p *PullPal) ListIssues(handles, labels []string) ([]vc.Issue, error) {
|
||||
issues, err := p.vcClient.ListOpenIssues(vc.ListIssueOptions{
|
||||
issues, err := p.ghClient.ListOpenIssues(vc.ListIssueOptions{
|
||||
Handles: handles,
|
||||
Labels: labels,
|
||||
})
|
||||
@ -276,7 +320,7 @@ func (p *PullPal) ListIssues(handles, labels []string) ([]vc.Issue, error) {
|
||||
|
||||
// ListComments gets a list of all comments meeting the provided criteria on a PR.
|
||||
func (p *PullPal) ListComments(changeID string, handles []string) ([]vc.Comment, error) {
|
||||
comments, err := p.vcClient.ListOpenComments(vc.ListCommentOptions{
|
||||
comments, err := p.ghClient.ListOpenComments(vc.ListCommentOptions{
|
||||
ChangeID: changeID,
|
||||
Handles: handles,
|
||||
})
|
||||
@ -302,7 +346,7 @@ func (p *PullPal) MakeLocalChange(issue vc.Issue) error {
|
||||
files := []llm.File{}
|
||||
for _, path := range fileList {
|
||||
path = strings.TrimSpace(path)
|
||||
nextFile, err := p.vcClient.GetLocalFile(path)
|
||||
nextFile, err := p.ghClient.GetLocalFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -326,3 +370,4 @@ func (p *PullPal) MakeLocalChange(issue vc.Issue) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
*/
|
||||
|
47
pullpal/debug.go
Normal file
47
pullpal/debug.go
Normal file
@ -0,0 +1,47 @@
|
||||
package pullpal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mobyvb/pull-pal/llm"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (p *PullPal) DebugGit() error {
|
||||
p.log.Info("Starting Pull Pal git debug")
|
||||
|
||||
// create commit with file changes
|
||||
err := p.localGitClient.StartCommit()
|
||||
//err = p.ghClient.StartCommit()
|
||||
if err != nil {
|
||||
p.log.Error("error starting commit", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
newBranchName := fmt.Sprintf("debug-branch")
|
||||
|
||||
for _, f := range []string{"a", "b"} {
|
||||
err = p.localGitClient.ReplaceOrAddLocalFile(llm.File{
|
||||
Path: f,
|
||||
Contents: "hello",
|
||||
})
|
||||
if err != nil {
|
||||
p.log.Error("error replacing or adding file", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
commitMessage := "debug commit message"
|
||||
err = p.localGitClient.FinishCommit(commitMessage)
|
||||
if err != nil {
|
||||
p.log.Error("error finishing commit", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
err = p.localGitClient.PushBranch(newBranchName)
|
||||
if err != nil {
|
||||
p.log.Error("error pushing branch", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
23
vc/common.go
23
vc/common.go
@ -3,8 +3,6 @@ package vc
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mobyvb/pull-pal/llm"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
)
|
||||
|
||||
@ -82,24 +80,3 @@ func (repo Repository) SSH() string {
|
||||
func (repo Repository) HTTPS() string {
|
||||
return fmt.Sprintf("https://%s/%s/%s.git", repo.HostDomain, repo.Owner.Handle, repo.Name)
|
||||
}
|
||||
|
||||
// VCClient is an interface for version control server's client, e.g. a Github or Gerrit client.
|
||||
type VCClient interface {
|
||||
// ListOpenIssues lists unresolved issues meeting the provided criteria on the version control server.
|
||||
ListOpenIssues(opts ListIssueOptions) ([]Issue, error)
|
||||
// ListOpenComments lists unresolved comments meeting the provided criteria on the version control server.
|
||||
ListOpenComments(opts ListCommentOptions) ([]Comment, error)
|
||||
// OpenCodeChangeRequest opens a new "code change request" on the version control server (e.g. "pull request" in Github).
|
||||
OpenCodeChangeRequest(req llm.CodeChangeRequest, res llm.CodeChangeResponse) (id, url string, err error)
|
||||
// UpdateCodeChangeRequest updates an existing code change request on the version control server.
|
||||
// UpdateCodeChangeRequest(id string, res llm.CodeChangeResponse)
|
||||
// TODO: add/read comments to/from issues and code change requests
|
||||
// GetLocalFile gets the current representation of the file at the provided path from the local git repo.
|
||||
GetLocalFile(path string) (llm.File, error)
|
||||
// StartCommit initiates a commit process, after which files can be modified and added to the commit.
|
||||
StartCommit() error
|
||||
// ReplaceOrAddLocalFile updates or adds a file in the locally cloned repo, and applies these changes to the current git worktree.
|
||||
ReplaceOrAddLocalFile(newFile llm.File) error
|
||||
// FinishCommit completes a commit, after which a code change request can be opened or updated.
|
||||
FinishCommit(message string) error
|
||||
}
|
||||
|
51
vc/git.go
51
vc/git.go
@ -11,10 +11,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mobyvb/pull-pal/llm"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/config"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
)
|
||||
@ -28,7 +28,8 @@ type LocalGitClient struct {
|
||||
}
|
||||
|
||||
// NewLocalGitClient initializes a local git client by checking out a repository locally.
|
||||
func NewLocalGitClient( /*ctx context.Context, log *zap.Logger, */ self Author, repo Repository) (*LocalGitClient, error) {
|
||||
func NewLocalGitClient( /*ctx context.Context, */ log *zap.Logger, self Author, repo Repository) (*LocalGitClient, error) {
|
||||
log.Info("checking out local github repo", zap.String("repo name", repo.Name), zap.String("local path", repo.LocalPath))
|
||||
// clone provided repository to local path
|
||||
if repo.LocalPath == "" {
|
||||
return nil, errors.New("local path to clone repository not provided")
|
||||
@ -59,24 +60,44 @@ func NewLocalGitClient( /*ctx context.Context, log *zap.Logger, */ self Author,
|
||||
}, nil
|
||||
}
|
||||
|
||||
/*
|
||||
func (gc *LocalGitClient) SwitchBranch(branchName string) (err error) {
|
||||
branchRefName := plumbing.NewBranchReferenceName(branchName)
|
||||
remoteName := "origin"
|
||||
if gc.worktree == nil {
|
||||
return errors.New("worktree is nil - cannot check out a branch")
|
||||
}
|
||||
|
||||
err = gc.repo.localRepo.CreateBranch(&config.Branch{
|
||||
Name: branchName,
|
||||
Remote: remoteName,
|
||||
Merge: branchRefName,
|
||||
branchRefName := plumbing.NewBranchReferenceName(branchName)
|
||||
// remoteName := "origin"
|
||||
|
||||
err = gc.repo.localRepo.Fetch(&git.FetchOptions{
|
||||
RefSpecs: []config.RefSpec{"refs/*:refs/*", "HEAD:refs/heads/HEAD"},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = gc.worktree.Checkout(&git.CheckoutOptions{
|
||||
Branch: branchRefName,
|
||||
Force: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = gc.repo.localRepo.CreateBranch(&config.Branch{
|
||||
Name: branchName,
|
||||
Remote: remoteName,
|
||||
Merge: branchRefName,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
*/
|
||||
|
||||
func (gc *LocalGitClient) PushBranch(branchName string) (err error) {
|
||||
branchRefName := plumbing.NewBranchReferenceName(branchName)
|
||||
//branchRefName := plumbing.NewBranchReferenceName(branchName)
|
||||
remoteName := "origin"
|
||||
|
||||
// Push the new branch to the remote repository
|
||||
@ -86,7 +107,9 @@ func (gc *LocalGitClient) PushBranch(branchName string) (err error) {
|
||||
}
|
||||
|
||||
err = remote.Push(&git.PushOptions{
|
||||
RefSpecs: []config.RefSpec{config.RefSpec(fmt.Sprintf("%s:refs/heads/%s", branchRefName, branchName))},
|
||||
RemoteName: remoteName,
|
||||
// TODO remove hardcoded "main"
|
||||
RefSpecs: []config.RefSpec{config.RefSpec(fmt.Sprintf("+refs/heads/%s:refs/heads/%s", "main", branchName))},
|
||||
Auth: &http.BasicAuth{
|
||||
Username: gc.self.Handle,
|
||||
Password: gc.self.Token,
|
||||
@ -146,9 +169,13 @@ func (gc *LocalGitClient) ReplaceOrAddLocalFile(newFile llm.File) error {
|
||||
if strings.HasSuffix(newFile.Path, ".go") {
|
||||
newContents, err := format.Source([]byte(newFile.Contents))
|
||||
if err != nil {
|
||||
return err
|
||||
// TODO also make logger accessible
|
||||
fmt.Println("go format error")
|
||||
// TODO handle this error
|
||||
// return err
|
||||
} else {
|
||||
newFile.Contents = string(newContents)
|
||||
}
|
||||
newFile.Contents = string(newContents)
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(gc.repo.LocalPath, newFile.Path)
|
||||
|
219
vc/github.go
219
vc/github.go
@ -2,25 +2,12 @@ package vc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mobyvb/pull-pal/llm"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/config"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
"github.com/google/go-github/github"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/oauth2"
|
||||
@ -34,8 +21,6 @@ type GithubClient struct {
|
||||
client *github.Client
|
||||
self Author
|
||||
repo Repository
|
||||
|
||||
worktree *git.Worktree
|
||||
}
|
||||
|
||||
// NewGithubClient initializes a Github client and checks out a repository locally.
|
||||
@ -50,35 +35,6 @@ func NewGithubClient(ctx context.Context, log *zap.Logger, self Author, repo Rep
|
||||
// oauth client is used to list issues, open pull requests, etc...
|
||||
tc := oauth2.NewClient(ctx, ts)
|
||||
|
||||
// clone provided repository to local path
|
||||
if repo.LocalPath == "" {
|
||||
return nil, errors.New("local path to clone repository not provided")
|
||||
}
|
||||
|
||||
if repo.LocalPath != "" {
|
||||
// remove local repo if it exists already
|
||||
err := os.RemoveAll(repo.LocalPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Cloning repository locally...", zap.String("local repo path", repo.LocalPath), zap.String("url", repo.SSH()))
|
||||
// TODO this can be done in-memory - see https://pkg.go.dev/github.com/go-git/go-git/v5#readme-in-memory-example
|
||||
localRepo, err := git.PlainClone(repo.LocalPath, false, &git.CloneOptions{
|
||||
URL: repo.SSH(),
|
||||
// URL: repo.HTTPS(),
|
||||
Auth: &http.BasicAuth{
|
||||
Username: self.Handle,
|
||||
Password: self.Token,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Info("failed")
|
||||
return nil, err
|
||||
}
|
||||
repo.localRepo = localRepo
|
||||
|
||||
log.Info("Success. Github client set up.")
|
||||
|
||||
return &GithubClient{
|
||||
@ -91,62 +47,22 @@ 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.
|
||||
func (gc *GithubClient) OpenCodeChangeRequest(req llm.CodeChangeRequest, res llm.CodeChangeResponse) (id, url string, err error) {
|
||||
func (gc *GithubClient) OpenCodeChangeRequest(req llm.CodeChangeRequest, res llm.CodeChangeResponse, fromBranch, toBranch string) (id, url string, err error) {
|
||||
// TODO handle gc.ctx canceled
|
||||
|
||||
title := req.Subject
|
||||
if title == "" {
|
||||
title = "update files"
|
||||
}
|
||||
branchName := randomBranchName()
|
||||
branchRefName := plumbing.NewBranchReferenceName(branchName)
|
||||
baseBranch := "main"
|
||||
remoteName := "origin"
|
||||
|
||||
body := res.Notes
|
||||
body += fmt.Sprintf("\n\nResolves #%s", req.IssueID)
|
||||
|
||||
// Create new local branch
|
||||
headRef, err := gc.repo.localRepo.Head()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
err = gc.repo.localRepo.CreateBranch(&config.Branch{
|
||||
Name: branchName,
|
||||
Remote: remoteName,
|
||||
Merge: branchRefName,
|
||||
})
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Update the branch to point to the new commit
|
||||
err = gc.repo.localRepo.Storer.SetReference(plumbing.NewHashReference(branchRefName, headRef.Hash()))
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Push the new branch to the remote repository
|
||||
remote, err := gc.repo.localRepo.Remote(remoteName)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
err = remote.Push(&git.PushOptions{
|
||||
RefSpecs: []config.RefSpec{config.RefSpec(fmt.Sprintf("%s:refs/heads/%s", branchRefName, branchName))},
|
||||
Auth: &http.BasicAuth{
|
||||
Username: gc.self.Handle,
|
||||
Password: gc.self.Token,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// 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{
|
||||
Title: &title,
|
||||
Head: &branchName,
|
||||
Base: &baseBranch,
|
||||
Head: &fromBranch,
|
||||
Base: &toBranch,
|
||||
Body: &body,
|
||||
})
|
||||
if err != nil {
|
||||
@ -159,12 +75,6 @@ func (gc *GithubClient) OpenCodeChangeRequest(req llm.CodeChangeRequest, res llm
|
||||
return id, url, nil
|
||||
}
|
||||
|
||||
func randomBranchName() string {
|
||||
bytes := make([]byte, 4)
|
||||
rand.Read(bytes)
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
// ListOpenIssues lists unresolved issues in the Github repository.
|
||||
func (gc *GithubClient) ListOpenIssues(options ListIssueOptions) ([]Issue, error) {
|
||||
// List and parse GitHub issues
|
||||
@ -206,6 +116,39 @@ func (gc *GithubClient) ListOpenIssues(options ListIssueOptions) ([]Issue, error
|
||||
return toReturn, nil
|
||||
}
|
||||
|
||||
// CommentOnIssue adds a comment to the issue provided.
|
||||
func (gc *GithubClient) CommentOnIssue(issueNumber int, comment string) error {
|
||||
ghComment := &github.IssueComment{
|
||||
Body: github.String(comment),
|
||||
}
|
||||
|
||||
_, _, err := gc.client.Issues.CreateComment(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, issueNumber, ghComment)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveLabelFromIssue removes the provided label from an issue if that label is applied.
|
||||
func (gc *GithubClient) RemoveLabelFromIssue(issueNumber int, label string) error {
|
||||
hasLabel := false
|
||||
labels, _, err := gc.client.Issues.ListLabelsByIssue(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, issueNumber, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, l := range labels {
|
||||
if l.GetName() == label {
|
||||
hasLabel = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasLabel {
|
||||
_, err = gc.client.Issues.RemoveLabelForIssue(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, issueNumber, label)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListOpenComments lists unresolved comments in the Github repository.
|
||||
func (gc *GithubClient) ListOpenComments(options ListCommentOptions) ([]Comment, error) {
|
||||
toReturn := []Comment{}
|
||||
@ -250,91 +193,3 @@ func (gc *GithubClient) ListOpenComments(options ListCommentOptions) ([]Comment,
|
||||
|
||||
return toReturn, nil
|
||||
}
|
||||
|
||||
// GetLocalFile gets the current representation of the file at the provided path from the local git repo.
|
||||
func (gc *GithubClient) GetLocalFile(path string) (llm.File, error) {
|
||||
fullPath := filepath.Join(gc.repo.LocalPath, path)
|
||||
|
||||
data, err := ioutil.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
// if file doesn't exist, just return an empty file
|
||||
// this means we want to prompt the llm to populate it for the first time
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return llm.File{
|
||||
Path: path,
|
||||
Contents: "",
|
||||
}, nil
|
||||
}
|
||||
return llm.File{}, err
|
||||
}
|
||||
|
||||
return llm.File{
|
||||
Path: path,
|
||||
Contents: string(data),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StartCommit creates a new worktree associated with this Github client.
|
||||
func (gc *GithubClient) StartCommit() error {
|
||||
if gc.worktree != nil {
|
||||
return errors.New("worktree is not nil - cannot start a new commit")
|
||||
}
|
||||
|
||||
worktree, err := gc.repo.localRepo.Worktree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gc.worktree = worktree
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReplaceOrAddLocalFile updates or adds a file in the locally cloned repo, and applies these changes to the current git worktree.
|
||||
func (gc *GithubClient) ReplaceOrAddLocalFile(newFile llm.File) error {
|
||||
if gc.worktree == nil {
|
||||
return errors.New("worktree is nil - StartCommit must be called")
|
||||
}
|
||||
|
||||
// TODO format non-go files as well
|
||||
if strings.HasSuffix(newFile.Path, ".go") {
|
||||
newContents, err := format.Source([]byte(newFile.Contents))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newFile.Contents = string(newContents)
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(gc.repo.LocalPath, newFile.Path)
|
||||
|
||||
err := ioutil.WriteFile(fullPath, []byte(newFile.Contents), 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = gc.worktree.Add(newFile.Path)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// FinishCommit completes a commit, after which a code change request can be opened or updated.
|
||||
func (gc *GithubClient) FinishCommit(message string) error {
|
||||
if gc.worktree == nil {
|
||||
return errors.New("worktree is nil - StartCommit must be called")
|
||||
}
|
||||
_, err := gc.worktree.Commit(message, &git.CommitOptions{
|
||||
Author: &object.Signature{
|
||||
Name: gc.self.Handle,
|
||||
Email: gc.self.Email,
|
||||
When: time.Now(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set worktree to nil so a new commit can be started
|
||||
gc.worktree = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user