mirror of
https://github.com/Pull-Pal/pull-pal.git
synced 2024-06-12 03:10:43 +00:00
big cleanup
This commit is contained in:
parent
eea53db3a2
commit
0c69b2fd4c
|
@ -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)
|
||||
}
|
90
cmd/root.go
90
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,67 +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)
|
||||
*/
|
||||
_ = changeRequest
|
||||
err = p.Run()
|
||||
if err != nil {
|
||||
fmt.Println("error running", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Done. Thank you!")
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -201,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")
|
||||
|
||||
|
@ -219,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"))
|
||||
}
|
||||
|
|
32
cmd/run.go
32
cmd/run.go
|
@ -1,32 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var runCmd = &cobra.Command{
|
||||
Use: "run",
|
||||
Short: "Runs a fully automated pull pal service",
|
||||
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")
|
||||
|
||||
err = p.Run()
|
||||
if err != nil {
|
||||
fmt.Println("error running", err)
|
||||
return
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(runCmd)
|
||||
}
|
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,7 +72,7 @@ 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 := ""
|
||||
if len(sections) > 0 {
|
||||
|
@ -93,7 +93,7 @@ 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{}
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ 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
|
||||
}
|
||||
|
|
|
@ -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,9 +39,9 @@ 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
|
||||
|
||||
// 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]
|
5
main.go
5
main.go
|
@ -3,10 +3,5 @@ package main
|
|||
import "github.com/mobyvb/pull-pal/cmd"
|
||||
|
||||
func main() {
|
||||
/*
|
||||
response := "Files:\n\n - name: main.go\n contents:\n ```go\n package main\n\n import (\n \"fmt\"\n \"log\"\n \"net/http\"\n )\n\n func main() {\n http.Handle(\"/\", http.FileServer(http.Dir(\"./\")))\n fmt.Println(\"Server listening on :7777\")\n log.Fatal(http.ListenAndServe(\":7777\", nil))\n }\n ```\n\n - name: index.html\n contents:\n ```html\n <!DOCTYPE html>\n <html>\n <head>\n <title>Pull Pal</title>\n <style>\n body {\n font-family: sans-serif;\n }\n .content {\n background-color: #f2f2f2;\n border-radius: 10px;\n box-shadow: 2px 2px 10px rgba(0,0,0,0.2);\n padding: 20px;\n margin: 20px;\n }\n h1 {\n color: #3399cc;\n background-color: #f2f2f2;\n text-align: center;\n padding: 10px;\n border-radius: 10px;\n box-shadow: 2px 2px 10px rgba(0,0,0,0.2);\n }\n </style>\n </head>\n <body>\n <div class=\"content\">\n <h1>Introducing Pull Pal!</h1>\n <p>Pull Pal is a digital assistant that can monitor your Github repositories and create pull requests using the power of artificial intelligence. Say goodbye to manual pull request creation like it's 2005!</p>\n <p>Sign up now to start automating your workflow and get more done in less time. </p>\n </div>\n </body>\n </html>\n ```\n\nNotes:\n- Added a basic HTTP server in main.go which serves index.html from the root path on port 7777\n- Added basic HTML structure and CSS styling to index.html, using sans-serif font and a soft color scheme with blue as the main accent color\n- Added a content container with an off-white background, rounded corners, and a box shadow, and a centered heading with blue font color and an off-white background to draw attention to the product name and value proposition"
|
||||
res := llm.ParseCodeChangeResponse(response)
|
||||
fmt.Println(res.String())
|
||||
*/
|
||||
cmd.Execute()
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -12,7 +11,6 @@ import (
|
|||
"github.com/mobyvb/pull-pal/llm"
|
||||
"github.com/mobyvb/pull-pal/vc"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
|
@ -20,10 +18,10 @@ 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
|
||||
|
@ -40,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
|
||||
}
|
||||
|
@ -108,7 +106,7 @@ func (p *PullPal) Run() error {
|
|||
files := []llm.File{}
|
||||
for _, path := range fileList {
|
||||
path = strings.TrimSpace(path)
|
||||
nextFile, err := p.ghClient.GetLocalFile(path)
|
||||
nextFile, err := p.localGitClient.GetLocalFile(path)
|
||||
if err != nil {
|
||||
p.log.Error("error getting file from vcclient", zap.Error(err))
|
||||
continue
|
||||
|
@ -189,6 +187,8 @@ func (p *PullPal) Run() error {
|
|||
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()
|
||||
|
@ -220,7 +220,8 @@ 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
|
||||
|
@ -263,7 +264,7 @@ 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) {
|
||||
|
@ -303,6 +304,7 @@ func (p *PullPal) ProcessResponse(codeChangeRequest llm.CodeChangeRequest, llmRe
|
|||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
// ListIssues gets a list of all issues meeting the provided criteria.
|
||||
func (p *PullPal) ListIssues(handles, labels []string) ([]vc.Issue, error) {
|
||||
issues, err := p.ghClient.ListOpenIssues(vc.ListIssueOptions{
|
||||
|
@ -368,3 +370,4 @@ func (p *PullPal) MakeLocalChange(issue vc.Issue) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
*/
|
||||
|
|
23
vc/common.go
23
vc/common.go
|
@ -80,26 +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
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -11,6 +11,7 @@ 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"
|
||||
|
@ -27,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")
|
||||
|
|
182
vc/github.go
182
vc/github.go
|
@ -2,23 +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/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"
|
||||
|
@ -32,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.
|
||||
|
@ -48,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{
|
||||
|
@ -96,54 +54,10 @@ func (gc *GithubClient) OpenCodeChangeRequest(req llm.CodeChangeRequest, res llm
|
|||
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,
|
||||
|
@ -161,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
|
||||
|
@ -285,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