experimental: codex / ai integration
This commit is contained in:
188
ai_diff.go
Normal file
188
ai_diff.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (c *GitHubClient) PullRequestDiff(ctx context.Context, owner, repo string, number int) (string, error) {
|
||||
base := strings.TrimSuffix(c.endpoint, "/")
|
||||
switch {
|
||||
case base == "https://api.github.com/graphql":
|
||||
base = "https://api.github.com"
|
||||
case strings.HasSuffix(base, "/api/graphql"):
|
||||
base = strings.TrimSuffix(base, "/api/graphql") + "/api/v3"
|
||||
default:
|
||||
base = strings.TrimSuffix(base, "/graphql")
|
||||
}
|
||||
requestURL := base + "/repos/" + url.PathEscape(owner) + "/" + url.PathEscape(repo) +
|
||||
"/pulls/" + strconv.Itoa(number)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
req.Header.Set("Accept", "application/vnd.github.v3.diff")
|
||||
req.Header.Set("User-Agent", "diple")
|
||||
response, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(io.LimitReader(response.Body, 4096))
|
||||
return "", fmt.Errorf("GitHub returned %s: %s", response.Status, strings.TrimSpace(string(body)))
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(response.Body, (32<<20)+1))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(body) > 32<<20 {
|
||||
return "", fmt.Errorf("PR diff exceeds the 32 MiB safety limit")
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func (c *CachedGitHubService) PullRequestDiff(ctx context.Context, owner, repo string, number int) (string, error) {
|
||||
service, ok := c.remote.(AIDiffService)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("configured GitHub service cannot load pull request diffs")
|
||||
}
|
||||
return service.PullRequestDiff(ctx, owner, repo, number)
|
||||
}
|
||||
|
||||
func prepareAIDiff(raw string, config AIConfig) ([]aiDiffFile, []string, int) {
|
||||
sections := strings.Split(raw, "\ndiff --git ")
|
||||
var files []aiDiffFile
|
||||
var excluded []string
|
||||
redactions := 0
|
||||
for sectionIndex, section := range sections {
|
||||
if sectionIndex > 0 {
|
||||
section = "diff --git " + section
|
||||
}
|
||||
file, ok := parseAIDiffFile(section)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
reason := aiExcludedReason(file, config)
|
||||
if reason != "" {
|
||||
excluded = append(excluded, file.Path+" ("+reason+")")
|
||||
continue
|
||||
}
|
||||
redacted, count := redactAISecrets(file.Text)
|
||||
file.Text = sanitizeAIControls(redacted)
|
||||
redactions += count
|
||||
files = append(files, file)
|
||||
}
|
||||
return files, excluded, redactions
|
||||
}
|
||||
|
||||
func parseAIDiffFile(section string) (aiDiffFile, bool) {
|
||||
scanner := bufio.NewScanner(strings.NewReader(section))
|
||||
scanner.Buffer(make([]byte, 4096), 2<<20)
|
||||
file := aiDiffFile{
|
||||
ChangedLines: make(map[int]bool), DeletedLines: make(map[int]bool), Text: section,
|
||||
}
|
||||
oldPath := ""
|
||||
oldLine := 0
|
||||
newLine := 0
|
||||
inHunk := false
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "--- ") && !inHunk {
|
||||
candidate := strings.TrimPrefix(line, "--- ")
|
||||
if strings.HasPrefix(candidate, `"`) {
|
||||
if unquoted, err := strconv.Unquote(candidate); err == nil {
|
||||
candidate = unquoted
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(candidate, "a/") {
|
||||
oldPath = strings.TrimPrefix(candidate, "a/")
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(line, "+++ ") && !inHunk {
|
||||
candidate := strings.TrimPrefix(line, "+++ ")
|
||||
if strings.HasPrefix(candidate, `"`) {
|
||||
if unquoted, err := strconv.Unquote(candidate); err == nil {
|
||||
candidate = unquoted
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(candidate, "b/") {
|
||||
file.Path = strings.TrimPrefix(candidate, "b/")
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(line, "@@ ") {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 3 {
|
||||
oldRange := strings.TrimPrefix(parts[1], "-")
|
||||
oldStart := strings.SplitN(oldRange, ",", 2)[0]
|
||||
oldLine, _ = strconv.Atoi(oldStart)
|
||||
rangePart := strings.TrimPrefix(parts[2], "+")
|
||||
start := strings.SplitN(rangePart, ",", 2)[0]
|
||||
newLine, _ = strconv.Atoi(start)
|
||||
inHunk = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !inHunk || line == "" {
|
||||
continue
|
||||
}
|
||||
switch line[0] {
|
||||
case '+':
|
||||
file.ChangedLines[newLine] = true
|
||||
newLine++
|
||||
case '-':
|
||||
file.DeletedLines[oldLine] = true
|
||||
oldLine++
|
||||
default:
|
||||
oldLine++
|
||||
newLine++
|
||||
}
|
||||
}
|
||||
if file.Path == "" {
|
||||
file.Path = oldPath
|
||||
}
|
||||
if file.Path == "" || file.Path == "/dev/null" {
|
||||
return aiDiffFile{}, false
|
||||
}
|
||||
file.Path = filepath.ToSlash(filepath.Clean(file.Path))
|
||||
if strings.HasPrefix(file.Path, "../") || filepath.IsAbs(file.Path) {
|
||||
return aiDiffFile{}, false
|
||||
}
|
||||
return file, len(file.ChangedLines) > 0 || len(file.DeletedLines) > 0
|
||||
}
|
||||
|
||||
func aiExcludedReason(file aiDiffFile, config AIConfig) string {
|
||||
if len(file.Text) > config.MaxFileBytes {
|
||||
return "oversized"
|
||||
}
|
||||
if strings.Contains(file.Text, "GIT binary patch") ||
|
||||
strings.Contains(file.Text, "Binary files ") || strings.IndexByte(file.Text, 0) >= 0 {
|
||||
return "binary"
|
||||
}
|
||||
lower := strings.ToLower(file.Path)
|
||||
for _, pattern := range config.Exclude {
|
||||
pattern = filepath.ToSlash(pattern)
|
||||
if strings.HasSuffix(pattern, "/") {
|
||||
directory := strings.ToLower(pattern)
|
||||
if strings.HasPrefix(lower, directory) || strings.Contains(lower, "/"+directory) {
|
||||
return "excluded"
|
||||
}
|
||||
}
|
||||
if matched, _ := path.Match(strings.ToLower(pattern), path.Base(lower)); matched {
|
||||
return "excluded"
|
||||
}
|
||||
if matched, _ := path.Match(strings.ToLower(pattern), lower); matched {
|
||||
return "excluded"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user