Files
kandev/apps/backend/internal/github/service_automation_auth.go
T

155 lines
4.9 KiB
Go

package github
import (
"context"
"errors"
"fmt"
"strings"
)
// exactHeadPRFinder is implemented by provider clients that can constrain a
// pull-request search to both the source repository and branch. A branch name
// is not unique across a fork network, so a parent-repository fallback must
// preserve the source repository identity.
type exactHeadPRFinder interface {
FindPRByHead(ctx context.Context, owner, repo, headOwner, headRepo, branch string) (*PR, error)
}
type repositoryDetailsReader interface {
GetRepository(ctx context.Context, owner, repo string) (*GitHubRepository, error)
}
// ResolveGitHubAutomationClient returns the workspace-owned automation client
// for non-repository GitHub operations such as Gist-backed task sharing.
func (s *Service) ResolveGitHubAutomationClient(ctx context.Context, workspaceID string) (Client, error) {
resolved, err := s.resolveAutomationClient(ctx, workspaceID, "", "")
if err != nil {
return nil, err
}
return resolved.Client, nil
}
func (s *Service) FindPRByBranchForWorkspace(
ctx context.Context,
workspaceID, owner, repo, branch string,
) (*PR, error) {
if err := s.ensureRepositoryInWorkspaceScope(ctx, workspaceID, owner, repo); err != nil {
return nil, err
}
resolved, err := s.resolveAutomationClient(ctx, workspaceID, owner, repo)
if err != nil {
return nil, err
}
return findPRByBranchInForkNetwork(ctx, resolved.Client, owner, repo, branch)
}
// findPRByBranchInForkNetwork first searches the requested repository. When
// that repository is a fork and no PR is found, it searches the parent for an
// open PR whose head is the exact fork owner and branch. This handles PRs that
// target the canonical repository while keeping same-named branches from
// another fork out of the association path.
func findPRByBranchInForkNetwork(
ctx context.Context, client Client, owner, repo, branch string,
) (*PR, error) {
pr, err := client.FindPRByBranch(ctx, owner, repo, branch)
if err != nil || pr != nil {
return pr, err
}
parentOwner, parentRepo, isFork, err := forkParentRepositoryForLookup(ctx, client, owner, repo)
if err != nil {
return nil, err
}
if !isFork {
return nil, nil
}
finder, ok := client.(exactHeadPRFinder)
if !ok {
return nil, nil
}
pr, err = finder.FindPRByHead(ctx, parentOwner, parentRepo, owner, repo, branch)
if err != nil || pr == nil {
return pr, err
}
if !sameRepositoryIdentity(pr.HeadRepoOwner, pr.HeadRepoName, owner, repo) {
return nil, nil
}
return pr, nil
}
func forkParentRepositoryForLookup(
ctx context.Context, client Client, owner, repo string,
) (parentOwner, parentRepo string, isFork bool, err error) {
repositoryReader, ok := client.(repositoryDetailsReader)
if !ok {
return "", "", false, nil
}
repository, err := repositoryReader.GetRepository(ctx, owner, repo)
if err != nil {
var apiErr *GitHubAPIError
if errors.As(err, &apiErr) && apiErr.StatusCode == 404 {
return "", "", false, nil
}
return "", "", false, fmt.Errorf("inspect repository %s/%s for parent PR lookup: %w", owner, repo, err)
}
if repository == nil || !repository.Fork {
return "", "", false, nil
}
parentOwner, parentRepo, ok = parseForkParentFullName(repository.ParentFullName)
if !ok || (strings.EqualFold(parentOwner, owner) && strings.EqualFold(parentRepo, repo)) {
return "", "", false, nil
}
return parentOwner, parentRepo, true, nil
}
func parseForkParentFullName(fullName string) (owner, repo string, ok bool) {
parts := strings.SplitN(strings.TrimSpace(fullName), "/", 2)
if len(parts) != 2 {
return "", "", false
}
owner, repo = strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
return owner, repo, owner != "" && repo != ""
}
func sameRepositoryIdentity(ownerA, repoA, ownerB, repoB string) bool {
return strings.EqualFold(strings.TrimSpace(ownerA), strings.TrimSpace(ownerB)) &&
strings.EqualFold(strings.TrimSpace(repoA), strings.TrimSpace(repoB))
}
func (s *Service) GetPRFeedbackForAutomation(
ctx context.Context,
workspaceID, owner, repo string,
number int,
) (*PRFeedback, error) {
if err := s.ensureRepositoryInWorkspaceScope(ctx, workspaceID, owner, repo); err != nil {
return nil, err
}
resolved, err := s.resolveAutomationClient(ctx, workspaceID, owner, repo)
if err != nil {
return nil, err
}
return s.getPRFeedback(ctx, resolved.Client, resolved.CacheScope, workspaceID, owner, repo, number)
}
func (s *Service) MergePRForAutomation(
ctx context.Context,
workspaceID, owner, repo string,
number int,
mergeMethod string,
) error {
if err := s.ensureRepositoryInWorkspaceScope(ctx, workspaceID, owner, repo); err != nil {
return err
}
resolved, err := s.resolveAutomationClient(ctx, workspaceID, owner, repo)
if err != nil {
return err
}
if err := requireGitHubCapability(resolved, CapabilityPullRequestWrite); err != nil {
return err
}
return s.mergePRWithClient(
ctx, resolved.Client, resolved.CacheScope, owner, repo, number, mergeMethod,
)
}