Some checks failed
Build and Release / Build Binaries (amd64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, linux) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, windows) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, linux) (push) Blocked by required conditions
Build and Release / Build Docker Image (push) Blocked by required conditions
Build and Release / Create Release (push) Blocked by required conditions
Build and Release / Lint and Test (push) Has been cancelled
- Replace encoding/json with modules/json (depguard)
- Add error handling for json.Unmarshal and WriteItem calls (errcheck)
- Use slices.Contains instead of manual loops (modernize)
- Use any instead of interface{} (modernize)
- Use min/max built-in functions (modernize)
- Use strings.FieldsSeq and strings.SplitSeq (modernize)
- Use range over int for loops (modernize)
- Use http.MethodOptions constant (usestdlibvars)
- Use tagged switch statements (staticcheck)
- Use += and /= operators (gocritic)
- Fix gofumpt formatting issues
- Remove unused streamLargeFile function
- Remove unused primaryLang parameter
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
777 lines
22 KiB
Go
777 lines
22 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package v2
|
|
|
|
import (
|
|
"net/http"
|
|
"path"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
issues_model "code.gitea.io/gitea/models/issues"
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
"code.gitea.io/gitea/models/unit"
|
|
apierrors "code.gitea.io/gitea/modules/errors"
|
|
"code.gitea.io/gitea/modules/git"
|
|
"code.gitea.io/gitea/modules/gitrepo"
|
|
"code.gitea.io/gitea/modules/json"
|
|
"code.gitea.io/gitea/services/context"
|
|
)
|
|
|
|
// AIRepoSummaryRequest represents a request for AI-friendly repo summary
|
|
type AIRepoSummaryRequest struct {
|
|
Owner string `json:"owner" binding:"Required"`
|
|
Repo string `json:"repo" binding:"Required"`
|
|
Ref string `json:"ref"`
|
|
}
|
|
|
|
// AIRepoSummaryResponse contains comprehensive repo information for AI tools
|
|
type AIRepoSummaryResponse struct {
|
|
// Basic info
|
|
Owner string `json:"owner"`
|
|
Repo string `json:"repo"`
|
|
FullName string `json:"full_name"`
|
|
Description string `json:"description"`
|
|
Website string `json:"website,omitempty"`
|
|
Language string `json:"primary_language"`
|
|
|
|
// Repository stats
|
|
Stars int `json:"stars"`
|
|
Forks int `json:"forks"`
|
|
Watchers int `json:"watchers"`
|
|
OpenIssues int `json:"open_issues"`
|
|
OpenPRs int `json:"open_pull_requests"`
|
|
Size int64 `json:"size_kb"`
|
|
IsPrivate bool `json:"is_private"`
|
|
IsFork bool `json:"is_fork"`
|
|
IsArchived bool `json:"is_archived"`
|
|
IsTemplate bool `json:"is_template"`
|
|
HasWiki bool `json:"has_wiki"`
|
|
HasIssues bool `json:"has_issues"`
|
|
HasProjects bool `json:"has_projects"`
|
|
|
|
// Git info
|
|
DefaultBranch string `json:"default_branch"`
|
|
Branches []string `json:"branches"`
|
|
Tags []string `json:"recent_tags"`
|
|
LastCommit struct {
|
|
SHA string `json:"sha"`
|
|
Message string `json:"message"`
|
|
Author string `json:"author"`
|
|
Timestamp string `json:"timestamp"`
|
|
} `json:"last_commit"`
|
|
|
|
// Structure overview
|
|
Structure struct {
|
|
TopLevelDirs []string `json:"top_level_dirs"`
|
|
TopLevelFiles []string `json:"top_level_files"`
|
|
FileCount int `json:"total_file_count"`
|
|
Languages map[string]int64 `json:"languages"` // language -> bytes
|
|
HasReadme bool `json:"has_readme"`
|
|
ReadmePath string `json:"readme_path,omitempty"`
|
|
HasLicense bool `json:"has_license"`
|
|
LicensePath string `json:"license_path,omitempty"`
|
|
HasContrib bool `json:"has_contributing"`
|
|
ContribPath string `json:"contributing_path,omitempty"`
|
|
ConfigFiles []string `json:"config_files"` // package.json, go.mod, etc.
|
|
} `json:"structure"`
|
|
|
|
// Recent activity
|
|
RecentActivity struct {
|
|
CommitsLastWeek int `json:"commits_last_week"`
|
|
CommitsLastMonth int `json:"commits_last_month"`
|
|
Contributors int `json:"contributors"`
|
|
} `json:"recent_activity"`
|
|
|
|
// AI-specific hints
|
|
AIHints struct {
|
|
SuggestedEntryPoints []string `json:"suggested_entry_points"`
|
|
ProjectType string `json:"project_type"` // "library", "application", "monorepo", etc.
|
|
BuildSystem string `json:"build_system,omitempty"`
|
|
TestFramework string `json:"test_framework,omitempty"`
|
|
} `json:"ai_hints"`
|
|
}
|
|
|
|
// GetAIRepoSummary returns a comprehensive AI-friendly summary of a repository
|
|
func GetAIRepoSummary(ctx *context.APIContext) {
|
|
var req AIRepoSummaryRequest
|
|
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
|
|
ctx.APIErrorWithCode(apierrors.ValidationFailed, map[string]any{
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Get repository
|
|
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, req.Owner, req.Repo)
|
|
if err != nil {
|
|
if repo_model.IsErrRepoNotExist(err) {
|
|
ctx.APIErrorWithCode(apierrors.RepoNotFound)
|
|
return
|
|
}
|
|
ctx.APIErrorWithCode(apierrors.InternalError)
|
|
return
|
|
}
|
|
|
|
// Check access
|
|
if repo.IsPrivate && !ctx.IsSigned {
|
|
ctx.APIErrorWithCode(apierrors.PermAccessDenied)
|
|
return
|
|
}
|
|
|
|
// Load owner
|
|
if err := repo.LoadOwner(ctx); err != nil {
|
|
ctx.APIErrorWithCode(apierrors.InternalError)
|
|
return
|
|
}
|
|
|
|
// Open git repo
|
|
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
|
|
if err != nil {
|
|
ctx.APIErrorWithCode(apierrors.InternalError)
|
|
return
|
|
}
|
|
defer gitRepo.Close()
|
|
|
|
response := AIRepoSummaryResponse{
|
|
Owner: repo.Owner.Name,
|
|
Repo: repo.Name,
|
|
FullName: repo.FullName(),
|
|
Description: repo.Description,
|
|
Website: repo.Website,
|
|
Language: repo.PrimaryLanguage.Language,
|
|
Stars: repo.NumStars,
|
|
Forks: repo.NumForks,
|
|
Watchers: repo.NumWatches,
|
|
OpenIssues: repo.NumOpenIssues,
|
|
OpenPRs: repo.NumOpenPulls,
|
|
Size: repo.Size,
|
|
IsPrivate: repo.IsPrivate,
|
|
IsFork: repo.IsFork,
|
|
IsArchived: repo.IsArchived,
|
|
IsTemplate: repo.IsTemplate,
|
|
HasWiki: repo.UnitEnabled(ctx, unit.TypeWiki),
|
|
HasIssues: repo.UnitEnabled(ctx, unit.TypeIssues),
|
|
HasProjects: repo.UnitEnabled(ctx, unit.TypeProjects),
|
|
DefaultBranch: repo.DefaultBranch,
|
|
}
|
|
|
|
// Get branches (limit to recent 20)
|
|
branchNames, _, err := gitRepo.GetBranchNames(0, 20)
|
|
if err == nil {
|
|
response.Branches = branchNames
|
|
}
|
|
|
|
// Get recent tags
|
|
tagInfos, _, err := gitRepo.GetTagInfos(1, 10)
|
|
if err == nil {
|
|
tagNames := make([]string, 0, len(tagInfos))
|
|
for _, t := range tagInfos {
|
|
tagNames = append(tagNames, t.Name)
|
|
}
|
|
response.Tags = tagNames
|
|
}
|
|
|
|
// Get last commit
|
|
ref := req.Ref
|
|
if ref == "" {
|
|
ref = repo.DefaultBranch
|
|
}
|
|
if commit, err := gitRepo.GetBranchCommit(ref); err == nil {
|
|
response.LastCommit.SHA = commit.ID.String()
|
|
response.LastCommit.Message = strings.Split(commit.CommitMessage, "\n")[0]
|
|
response.LastCommit.Author = commit.Author.Name
|
|
response.LastCommit.Timestamp = commit.Author.When.Format("2006-01-02T15:04:05Z07:00")
|
|
}
|
|
|
|
// Analyze structure
|
|
if commit, err := gitRepo.GetBranchCommit(ref); err == nil {
|
|
if tree, err := commit.SubTree(""); err == nil {
|
|
entries, _ := tree.ListEntries()
|
|
|
|
for _, entry := range entries {
|
|
name := entry.Name()
|
|
if entry.IsDir() {
|
|
response.Structure.TopLevelDirs = append(response.Structure.TopLevelDirs, name)
|
|
} else {
|
|
response.Structure.TopLevelFiles = append(response.Structure.TopLevelFiles, name)
|
|
|
|
// Check for special files
|
|
lowerName := strings.ToLower(name)
|
|
if strings.HasPrefix(lowerName, "readme") {
|
|
response.Structure.HasReadme = true
|
|
response.Structure.ReadmePath = name
|
|
}
|
|
if strings.HasPrefix(lowerName, "license") || lowerName == "copying" {
|
|
response.Structure.HasLicense = true
|
|
response.Structure.LicensePath = name
|
|
}
|
|
if strings.HasPrefix(lowerName, "contributing") {
|
|
response.Structure.HasContrib = true
|
|
response.Structure.ContribPath = name
|
|
}
|
|
|
|
// Check for config files
|
|
if isConfigFile(name) {
|
|
response.Structure.ConfigFiles = append(response.Structure.ConfigFiles, name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Count total files (approximate via tree walk, limit depth)
|
|
response.Structure.FileCount = countFiles(commit, "", 0, 5)
|
|
}
|
|
|
|
// Get language stats from repo
|
|
response.Structure.Languages = make(map[string]int64)
|
|
if langs, err := repo_model.GetLanguageStats(ctx, repo); err == nil {
|
|
for _, lang := range langs {
|
|
response.Structure.Languages[lang.Language] = int64(lang.Percentage)
|
|
}
|
|
}
|
|
|
|
// AI hints
|
|
response.AIHints = detectProjectType(response.Structure.TopLevelFiles, response.Structure.ConfigFiles, response.Language)
|
|
|
|
ctx.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// AINavigationRequest represents a request to understand repo navigation
|
|
type AINavigationRequest struct {
|
|
Owner string `json:"owner" binding:"Required"`
|
|
Repo string `json:"repo" binding:"Required"`
|
|
Ref string `json:"ref"`
|
|
Query string `json:"query"` // What the AI is looking for
|
|
}
|
|
|
|
// AINavigationResponse provides navigation hints for AI
|
|
type AINavigationResponse struct {
|
|
// Directory tree (limited depth)
|
|
Tree []TreeNode `json:"tree"`
|
|
|
|
// Important paths
|
|
ImportantPaths struct {
|
|
Entrypoints []PathInfo `json:"entrypoints"`
|
|
Config []PathInfo `json:"config"`
|
|
Tests []PathInfo `json:"tests"`
|
|
Docs []PathInfo `json:"docs"`
|
|
} `json:"important_paths"`
|
|
|
|
// File type summary
|
|
FileTypes map[string]int `json:"file_types"` // extension -> count
|
|
}
|
|
|
|
// TreeNode represents a node in the directory tree
|
|
type TreeNode struct {
|
|
Path string `json:"path"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"` // "file" or "dir"
|
|
Size int64 `json:"size,omitempty"`
|
|
Children []TreeNode `json:"children,omitempty"`
|
|
}
|
|
|
|
// PathInfo provides information about an important path
|
|
type PathInfo struct {
|
|
Path string `json:"path"`
|
|
Description string `json:"description"`
|
|
Priority int `json:"priority"` // 1-10, higher is more important
|
|
}
|
|
|
|
// GetAINavigation returns navigation hints for AI tools
|
|
func GetAINavigation(ctx *context.APIContext) {
|
|
var req AINavigationRequest
|
|
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
|
|
ctx.APIErrorWithCode(apierrors.ValidationFailed, map[string]any{
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Get repository
|
|
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, req.Owner, req.Repo)
|
|
if err != nil {
|
|
if repo_model.IsErrRepoNotExist(err) {
|
|
ctx.APIErrorWithCode(apierrors.RepoNotFound)
|
|
return
|
|
}
|
|
ctx.APIErrorWithCode(apierrors.InternalError)
|
|
return
|
|
}
|
|
|
|
// Check access
|
|
if repo.IsPrivate && !ctx.IsSigned {
|
|
ctx.APIErrorWithCode(apierrors.PermAccessDenied)
|
|
return
|
|
}
|
|
|
|
// Open git repo
|
|
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
|
|
if err != nil {
|
|
ctx.APIErrorWithCode(apierrors.InternalError)
|
|
return
|
|
}
|
|
defer gitRepo.Close()
|
|
|
|
ref := req.Ref
|
|
if ref == "" {
|
|
ref = repo.DefaultBranch
|
|
}
|
|
|
|
response := AINavigationResponse{
|
|
FileTypes: make(map[string]int),
|
|
}
|
|
|
|
commit, err := gitRepo.GetBranchCommit(ref)
|
|
if err != nil {
|
|
ctx.APIErrorWithCode(apierrors.RefNotFound)
|
|
return
|
|
}
|
|
|
|
// Build tree (max depth 3)
|
|
response.Tree = buildTree(commit, "", 0, 3)
|
|
|
|
// Collect file types and identify important paths
|
|
collectFileInfo(commit, "", &response)
|
|
|
|
ctx.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// AIIssueContextRequest represents a request for issue context
|
|
type AIIssueContextRequest struct {
|
|
Owner string `json:"owner" binding:"Required"`
|
|
Repo string `json:"repo" binding:"Required"`
|
|
IssueNumber int64 `json:"issue_number" binding:"Required"`
|
|
}
|
|
|
|
// AIIssueContextResponse provides rich context about an issue
|
|
type AIIssueContextResponse struct {
|
|
// Issue details
|
|
Number int64 `json:"number"`
|
|
Title string `json:"title"`
|
|
Body string `json:"body"`
|
|
State string `json:"state"`
|
|
Labels []string `json:"labels"`
|
|
Author string `json:"author"`
|
|
Assignees []string `json:"assignees"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
|
|
// Comments
|
|
Comments []struct {
|
|
Author string `json:"author"`
|
|
Body string `json:"body"`
|
|
CreatedAt string `json:"created_at"`
|
|
} `json:"comments"`
|
|
|
|
// Related info
|
|
RelatedIssues []struct {
|
|
Number int64 `json:"number"`
|
|
Title string `json:"title"`
|
|
State string `json:"state"`
|
|
} `json:"related_issues,omitempty"`
|
|
|
|
// Code references (files mentioned in issue/comments)
|
|
CodeReferences []string `json:"code_references,omitempty"`
|
|
|
|
// AI hints
|
|
AIHints struct {
|
|
Category string `json:"category"` // "bug", "feature", "question", etc.
|
|
Complexity string `json:"complexity"` // "simple", "moderate", "complex"
|
|
SuggestedFiles []string `json:"suggested_files,omitempty"`
|
|
} `json:"ai_hints"`
|
|
}
|
|
|
|
// GetAIIssueContext returns rich context about an issue for AI tools
|
|
func GetAIIssueContext(ctx *context.APIContext) {
|
|
var req AIIssueContextRequest
|
|
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
|
|
ctx.APIErrorWithCode(apierrors.ValidationFailed, map[string]any{
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Get repository
|
|
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, req.Owner, req.Repo)
|
|
if err != nil {
|
|
if repo_model.IsErrRepoNotExist(err) {
|
|
ctx.APIErrorWithCode(apierrors.RepoNotFound)
|
|
return
|
|
}
|
|
ctx.APIErrorWithCode(apierrors.InternalError)
|
|
return
|
|
}
|
|
|
|
// Check access
|
|
if repo.IsPrivate && !ctx.IsSigned {
|
|
ctx.APIErrorWithCode(apierrors.PermAccessDenied)
|
|
return
|
|
}
|
|
|
|
// Get issue
|
|
issue, err := issues_model.GetIssueByIndex(ctx, repo.ID, req.IssueNumber)
|
|
if err != nil {
|
|
if issues_model.IsErrIssueNotExist(err) {
|
|
ctx.APIErrorWithCode(apierrors.IssueNotFound, map[string]any{
|
|
"issue_number": req.IssueNumber,
|
|
})
|
|
return
|
|
}
|
|
ctx.APIErrorWithCode(apierrors.InternalError)
|
|
return
|
|
}
|
|
|
|
// Load related data
|
|
if err := issue.LoadPoster(ctx); err != nil {
|
|
ctx.APIErrorWithCode(apierrors.InternalError)
|
|
return
|
|
}
|
|
if err := issue.LoadLabels(ctx); err != nil {
|
|
ctx.APIErrorWithCode(apierrors.InternalError)
|
|
return
|
|
}
|
|
if err := issue.LoadAssignees(ctx); err != nil {
|
|
ctx.APIErrorWithCode(apierrors.InternalError)
|
|
return
|
|
}
|
|
|
|
response := AIIssueContextResponse{
|
|
Number: issue.Index,
|
|
Title: issue.Title,
|
|
Body: issue.Content,
|
|
State: map[bool]string{true: "closed", false: "open"}[issue.IsClosed],
|
|
Author: issue.Poster.Name,
|
|
CreatedAt: issue.CreatedUnix.AsTime().Format("2006-01-02T15:04:05Z07:00"),
|
|
UpdatedAt: issue.UpdatedUnix.AsTime().Format("2006-01-02T15:04:05Z07:00"),
|
|
}
|
|
|
|
// Labels
|
|
for _, label := range issue.Labels {
|
|
response.Labels = append(response.Labels, label.Name)
|
|
}
|
|
|
|
// Assignees
|
|
for _, assignee := range issue.Assignees {
|
|
response.Assignees = append(response.Assignees, assignee.Name)
|
|
}
|
|
|
|
// Get comments
|
|
comments, err := issues_model.FindComments(ctx, &issues_model.FindCommentsOptions{
|
|
IssueID: issue.ID,
|
|
Type: issues_model.CommentTypeComment,
|
|
})
|
|
if err == nil {
|
|
for _, comment := range comments {
|
|
if err := comment.LoadPoster(ctx); err != nil {
|
|
continue
|
|
}
|
|
response.Comments = append(response.Comments, struct {
|
|
Author string `json:"author"`
|
|
Body string `json:"body"`
|
|
CreatedAt string `json:"created_at"`
|
|
}{
|
|
Author: comment.Poster.Name,
|
|
Body: comment.Content,
|
|
CreatedAt: comment.CreatedUnix.AsTime().Format("2006-01-02T15:04:05Z07:00"),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Extract code references from issue body and comments
|
|
codeRefs := extractCodeReferences(issue.Content)
|
|
for _, comment := range response.Comments {
|
|
codeRefs = append(codeRefs, extractCodeReferences(comment.Body)...)
|
|
}
|
|
response.CodeReferences = uniqueStrings(codeRefs)
|
|
|
|
// AI hints based on labels and content
|
|
response.AIHints.Category = categorizeIssue(issue.Labels, issue.Title, issue.Content)
|
|
response.AIHints.Complexity = estimateComplexity(issue.Content, len(response.Comments))
|
|
|
|
ctx.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func isConfigFile(name string) bool {
|
|
configFiles := []string{
|
|
"package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
|
|
"go.mod", "go.sum",
|
|
"Cargo.toml", "Cargo.lock",
|
|
"requirements.txt", "setup.py", "pyproject.toml", "Pipfile",
|
|
"Gemfile", "Gemfile.lock",
|
|
"composer.json", "composer.lock",
|
|
"pom.xml", "build.gradle", "build.gradle.kts",
|
|
"CMakeLists.txt", "Makefile", "makefile",
|
|
"Dockerfile", "docker-compose.yml", "docker-compose.yaml",
|
|
".gitignore", ".gitattributes",
|
|
"tsconfig.json", "jsconfig.json",
|
|
".eslintrc", ".eslintrc.js", ".eslintrc.json",
|
|
".prettierrc", ".prettierrc.js", ".prettierrc.json",
|
|
"webpack.config.js", "vite.config.js", "rollup.config.js",
|
|
".env.example", ".env.sample",
|
|
}
|
|
return slices.Contains(configFiles, name)
|
|
}
|
|
|
|
func countFiles(commit *git.Commit, dir string, depth, maxDepth int) int {
|
|
if depth > maxDepth {
|
|
return 0
|
|
}
|
|
tree, err := commit.SubTree(dir)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
entries, _ := tree.ListEntries()
|
|
count := 0
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
subPath := entry.Name()
|
|
if dir != "" {
|
|
subPath = dir + "/" + entry.Name()
|
|
}
|
|
count += countFiles(commit, subPath, depth+1, maxDepth)
|
|
} else {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func detectProjectType(files, configFiles []string, _ string) struct {
|
|
SuggestedEntryPoints []string `json:"suggested_entry_points"`
|
|
ProjectType string `json:"project_type"`
|
|
BuildSystem string `json:"build_system,omitempty"`
|
|
TestFramework string `json:"test_framework,omitempty"`
|
|
} {
|
|
hints := struct {
|
|
SuggestedEntryPoints []string `json:"suggested_entry_points"`
|
|
ProjectType string `json:"project_type"`
|
|
BuildSystem string `json:"build_system,omitempty"`
|
|
TestFramework string `json:"test_framework,omitempty"`
|
|
}{
|
|
ProjectType: "unknown",
|
|
}
|
|
|
|
for _, cf := range configFiles {
|
|
switch cf {
|
|
case "package.json":
|
|
hints.BuildSystem = "npm/yarn"
|
|
hints.SuggestedEntryPoints = append(hints.SuggestedEntryPoints, "package.json", "src/index.js", "src/index.ts")
|
|
case "go.mod":
|
|
hints.BuildSystem = "go modules"
|
|
hints.SuggestedEntryPoints = append(hints.SuggestedEntryPoints, "go.mod", "main.go", "cmd/")
|
|
case "Cargo.toml":
|
|
hints.BuildSystem = "cargo"
|
|
hints.SuggestedEntryPoints = append(hints.SuggestedEntryPoints, "Cargo.toml", "src/main.rs", "src/lib.rs")
|
|
case "requirements.txt", "pyproject.toml":
|
|
hints.BuildSystem = "pip/poetry"
|
|
hints.SuggestedEntryPoints = append(hints.SuggestedEntryPoints, "setup.py", "main.py", "app.py")
|
|
case "Makefile", "makefile":
|
|
hints.BuildSystem = "make"
|
|
}
|
|
}
|
|
|
|
// Detect project type
|
|
for _, f := range files {
|
|
if f == "main.go" || f == "main.rs" || f == "main.py" {
|
|
hints.ProjectType = "application"
|
|
break
|
|
}
|
|
}
|
|
|
|
if hints.ProjectType == "unknown" {
|
|
for _, cf := range configFiles {
|
|
if cf == "setup.py" || cf == "Cargo.toml" {
|
|
hints.ProjectType = "library"
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return hints
|
|
}
|
|
|
|
func buildTree(commit *git.Commit, dir string, depth, maxDepth int) []TreeNode {
|
|
if depth >= maxDepth {
|
|
return nil
|
|
}
|
|
|
|
tree, err := commit.SubTree(dir)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
entries, _ := tree.ListEntries()
|
|
nodes := make([]TreeNode, 0, len(entries))
|
|
|
|
for _, entry := range entries {
|
|
node := TreeNode{
|
|
Name: entry.Name(),
|
|
Path: path.Join(dir, entry.Name()),
|
|
}
|
|
|
|
if entry.IsDir() {
|
|
node.Type = "dir"
|
|
node.Children = buildTree(commit, node.Path, depth+1, maxDepth)
|
|
} else {
|
|
node.Type = "file"
|
|
node.Size = entry.Size()
|
|
}
|
|
|
|
nodes = append(nodes, node)
|
|
}
|
|
|
|
// Sort: directories first, then alphabetically
|
|
sort.Slice(nodes, func(i, j int) bool {
|
|
if nodes[i].Type != nodes[j].Type {
|
|
return nodes[i].Type == "dir"
|
|
}
|
|
return nodes[i].Name < nodes[j].Name
|
|
})
|
|
|
|
return nodes
|
|
}
|
|
|
|
func collectFileInfo(commit *git.Commit, dir string, response *AINavigationResponse) {
|
|
tree, err := commit.SubTree(dir)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
entries, _ := tree.ListEntries()
|
|
for _, entry := range entries {
|
|
fullPath := path.Join(dir, entry.Name())
|
|
|
|
if entry.IsDir() {
|
|
// Check for important directories
|
|
name := strings.ToLower(entry.Name())
|
|
if name == "test" || name == "tests" || name == "__tests__" || name == "spec" {
|
|
response.ImportantPaths.Tests = append(response.ImportantPaths.Tests, PathInfo{
|
|
Path: fullPath,
|
|
Description: "Test directory",
|
|
Priority: 7,
|
|
})
|
|
}
|
|
if name == "docs" || name == "documentation" {
|
|
response.ImportantPaths.Docs = append(response.ImportantPaths.Docs, PathInfo{
|
|
Path: fullPath,
|
|
Description: "Documentation directory",
|
|
Priority: 6,
|
|
})
|
|
}
|
|
if name == "src" || name == "lib" || name == "pkg" {
|
|
response.ImportantPaths.Entrypoints = append(response.ImportantPaths.Entrypoints, PathInfo{
|
|
Path: fullPath,
|
|
Description: "Source directory",
|
|
Priority: 8,
|
|
})
|
|
}
|
|
} else {
|
|
// Count file extensions
|
|
ext := strings.ToLower(path.Ext(entry.Name()))
|
|
if ext != "" {
|
|
response.FileTypes[ext]++
|
|
}
|
|
|
|
// Check for config files
|
|
if isConfigFile(entry.Name()) {
|
|
response.ImportantPaths.Config = append(response.ImportantPaths.Config, PathInfo{
|
|
Path: fullPath,
|
|
Description: "Configuration file",
|
|
Priority: 5,
|
|
})
|
|
}
|
|
|
|
// Check for entry points
|
|
name := strings.ToLower(entry.Name())
|
|
if name == "main.go" || name == "main.rs" || name == "main.py" || name == "index.js" || name == "index.ts" {
|
|
response.ImportantPaths.Entrypoints = append(response.ImportantPaths.Entrypoints, PathInfo{
|
|
Path: fullPath,
|
|
Description: "Application entry point",
|
|
Priority: 10,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func extractCodeReferences(text string) []string {
|
|
// Simple extraction of file paths mentioned in text
|
|
// Look for patterns like `path/to/file.ext` or file.ext
|
|
refs := []string{}
|
|
for word := range strings.FieldsSeq(text) {
|
|
// Clean up markdown code blocks
|
|
word = strings.Trim(word, "`*_[]()\"'")
|
|
if strings.Contains(word, ".") && (strings.Contains(word, "/") || strings.Contains(word, "\\")) {
|
|
// Looks like a file path
|
|
if len(word) > 3 && len(word) < 200 {
|
|
refs = append(refs, word)
|
|
}
|
|
}
|
|
}
|
|
return refs
|
|
}
|
|
|
|
func uniqueStrings(input []string) []string {
|
|
seen := make(map[string]bool)
|
|
result := []string{}
|
|
for _, s := range input {
|
|
if !seen[s] {
|
|
seen[s] = true
|
|
result = append(result, s)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func categorizeIssue(labels []*issues_model.Label, title, body string) string {
|
|
// Check labels first
|
|
for _, label := range labels {
|
|
name := strings.ToLower(label.Name)
|
|
if strings.Contains(name, "bug") {
|
|
return "bug"
|
|
}
|
|
if strings.Contains(name, "feature") || strings.Contains(name, "enhancement") {
|
|
return "feature"
|
|
}
|
|
if strings.Contains(name, "question") || strings.Contains(name, "help") {
|
|
return "question"
|
|
}
|
|
if strings.Contains(name, "documentation") || strings.Contains(name, "docs") {
|
|
return "documentation"
|
|
}
|
|
}
|
|
|
|
// Check title/body keywords
|
|
combined := strings.ToLower(title + " " + body)
|
|
if strings.Contains(combined, "error") || strings.Contains(combined, "crash") || strings.Contains(combined, "bug") {
|
|
return "bug"
|
|
}
|
|
if strings.Contains(combined, "feature") || strings.Contains(combined, "add support") || strings.Contains(combined, "would be nice") {
|
|
return "feature"
|
|
}
|
|
if strings.Contains(combined, "how to") || strings.Contains(combined, "how do i") || strings.Contains(combined, "?") {
|
|
return "question"
|
|
}
|
|
|
|
return "general"
|
|
}
|
|
|
|
func estimateComplexity(body string, commentCount int) string {
|
|
// Simple heuristics
|
|
lines := len(strings.Split(body, "\n"))
|
|
words := len(strings.Fields(body))
|
|
|
|
if lines < 10 && words < 100 && commentCount < 3 {
|
|
return "simple"
|
|
}
|
|
if lines > 50 || words > 500 || commentCount > 10 {
|
|
return "complex"
|
|
}
|
|
return "moderate"
|
|
}
|