Téléverser les fichiers vers "/"

This commit is contained in:
RainbowYoshi 2026-01-31 21:51:49 +00:00
parent ebd4775d5e
commit 877280aa22
3 changed files with 767 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
# Binaries
PortaGit.exe
portagit
*.exe
*.dll
# Application Data
repositories/
uploads/
portagit.json
*.db
# OS specific checks
.DS_Store
thumbs.db

67
config_utils.go Normal file
View File

@ -0,0 +1,67 @@
package main
import (
"encoding/json"
"fmt"
"math"
"os"
"path/filepath"
)
// loadConfig reads the configuration file from the specified working directory.
// If the file does not exist or cannot be read, it returns a default configuration.
func loadConfig(cwd string) Config {
configPath := filepath.Join(cwd, configFileName)
file, err := os.ReadFile(configPath)
if err != nil {
return Config{
CustomColors: CustomColors{
BgColor: "#0d1117",
CardBg: "#161b22",
BorderColor: "#30363d",
AccentColor: "#58a6ff",
TextPrimary: "#c9d1d9",
TextSecondary: "#8b949e",
TextMuted: "#484f58",
ButtonText: "#ffffff",
},
}
}
var config Config
json.Unmarshal(file, &config)
if config.CustomColors.BgColor == "" {
config.CustomColors = CustomColors{
BgColor: "#0d1117",
CardBg: "#161b22",
BorderColor: "#30363d",
AccentColor: "#58a6ff",
TextPrimary: "#c9d1d9",
TextSecondary: "#8b949e",
TextMuted: "#484f58",
ButtonText: "#ffffff",
}
}
return config
}
// saveConfig writes the provided configuration to the specified file path.
// It marshals the config struct to JSON with indentation.
func saveConfig(path string, config Config) {
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
fmt.Println("Error saving config:", err)
return
}
os.WriteFile(path, data, 0644)
}
// humanize converts a byte count into a human-readable string (e.g., "1.2 MiB").
func humanize(bytes int64) string {
if bytes < 1024 {
return fmt.Sprintf("%d B", bytes)
}
exp := int(math.Log(float64(bytes)) / math.Log(1024))
pre := "KMGTPE"[exp-1 : exp]
return fmt.Sprintf("%.1f %siB", float64(bytes)/math.Pow(1024, float64(exp)), pre)
}

685
git_utils.go Normal file
View File

@ -0,0 +1,685 @@
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
}