summaryrefslogtreecommitdiff
path: root/internal/showcase/metadata.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/showcase/metadata.go')
-rw-r--r--internal/showcase/metadata.go365
1 files changed, 365 insertions, 0 deletions
diff --git a/internal/showcase/metadata.go b/internal/showcase/metadata.go
new file mode 100644
index 0000000..209c7b6
--- /dev/null
+++ b/internal/showcase/metadata.go
@@ -0,0 +1,365 @@
+package showcase
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// RepoMetadata holds metadata about a repository
+type RepoMetadata struct {
+ Languages []string
+ CommitCount int
+ LinesOfCode int
+ FirstCommitDate string
+ LastCommitDate string
+ License string
+ AvgCommitAge float64 // Average age of last 100 commits in days
+}
+
+// extractRepoMetadata extracts metadata from a repository
+func extractRepoMetadata(repoPath string) (*RepoMetadata, error) {
+ metadata := &RepoMetadata{}
+
+ // Get programming languages by analyzing file extensions
+ languages, err := detectLanguages(repoPath)
+ if err != nil {
+ fmt.Printf("Warning: Failed to detect languages: %v\n", err)
+ }
+ metadata.Languages = languages
+
+ // Get commit count
+ commitCount, err := getCommitCount(repoPath)
+ if err != nil {
+ fmt.Printf("Warning: Failed to get commit count: %v\n", err)
+ }
+ metadata.CommitCount = commitCount
+
+ // Get lines of code
+ loc, err := countLinesOfCode(repoPath)
+ if err != nil {
+ fmt.Printf("Warning: Failed to count lines of code: %v\n", err)
+ }
+ metadata.LinesOfCode = loc
+
+ // Get first and last commit dates
+ firstDate, err := getFirstCommitDate(repoPath)
+ if err != nil {
+ fmt.Printf("Warning: Failed to get first commit date: %v\n", err)
+ }
+ metadata.FirstCommitDate = firstDate
+
+ lastDate, err := getLastCommitDate(repoPath)
+ if err != nil {
+ fmt.Printf("Warning: Failed to get last commit date: %v\n", err)
+ }
+ metadata.LastCommitDate = lastDate
+
+ // Check for license file
+ license := detectLicense(repoPath)
+ metadata.License = license
+
+ // Get average age of last 100 commits
+ avgAge, err := getAverageCommitAge(repoPath, 100)
+ if err != nil {
+ fmt.Printf("Warning: Failed to get average commit age: %v\n", err)
+ }
+ metadata.AvgCommitAge = avgAge
+
+ return metadata, nil
+}
+
+// detectLanguages detects programming languages used in the repository
+func detectLanguages(repoPath string) ([]string, error) {
+ languageMap := make(map[string]bool)
+
+ // Define common language extensions
+ langExtensions := map[string]string{
+ ".go": "Go",
+ ".py": "Python",
+ ".js": "JavaScript",
+ ".ts": "TypeScript",
+ ".java": "Java",
+ ".c": "C",
+ ".cpp": "C++",
+ ".cc": "C++",
+ ".h": "C/C++",
+ ".hpp": "C++",
+ ".cs": "C#",
+ ".rb": "Ruby",
+ ".php": "PHP",
+ ".swift": "Swift",
+ ".kt": "Kotlin",
+ ".rs": "Rust",
+ ".scala": "Scala",
+ ".r": "R",
+ ".m": "Objective-C",
+ ".mm": "Objective-C++",
+ ".sh": "Shell",
+ ".bash": "Bash",
+ ".zsh": "Zsh",
+ ".pl": "Perl",
+ ".lua": "Lua",
+ ".vim": "Vim Script",
+ ".el": "Emacs Lisp",
+ ".clj": "Clojure",
+ ".hs": "Haskell",
+ ".ml": "OCaml",
+ ".ex": "Elixir",
+ ".exs": "Elixir",
+ ".dart": "Dart",
+ ".jl": "Julia",
+ ".nim": "Nim",
+ ".v": "V",
+ ".zig": "Zig",
+ }
+
+ // Walk through the repository
+ err := filepath.Walk(repoPath, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return nil // Skip errors
+ }
+
+ // Skip hidden directories and common non-code directories
+ if info.IsDir() {
+ name := info.Name()
+ if strings.HasPrefix(name, ".") || name == "node_modules" || name == "vendor" || name == "target" || name == "dist" || name == "build" {
+ return filepath.SkipDir
+ }
+ return nil
+ }
+
+ // Check file extension
+ ext := strings.ToLower(filepath.Ext(path))
+ if lang, ok := langExtensions[ext]; ok {
+ languageMap[lang] = true
+ }
+
+ // Check for special files
+ basename := filepath.Base(path)
+ switch strings.ToLower(basename) {
+ case "makefile", "gnumakefile":
+ languageMap["Make"] = true
+ case "dockerfile":
+ languageMap["Docker"] = true
+ case "cmakelists.txt":
+ languageMap["CMake"] = true
+ case "rakefile":
+ languageMap["Ruby"] = true
+ case "gemfile":
+ languageMap["Ruby"] = true
+ case "package.json":
+ languageMap["JavaScript/Node.js"] = true
+ case "cargo.toml":
+ languageMap["Rust"] = true
+ case "go.mod":
+ languageMap["Go"] = true
+ case "pom.xml":
+ languageMap["Java/Maven"] = true
+ case "build.gradle", "build.gradle.kts":
+ languageMap["Java/Gradle"] = true
+ case "requirements.txt", "setup.py", "pyproject.toml":
+ languageMap["Python"] = true
+ case "composer.json":
+ languageMap["PHP"] = true
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return nil, err
+ }
+
+ // Convert map to slice
+ var languages []string
+ for lang := range languageMap {
+ languages = append(languages, lang)
+ }
+
+ return languages, nil
+}
+
+// getCommitCount returns the total number of commits
+func getCommitCount(repoPath string) (int, error) {
+ cmd := exec.Command("git", "-C", repoPath, "rev-list", "--all", "--count")
+ output, err := cmd.Output()
+ if err != nil {
+ return 0, err
+ }
+
+ count, err := strconv.Atoi(strings.TrimSpace(string(output)))
+ if err != nil {
+ return 0, err
+ }
+
+ return count, nil
+}
+
+// countLinesOfCode counts lines of code (excluding binary files and common non-code files)
+func countLinesOfCode(repoPath string) (int, error) {
+ // Use git ls-files to get tracked files, then count lines
+ // Exclude binary files and common non-code files
+ cmd := exec.Command("bash", "-c", fmt.Sprintf(
+ `cd "%s" && git ls-files | grep -E '\.(go|py|js|ts|java|c|cpp|h|hpp|cs|rb|php|swift|kt|rs|scala|r|sh|bash|zsh|pl|lua|vim|el|clj|hs|ml|ex|exs|dart|jl|nim|v|zig|html|css|scss|sass|json|xml|yaml|yml|toml|ini|conf|cfg)$' | xargs wc -l 2>/dev/null | tail -n 1 | awk '{print $1}'`,
+ repoPath,
+ ))
+
+ output, err := cmd.Output()
+ if err != nil {
+ // Fallback: try a simpler approach
+ cmd = exec.Command("bash", "-c", fmt.Sprintf(
+ `find "%s" -type f -name "*.go" -o -name "*.py" -o -name "*.js" -o -name "*.java" -o -name "*.c" -o -name "*.cpp" -o -name "*.rs" | xargs wc -l 2>/dev/null | tail -n 1 | awk '{print $1}'`,
+ repoPath,
+ ))
+ output, err = cmd.Output()
+ if err != nil {
+ return 0, err
+ }
+ }
+
+ loc, err := strconv.Atoi(strings.TrimSpace(string(output)))
+ if err != nil {
+ return 0, err
+ }
+
+ return loc, nil
+}
+
+// getFirstCommitDate returns the date of the first commit
+func getFirstCommitDate(repoPath string) (string, error) {
+ cmd := exec.Command("git", "-C", repoPath, "log", "--reverse", "--pretty=format:%ai", "--date=short")
+ output, err := cmd.Output()
+ if err != nil {
+ return "", err
+ }
+
+ lines := strings.Split(string(output), "\n")
+ if len(lines) > 0 && lines[0] != "" {
+ // Extract just the date part (YYYY-MM-DD)
+ parts := strings.Fields(lines[0])
+ if len(parts) > 0 {
+ return parts[0], nil
+ }
+ }
+
+ return "", fmt.Errorf("no commits found")
+}
+
+// getLastCommitDate returns the date of the last commit
+func getLastCommitDate(repoPath string) (string, error) {
+ cmd := exec.Command("git", "-C", repoPath, "log", "-1", "--pretty=format:%ai", "--date=short")
+ output, err := cmd.Output()
+ if err != nil {
+ return "", err
+ }
+
+ // Extract just the date part (YYYY-MM-DD)
+ parts := strings.Fields(string(output))
+ if len(parts) > 0 {
+ return parts[0], nil
+ }
+
+ return "", fmt.Errorf("no commits found")
+}
+
+// detectLicense checks for common license files
+func detectLicense(repoPath string) string {
+ licenseFiles := []string{
+ "LICENSE",
+ "LICENSE.txt",
+ "LICENSE.md",
+ "license",
+ "license.txt",
+ "license.md",
+ "COPYING",
+ "COPYING.txt",
+ "COPYRIGHT",
+ "COPYRIGHT.txt",
+ }
+
+ for _, filename := range licenseFiles {
+ path := filepath.Join(repoPath, filename)
+ if info, err := os.Stat(path); err == nil && !info.IsDir() {
+ // Try to detect license type by reading the file
+ content, err := os.ReadFile(path)
+ if err == nil {
+ contentStr := string(content)
+ switch {
+ case strings.Contains(contentStr, "MIT License"):
+ return "MIT"
+ case strings.Contains(contentStr, "Apache License") && strings.Contains(contentStr, "Version 2.0"):
+ return "Apache-2.0"
+ case strings.Contains(contentStr, "GNU GENERAL PUBLIC LICENSE") && strings.Contains(contentStr, "Version 3"):
+ return "GPL-3.0"
+ case strings.Contains(contentStr, "GNU GENERAL PUBLIC LICENSE") && strings.Contains(contentStr, "Version 2"):
+ return "GPL-2.0"
+ case strings.Contains(contentStr, "BSD 3-Clause License"):
+ return "BSD-3-Clause"
+ case strings.Contains(contentStr, "BSD 2-Clause License"):
+ return "BSD-2-Clause"
+ case strings.Contains(contentStr, "Mozilla Public License Version 2.0"):
+ return "MPL-2.0"
+ case strings.Contains(contentStr, "ISC License"):
+ return "ISC"
+ case strings.Contains(contentStr, "GNU LESSER GENERAL PUBLIC LICENSE"):
+ return "LGPL"
+ case strings.Contains(contentStr, "The Unlicense"):
+ return "Unlicense"
+ case strings.Contains(contentStr, "CC0"):
+ return "CC0"
+ default:
+ return "Custom License"
+ }
+ }
+ return "License file found"
+ }
+ }
+
+ return "No license found"
+}
+
+// getAverageCommitAge calculates the average age of the last N commits in days
+func getAverageCommitAge(repoPath string, commitCount int) (float64, error) {
+ // Get the last N commit dates
+ cmd := exec.Command("git", "-C", repoPath, "log", fmt.Sprintf("-%d", commitCount), "--pretty=format:%at")
+ output, err := cmd.Output()
+ if err != nil {
+ return 0, err
+ }
+
+ lines := strings.Split(strings.TrimSpace(string(output)), "\n")
+ if len(lines) == 0 || lines[0] == "" {
+ return 0, fmt.Errorf("no commits found")
+ }
+
+ // Calculate average age
+ now := float64(time.Now().Unix())
+ var totalAge float64
+ validCommits := 0
+
+ for _, line := range lines {
+ if line == "" {
+ continue
+ }
+
+ timestamp, err := strconv.ParseInt(line, 10, 64)
+ if err != nil {
+ continue
+ }
+
+ age := (now - float64(timestamp)) / 86400 // Convert to days
+ totalAge += age
+ validCommits++
+ }
+
+ if validCommits == 0 {
+ return 0, fmt.Errorf("no valid commits found")
+ }
+
+ return totalAge / float64(validCommits), nil
+} \ No newline at end of file