package main import ( "bufio" "bytes" "fmt" "os" "os/exec" "path/filepath" "sort" "strconv" "strings" "sync" "time" ) // migrateRepositories moves existing top-level directories into the "repositories" folder. // It skips specific reserved directories and configuration files. func migrateRepositories(root string) { reposDir := filepath.Join(root, "repositories") if _, err := os.Stat(reposDir); os.IsNotExist(err) { os.MkdirAll(reposDir, 0755) } entries, err := os.ReadDir(root) if err != nil { return } exclusions := map[string]bool{ "web": true, "repositories": true, ".git": true, ".agent": true, ".gemini": true, "tmp": true, "portagit.json": true, "main.go": true, "go.mod": true, "go.sum": true, "types.go": true, "git_utils.go": true, "config_utils.go": true, } // Define a list of reserved file and directory names that should be excluded from the migration process. for _, e := range entries { if e.IsDir() { name := e.Name() if exclusions[name] { continue } oldPath := filepath.Join(root, name) if _, err := os.Stat(filepath.Join(oldPath, ".git")); err == nil { newPath := filepath.Join(reposDir, name) fmt.Printf("Migrating repository %s to %s...\n", name, newPath) os.Rename(oldPath, newPath) } } } } // scanRepositories iterates through the repositories folder and collects metadata // for each git repository found (name, description, language, update time). // It utilizes concurrent goroutines to optimize the scanning performance. func scanRepositories(root string) []Repository { var repos []Repository var mu sync.Mutex var wg sync.WaitGroup entries, err := os.ReadDir(root) if err != nil { return repos } sem := make(chan struct{}, 20) for _, e := range entries { if e.IsDir() { wg.Add(1) go func(e os.DirEntry) { defer wg.Done() sem <- struct{}{} defer func() { <-sem }() repoPath := filepath.Join(root, e.Name()) if _, err := os.Stat(filepath.Join(repoPath, ".git")); err == nil { description := "" if descBytes, err := os.ReadFile(filepath.Join(repoPath, ".git", "description")); err == nil { desc := strings.TrimSpace(string(descBytes)) if desc != "" && !strings.Contains(desc, "Unnamed repository") { description = desc } } updated := "Unknown" cmd := exec.Command("git", "-C", repoPath, "log", "-1", "--format=%ar") if out, err := cmd.Output(); err == nil { updated = strings.TrimSpace(string(out)) } else { if info, err := e.Info(); err == nil { updated = info.ModTime().Format("Jan 02, 2006") } } // Determine the primary programming language by analyzing file extensions within the repository. language := "Unknown" extCounts := make(map[string]int) // Recursively walk through the repository directory tree to count file extensions. filepath.WalkDir(repoPath, func(path string, d os.DirEntry, err error) error { if err != nil { return nil } if d.IsDir() { if d.Name() == ".git" || d.Name() == "node_modules" || d.Name() == "vendor" || d.Name() == "dist" || d.Name() == "build" || d.Name() == ".idea" || d.Name() == ".vscode" || d.Name() == "bin" || d.Name() == "obj" { return filepath.SkipDir } return nil } ext := strings.ToLower(filepath.Ext(d.Name())) switch ext { case ".go": extCounts["Go"]++ case ".py": extCounts["Python"]++ case ".js", ".mjs", ".jsx": extCounts["JavaScript"]++ case ".ts", ".tsx": extCounts["TypeScript"]++ case ".html", ".htm": extCounts["HTML"]++ case ".css", ".scss", ".less": extCounts["CSS"]++ case ".java": extCounts["Java"]++ case ".c", ".h": extCounts["C"]++ case ".cpp", ".hpp", ".cc": extCounts["C++"]++ case ".php": extCounts["PHP"]++ case ".rb": extCounts["Ruby"]++ case ".rs": extCounts["Rust"]++ case ".cs": extCounts["C#"]++ case ".swift": extCounts["Swift"]++ case ".kt", ".kts": extCounts["Kotlin"]++ case ".dart": extCounts["Dart"]++ case ".lua": extCounts["Lua"]++ case ".sh", ".bash", ".zsh": extCounts["Shell"]++ } return nil }) maxCount := 0 for lang, count := range extCounts { if count > maxCount { maxCount = count language = lang } } mu.Lock() repos = append(repos, Repository{ Name: e.Name(), Description: description, Language: language, Path: e.Name(), UpdatedAt: updated, }) mu.Unlock() } }(e) } } wg.Wait() sort.Slice(repos, func(i, j int) bool { return repos[i].Name < repos[j].Name }) return repos } // getBranches retrieves all branches for a given repository and identifies the current active branch. func getBranches(repoPath string) ([]string, string) { var branches []string var current string cmd := exec.Command("git", "-C", repoPath, "branch", "-a") out, err := cmd.Output() if err != nil { return branches, "" } scanner := bufio.NewScanner(bytes.NewReader(out)) for scanner.Scan() { line := scanner.Text() line = strings.TrimSpace(line) if strings.HasPrefix(line, "* ") { current = strings.TrimPrefix(line, "* ") branches = append(branches, current) } else { cleanName := strings.TrimSpace(line) if strings.HasPrefix(cleanName, "remotes/origin/") { cleanName = strings.TrimPrefix(cleanName, "remotes/origin/") if cleanName == "HEAD" || strings.HasPrefix(cleanName, "HEAD ->") { continue } } // Avoid duplicates if a branch exists both locally and remotely duplicate := false for _, b := range branches { if b == cleanName { duplicate = true break } } if !duplicate { branches = append(branches, cleanName) } } } return branches, current } // createBranch creates a new branch in the specified repository. func createBranch(repoPath, branchName string) error { cmd := exec.Command("git", "-C", repoPath, "branch", branchName) return cmd.Run() } // getCommits fetches specific metadata for the latest 20 commits of the repository. func getCommits(repoPath string) []Commit { var commits []Commit cmd := exec.Command("git", "-C", repoPath, "log", "--all", "--pretty=format:%h|%s|%an|%ar", "-n", "20") out, err := cmd.Output() if err != nil { return commits } scanner := bufio.NewScanner(bytes.NewReader(out)) for scanner.Scan() { parts := strings.Split(scanner.Text(), "|") if len(parts) >= 4 { commits = append(commits, Commit{ Hash: parts[0], Message: parts[1], Author: parts[2], Date: parts[3], }) } } return commits } // getCommitsForMonth fetches all commits within a specific month and year. func getCommitsForMonth(repoPath string, year, month int) []Commit { var commits []Commit start := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC) end := start.AddDate(0, 1, 0) since := start.Format("2006-01-02") until := end.Format("2006-01-02") cmd := exec.Command("git", "-C", repoPath, "log", "--all", fmt.Sprintf("--since=%s", since), fmt.Sprintf("--until=%s", until), "--pretty=format:%h|%s|%an|%ar|%ad", "--date=short", ) out, err := cmd.Output() if err != nil { fmt.Printf("Error getting commits for month: %v\n", err) return commits } scanner := bufio.NewScanner(bytes.NewReader(out)) for scanner.Scan() { parts := strings.Split(scanner.Text(), "|") if len(parts) >= 4 { c := Commit{ Hash: parts[0], Message: parts[1], Author: parts[2], Date: parts[3], } commits = append(commits, c) } } return commits } // getCommitDiff retrieves the file changes (diffs) for a specific commit hash. func getCommitDiff(repoPath, hash string) []FileDiff { cmd := exec.Command("git", "-C", repoPath, "show", hash, "--name-only", "--format=") out, err := cmd.Output() if err != nil { return nil } files := strings.Split(strings.TrimSpace(string(out)), "\n") var diffs []FileDiff for _, file := range files { if file == "" { continue } diffCmd := exec.Command("git", "-C", repoPath, "show", hash, "--", file) diffOut, _ := diffCmd.Output() diffs = append(diffs, FileDiff{ Name: file, Content: string(diffOut), }) } return diffs } // getAvailableYears scans all repositories to find years that have commit activity matching the user's identity. func getAvailableYears(root string, authorPatterns []string) []int { yearsMap := make(map[int]bool) var mu sync.Mutex var wg sync.WaitGroup entries, err := os.ReadDir(root) if err != nil { return []int{time.Now().Year()} } sem := make(chan struct{}, 20) for _, e := range entries { if e.IsDir() { wg.Add(1) go func(e os.DirEntry) { defer wg.Done() sem <- struct{}{} defer func() { <-sem }() repoPath := filepath.Join(root, e.Name()) if _, err := os.Stat(filepath.Join(repoPath, ".git")); err == nil { args := []string{"-C", repoPath, "log", "--all", "--format=%ad", "--date=format:%Y"} for _, author := range authorPatterns { if author != "" { args = append(args, "--author="+author) } } cmd := exec.Command("git", args...) out, err := cmd.Output() if err == nil { localYears := make(map[int]bool) scanner := bufio.NewScanner(bytes.NewReader(out)) for scanner.Scan() { yStr := strings.TrimSpace(scanner.Text()) if y, err := strconv.Atoi(yStr); err == nil { localYears[y] = true } } mu.Lock() for y := range localYears { yearsMap[y] = true } mu.Unlock() } } }(e) } } wg.Wait() var years []int for y := range yearsMap { years = append(years, y) } if len(years) == 0 { years = append(years, time.Now().Year()) } sort.Sort(sort.Reverse(sort.IntSlice(years))) return years } // scanCommitsForYear aggregates commit activity for a given year across all repositories. // It returns data suitable for populating the contribution graph and the activity feed. func scanCommitsForYear(root string, year int, authorPatterns []string) ([]ContributionDay, []ActivityMonth) { commitCounts := make(map[string]int) repoMonthlyActivity := make(map[string]map[string]int) var mu sync.Mutex var wg sync.WaitGroup entries, err := os.ReadDir(root) if err != nil { return []ContributionDay{}, []ActivityMonth{} } sem := make(chan struct{}, 20) for _, e := range entries { if e.IsDir() { wg.Add(1) go func(e os.DirEntry) { defer wg.Done() sem <- struct{}{} defer func() { <-sem }() repoPath := filepath.Join(root, e.Name()) if _, err := os.Stat(filepath.Join(repoPath, ".git")); err == nil { args := []string{"-C", repoPath, "log", "--all", "--format=%ad", "--date=short"} for _, author := range authorPatterns { if author != "" { args = append(args, "--author="+author) } } cmd := exec.Command("git", args...) out, err := cmd.Output() if err == nil { localCounts := make(map[string]int) localMonthly := make(map[string]int) scanner := bufio.NewScanner(bytes.NewReader(out)) for scanner.Scan() { date := strings.TrimSpace(scanner.Text()) if strings.HasPrefix(date, fmt.Sprintf("%d-", year)) { localCounts[date]++ t, _ := time.Parse("2006-01-02", date) monthKey := t.Format("January 2006") localMonthly[monthKey]++ } } mu.Lock() for d, c := range localCounts { commitCounts[d] += c } for m, c := range localMonthly { if repoMonthlyActivity[m] == nil { repoMonthlyActivity[m] = make(map[string]int) } repoMonthlyActivity[m][e.Name()] += c } mu.Unlock() } } }(e) } } wg.Wait() wg.Wait() // Initialize the contribution graph grid, accounting for leap years and weekly offset. var graph []ContributionDay startDate := time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC) endDate := time.Date(year, 12, 31, 0, 0, 0, 0, time.UTC) offset := int(startDate.Weekday()) for i := 0; i < offset; i++ { graph = append(graph, ContributionDay{ Date: "", Count: 0, Color: "contrib-empty", }) } for d := startDate; !d.After(endDate); d = d.AddDate(0, 0, 1) { // Calculate the contribution level (color intensity) based on the number of commits for the day. dateStr := d.Format("2006-01-02") count := commitCounts[dateStr] level := 0 if count > 0 { level = 1 } if count >= 3 { level = 2 } if count >= 6 { level = 3 } if count >= 10 { level = 4 } colorClass := fmt.Sprintf("contrib-level-%d", level) graph = append(graph, ContributionDay{ Date: dateStr, Count: count, Color: colorClass, }) } var activityFeed []ActivityMonth for m := 12; m >= 1; m-- { t := time.Date(year, time.Month(m), 1, 0, 0, 0, 0, time.UTC) monthKey := t.Format("January 2006") if repos, ok := repoMonthlyActivity[monthKey]; ok { var items []ActivityItem totalCommitsInMonth := 0 for repoName, count := range repos { items = append(items, ActivityItem{ RepoName: repoName, Commits: count, }) totalCommitsInMonth += count } sort.Slice(items, func(i, j int) bool { return items[i].Commits > items[j].Commits }) activityFeed = append(activityFeed, ActivityMonth{ MonthName: monthKey, YearInt: year, MonthInt: m, TotalCommits: totalCommitsInMonth, RepoCount: len(items), Items: items, }) } } return graph, activityFeed } // scanActivityForDate retrieves detailed activity (list of repositories and commit counts) for a specific date. func scanActivityForDate(scanPath string, dateStr string, authorPatterns []string) []ActivityMonth { var activityFeed []ActivityMonth t, err := time.Parse("2006-01-02", dateStr) if err != nil { return activityFeed } entries, err := os.ReadDir(scanPath) if err != nil { return activityFeed } reposMap := make(map[string]int) var mu sync.Mutex var wg sync.WaitGroup sem := make(chan struct{}, 20) for _, e := range entries { if e.IsDir() { wg.Add(1) go func(repoName string) { defer wg.Done() sem <- struct{}{} defer func() { <-sem }() repoPath := filepath.Join(scanPath, repoName) if _, err := os.Stat(filepath.Join(repoPath, ".git")); err != nil { return } args := []string{"-C", repoPath, "log", "--all", "--format=%an|%ae|%ad", "--date=short"} cmd := exec.Command("git", args...) out, err := cmd.Output() if err == nil && len(out) > 0 { scanner := bufio.NewScanner(bytes.NewReader(out)) count := 0 for scanner.Scan() { line := scanner.Text() parts := strings.Split(line, "|") if len(parts) < 3 { continue } name := strings.TrimSpace(parts[0]) email := strings.TrimSpace(parts[1]) date := strings.TrimSpace(parts[2]) if date != dateStr { continue } match := false if len(authorPatterns) == 0 { match = true } else { for _, p := range authorPatterns { if strings.EqualFold(p, name) || strings.EqualFold(p, email) { match = true break } } } if match { count++ } } if count > 0 { mu.Lock() reposMap[repoName] = count mu.Unlock() } } }(e.Name()) } } wg.Wait() if len(reposMap) > 0 { var items []ActivityItem total := 0 for name, count := range reposMap { items = append(items, ActivityItem{RepoName: name, Commits: count}) total += count } sort.Slice(items, func(i, j int) bool { return items[i].Commits > items[j].Commits }) title := fmt.Sprintf("Activity on %s", t.Format("Jan 02, 2006")) activityFeed = append(activityFeed, ActivityMonth{ MonthName: title, TotalCommits: total, RepoCount: len(items), Items: items, YearInt: t.Year(), MonthInt: int(t.Month()), Date: dateStr, }) } return activityFeed } // getCommitsForDate retrieves all commits made on a specific date in a given repository. func getCommitsForDate(repoPath string, dateStr string) []Commit { var commits []Commit args := []string{"-C", repoPath, "log", "--all", "--format=%h|%s|%an|%ar|%ad", "--date=short"} cmd := exec.Command("git", args...) out, err := cmd.Output() if err != nil { return commits } scanner := bufio.NewScanner(bytes.NewReader(out)) for scanner.Scan() { line := scanner.Text() parts := strings.Split(line, "|") if len(parts) < 5 { continue } cDate := strings.TrimSpace(parts[4]) if cDate == dateStr { commits = append(commits, Commit{ Hash: parts[0], Message: parts[1], Author: parts[2], Date: parts[3], }) } } return commits }