update QoL and ai integration
This commit is contained in:
167
ai_diff.go
167
ai_diff.go
@@ -3,6 +3,8 @@ package main
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -50,6 +52,149 @@ func (c *CachedGitHubService) PullRequestDiff(ctx context.Context, owner, repo s
|
||||
return service.PullRequestDiff(ctx, owner, repo, number)
|
||||
}
|
||||
|
||||
func (c *GitHubClient) RepositoryTree(
|
||||
ctx context.Context, owner, repo, commitOID string,
|
||||
) (AIRepositoryTree, error) {
|
||||
baseURL := c.restBaseURL() + "/repos/" + url.PathEscape(owner) + "/" + url.PathEscape(repo)
|
||||
var commit struct {
|
||||
Tree struct {
|
||||
SHA string `json:"sha"`
|
||||
} `json:"tree"`
|
||||
}
|
||||
if err := c.getAIRepositoryJSON(
|
||||
ctx, baseURL+"/git/commits/"+url.PathEscape(commitOID), 1<<20, &commit,
|
||||
); err != nil {
|
||||
return AIRepositoryTree{}, fmt.Errorf("load repository commit: %w", err)
|
||||
}
|
||||
if commit.Tree.SHA == "" {
|
||||
return AIRepositoryTree{}, fmt.Errorf("load repository commit: GitHub returned no tree OID")
|
||||
}
|
||||
var tree struct {
|
||||
Truncated bool `json:"truncated"`
|
||||
Tree []struct {
|
||||
Path string `json:"path"`
|
||||
Mode string `json:"mode"`
|
||||
Type string `json:"type"`
|
||||
SHA string `json:"sha"`
|
||||
Size int64 `json:"size"`
|
||||
} `json:"tree"`
|
||||
}
|
||||
if err := c.getAIRepositoryJSON(
|
||||
ctx, baseURL+"/git/trees/"+url.PathEscape(commit.Tree.SHA)+"?recursive=1",
|
||||
8<<20, &tree,
|
||||
); err != nil {
|
||||
return AIRepositoryTree{}, fmt.Errorf("load repository tree: %w", err)
|
||||
}
|
||||
result := AIRepositoryTree{
|
||||
CommitOID: commitOID,
|
||||
Entries: make([]AIRepositoryEntry, 0, len(tree.Tree)),
|
||||
Truncated: tree.Truncated,
|
||||
}
|
||||
for _, entry := range tree.Tree {
|
||||
clean := filepath.ToSlash(filepath.Clean(entry.Path))
|
||||
if clean == "." || clean == "" || filepath.IsAbs(clean) ||
|
||||
strings.HasPrefix(clean, "../") {
|
||||
continue
|
||||
}
|
||||
result.Entries = append(result.Entries, AIRepositoryEntry{
|
||||
Path: clean, OID: entry.SHA, Type: entry.Type, Mode: entry.Mode, Size: entry.Size,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *CachedGitHubService) RepositoryTree(
|
||||
ctx context.Context, owner, repo, commitOID string,
|
||||
) (AIRepositoryTree, error) {
|
||||
service, ok := c.remote.(AIRepositoryService)
|
||||
if !ok {
|
||||
return AIRepositoryTree{}, fmt.Errorf("configured GitHub service cannot load repository trees")
|
||||
}
|
||||
return service.RepositoryTree(ctx, owner, repo, commitOID)
|
||||
}
|
||||
|
||||
func (c *GitHubClient) RepositoryBlob(
|
||||
ctx context.Context, owner, repo, blobOID string, maxBytes int,
|
||||
) ([]byte, error) {
|
||||
baseURL := c.restBaseURL() + "/repos/" + url.PathEscape(owner) + "/" + url.PathEscape(repo)
|
||||
var blob struct {
|
||||
Content string `json:"content"`
|
||||
Encoding string `json:"encoding"`
|
||||
Size int `json:"size"`
|
||||
SHA string `json:"sha"`
|
||||
}
|
||||
responseLimit := int64(max(16_384, maxBytes*2+8_192))
|
||||
if err := c.getAIRepositoryJSON(
|
||||
ctx, baseURL+"/git/blobs/"+url.PathEscape(blobOID), responseLimit, &blob,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("load repository blob: %w", err)
|
||||
}
|
||||
if blob.SHA != "" && blob.SHA != blobOID {
|
||||
return nil, fmt.Errorf("load repository blob: GitHub returned an unexpected blob OID")
|
||||
}
|
||||
if blob.Size > maxBytes {
|
||||
return nil, fmt.Errorf("repository file exceeds the %d-byte AI limit", maxBytes)
|
||||
}
|
||||
if blob.Encoding != "base64" {
|
||||
return nil, fmt.Errorf("load repository blob: unsupported encoding %q", blob.Encoding)
|
||||
}
|
||||
content, err := base64.StdEncoding.DecodeString(strings.Map(func(r rune) rune {
|
||||
if r == '\r' || r == '\n' || r == ' ' || r == '\t' {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, blob.Content))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode repository blob: %w", err)
|
||||
}
|
||||
if len(content) > maxBytes {
|
||||
return nil, fmt.Errorf("repository file exceeds the %d-byte AI limit", maxBytes)
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func (c *CachedGitHubService) RepositoryBlob(
|
||||
ctx context.Context, owner, repo, blobOID string, maxBytes int,
|
||||
) ([]byte, error) {
|
||||
service, ok := c.remote.(AIRepositoryService)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("configured GitHub service cannot load repository blobs")
|
||||
}
|
||||
return service.RepositoryBlob(ctx, owner, repo, blobOID, maxBytes)
|
||||
}
|
||||
|
||||
func (c *GitHubClient) getAIRepositoryJSON(
|
||||
ctx context.Context, requestURL string, limit int64, output any,
|
||||
) error {
|
||||
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+json")
|
||||
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)))
|
||||
}
|
||||
data, err := io.ReadAll(io.LimitReader(response.Body, limit+1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if int64(len(data)) > limit {
|
||||
return fmt.Errorf("GitHub response exceeds the %d-byte safety limit", limit)
|
||||
}
|
||||
if err := json.Unmarshal(data, output); err != nil {
|
||||
return fmt.Errorf("decode GitHub response: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareAIDiff(raw string, config AIConfig) ([]aiDiffFile, []string, int) {
|
||||
sections := strings.Split(raw, "\ndiff --git ")
|
||||
var files []aiDiffFile
|
||||
@@ -159,21 +304,31 @@ func aiExcludedReason(file aiDiffFile, config AIConfig) string {
|
||||
strings.Contains(file.Text, "Binary files ") || strings.IndexByte(file.Text, 0) >= 0 {
|
||||
return "binary"
|
||||
}
|
||||
lower := strings.ToLower(file.Path)
|
||||
for _, pattern := range config.Exclude {
|
||||
if aiPathMatches(file.Path, config.SensitivePaths) {
|
||||
return "sensitive"
|
||||
}
|
||||
if aiPathMatches(file.Path, config.Exclude) {
|
||||
return "excluded"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func aiPathMatches(filePath string, patterns []string) bool {
|
||||
lower := strings.ToLower(filepath.ToSlash(filePath))
|
||||
for _, pattern := range patterns {
|
||||
pattern = filepath.ToSlash(pattern)
|
||||
if strings.HasSuffix(pattern, "/") {
|
||||
directory := strings.ToLower(pattern)
|
||||
if strings.HasPrefix(lower, directory) || strings.Contains(lower, "/"+directory) {
|
||||
return "excluded"
|
||||
return true
|
||||
}
|
||||
}
|
||||
if matched, _ := path.Match(strings.ToLower(pattern), path.Base(lower)); matched {
|
||||
return "excluded"
|
||||
return true
|
||||
}
|
||||
if matched, _ := path.Match(strings.ToLower(pattern), lower); matched {
|
||||
return "excluded"
|
||||
return true
|
||||
}
|
||||
}
|
||||
return ""
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user