mirror of
				https://github.com/Pull-Pal/pull-pal.git
				synced 2025-11-03 18:27:28 -05:00 
			
		
		
		
	* 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
		
			
				
	
	
		
			236 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			236 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package vc
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"github.com/mobyvb/pull-pal/llm"
 | 
						|
 | 
						|
	"github.com/google/go-github/github"
 | 
						|
	"go.uber.org/zap"
 | 
						|
	"golang.org/x/oauth2"
 | 
						|
)
 | 
						|
 | 
						|
// GithubClient implements the VCClient interface.
 | 
						|
type GithubClient struct {
 | 
						|
	ctx context.Context
 | 
						|
	log *zap.Logger
 | 
						|
 | 
						|
	client *github.Client
 | 
						|
	self   Author
 | 
						|
	repo   Repository
 | 
						|
}
 | 
						|
 | 
						|
// NewGithubClient initializes a Github client and checks out a repository locally.
 | 
						|
func NewGithubClient(ctx context.Context, log *zap.Logger, self Author, repo Repository) (*GithubClient, error) {
 | 
						|
	log.Info("Creating new Github client...")
 | 
						|
	if self.Token == "" {
 | 
						|
		return nil, errors.New("Github access token not provided")
 | 
						|
	}
 | 
						|
	ts := oauth2.StaticTokenSource(
 | 
						|
		&oauth2.Token{AccessToken: self.Token},
 | 
						|
	)
 | 
						|
	// oauth client is used to list issues, open pull requests, etc...
 | 
						|
	tc := oauth2.NewClient(ctx, ts)
 | 
						|
 | 
						|
	log.Info("Success. Github client set up.")
 | 
						|
 | 
						|
	return &GithubClient{
 | 
						|
		ctx:    ctx,
 | 
						|
		log:    log,
 | 
						|
		client: github.NewClient(tc),
 | 
						|
		self:   self,
 | 
						|
		repo:   repo,
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
// OpenCodeChangeRequest pushes to a new remote branch and opens a PR on Github.
 | 
						|
func (gc *GithubClient) OpenCodeChangeRequest(req llm.CodeChangeRequest, res llm.CodeChangeResponse, fromBranch string) (id, url string, err error) {
 | 
						|
	// TODO handle gc.ctx canceled
 | 
						|
 | 
						|
	title := req.Subject
 | 
						|
	if title == "" {
 | 
						|
		title = "update files"
 | 
						|
	}
 | 
						|
 | 
						|
	body := res.Notes
 | 
						|
	body += fmt.Sprintf("\n\nResolves #%d", req.IssueNumber)
 | 
						|
 | 
						|
	// 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:  &fromBranch,
 | 
						|
		Base:  &req.BaseBranch,
 | 
						|
		Body:  &body,
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		return "", "", err
 | 
						|
	}
 | 
						|
 | 
						|
	url = pr.GetHTMLURL()
 | 
						|
	id = strconv.Itoa(int(pr.GetID()))
 | 
						|
 | 
						|
	return id, url, nil
 | 
						|
}
 | 
						|
 | 
						|
// ListOpenIssues lists unresolved issues in the Github repository.
 | 
						|
func (gc *GithubClient) ListOpenIssues(options ListIssueOptions) ([]Issue, error) {
 | 
						|
	// List and parse GitHub issues
 | 
						|
	opt := &github.IssueListByRepoOptions{
 | 
						|
		Labels: options.Labels,
 | 
						|
	}
 | 
						|
	issues, _, err := gc.client.Issues.ListByRepo(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, opt)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	toReturn := []Issue{}
 | 
						|
	for _, issue := range issues {
 | 
						|
		issueUser := issue.GetUser().GetLogin()
 | 
						|
		allowedUser := false
 | 
						|
		for _, u := range options.Handles {
 | 
						|
			if issueUser == u {
 | 
						|
				allowedUser = true
 | 
						|
				break
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if !allowedUser {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		nextIssue := Issue{
 | 
						|
			Number:  issue.GetNumber(),
 | 
						|
			Subject: issue.GetTitle(),
 | 
						|
			Body:    issue.GetBody(),
 | 
						|
			URL:     issue.GetHTMLURL(),
 | 
						|
			Author: Author{
 | 
						|
				Email:  issue.GetUser().GetEmail(),
 | 
						|
				Handle: issue.GetUser().GetLogin(),
 | 
						|
			},
 | 
						|
		}
 | 
						|
		toReturn = append(toReturn, nextIssue)
 | 
						|
	}
 | 
						|
 | 
						|
	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) {
 | 
						|
	prs, _, err := gc.client.PullRequests.List(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, nil)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	allComments := []Comment{}
 | 
						|
	repliedTo := make(map[int64]bool)
 | 
						|
 | 
						|
	for _, pr := range prs {
 | 
						|
		if pr.GetUser().GetLogin() != gc.self.Handle {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		branch := ""
 | 
						|
		if pr.Head != nil {
 | 
						|
			branch = pr.Head.GetLabel()
 | 
						|
			if strings.Contains(branch, ":") {
 | 
						|
				branch = strings.Split(branch, ":")[1]
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		comments, _, err := gc.client.PullRequests.ListComments(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, pr.GetNumber(), nil)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
 | 
						|
		for _, c := range comments {
 | 
						|
			commentUser := c.GetUser().GetLogin()
 | 
						|
			if commentUser == gc.self.Handle {
 | 
						|
				repliedTo[c.GetInReplyTo()] = true
 | 
						|
			}
 | 
						|
			allowedUser := false
 | 
						|
			for _, u := range options.Handles {
 | 
						|
				if commentUser == u {
 | 
						|
					allowedUser = true
 | 
						|
					break
 | 
						|
				}
 | 
						|
			}
 | 
						|
			if !allowedUser {
 | 
						|
				continue
 | 
						|
			}
 | 
						|
 | 
						|
			nextComment := Comment{
 | 
						|
				ID:       c.GetID(),
 | 
						|
				ChangeID: strconv.Itoa(pr.GetNumber()),
 | 
						|
				URL:      c.GetHTMLURL(),
 | 
						|
				Author: Author{
 | 
						|
					Email:  c.GetUser().GetEmail(),
 | 
						|
					Handle: c.GetUser().GetLogin(),
 | 
						|
				},
 | 
						|
				Body:     c.GetBody(),
 | 
						|
				FilePath: c.GetPath(),
 | 
						|
				Position: c.GetPosition(),
 | 
						|
				DiffHunk: c.GetDiffHunk(),
 | 
						|
				Branch:   branch,
 | 
						|
				PRNumber: pr.GetNumber(),
 | 
						|
			}
 | 
						|
			allComments = append(allComments, nextComment)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// remove any comments that bot has replied to already from the list
 | 
						|
	toReturn := []Comment{}
 | 
						|
	for _, c := range allComments {
 | 
						|
		if !repliedTo[c.ID] {
 | 
						|
			toReturn = append(toReturn, c)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return toReturn, nil
 | 
						|
}
 | 
						|
 | 
						|
// RespondToComment adds a comment to the provided thread.
 | 
						|
func (gc *GithubClient) RespondToComment(prNumber int, commentID int64, comment string) error {
 | 
						|
	_, _, err := gc.client.PullRequests.CreateCommentInReplyTo(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, prNumber, comment, commentID)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	return err
 | 
						|
}
 |