apps/kepler/repos.go 3.1 K raw
1
package main
2
3
import (
4
	"errors"
5
	"os"
6
	"path/filepath"
7
	"sort"
8
	"strings"
9
10
	"github.com/go-git/go-git/v5"
11
	"github.com/go-git/go-git/v5/plumbing"
12
)
13
14
var defaultDescriptionPlaceholder = "Unnamed repository; edit this file 'description' to name the repository."
15
16
func (a *App) listRepos() ([]RepoSummary, error) {
17
	entries, err := os.ReadDir(a.RepoRoot)
18
	if err != nil {
19
		return nil, err
20
	}
21
	var out []RepoSummary
22
	for _, e := range entries {
23
		if !e.IsDir() {
24
			continue
25
		}
26
		path, name, ok := resolveRepoDir(a.RepoRoot, e.Name())
27
		if !ok {
28
			continue
29
		}
30
		s := RepoSummary{Name: name, Description: readDescription(path)}
31
		if repo, err := git.PlainOpen(path); err == nil {
32
			s.DefaultRef = defaultBranchName(repo)
33
			if head, err := repo.Head(); err == nil {
34
				if c, err := repo.CommitObject(head.Hash()); err == nil {
35
					s.LastCommit = c.Author.When
36
				}
37
			}
38
		}
39
		out = append(out, s)
40
	}
41
	sort.Slice(out, func(i, j int) bool {
42
		if !out[i].LastCommit.Equal(out[j].LastCommit) {
43
			return out[i].LastCommit.After(out[j].LastCommit)
44
		}
45
		return out[i].Name < out[j].Name
46
	})
47
	return out, nil
48
}
49
50
func (a *App) openRepo(name string) (*git.Repository, RepoSummary, error) {
51
	clean := strings.Trim(filepath.Clean(name), "/\\")
52
	if clean == "" || strings.Contains(clean, "..") || strings.ContainsAny(clean, "/\\") {
53
		return nil, RepoSummary{}, errors.New("invalid repo name")
54
	}
55
	path, resolvedName, ok := findRepoByName(a.RepoRoot, clean)
56
	if !ok {
57
		return nil, RepoSummary{}, errors.New("repo not found")
58
	}
59
	repo, err := git.PlainOpen(path)
60
	if err != nil {
61
		return nil, RepoSummary{}, err
62
	}
63
	s := RepoSummary{
64
		Name:        resolvedName,
65
		Description: readDescription(path),
66
		DefaultRef:  defaultBranchName(repo),
67
	}
68
	if head, err := repo.Head(); err == nil {
69
		if c, err := repo.CommitObject(head.Hash()); err == nil {
70
			s.LastCommit = c.Author.When
71
		}
72
	}
73
	return repo, s, nil
74
}
75
76
func resolveRepoDir(root, entry string) (path, name string, ok bool) {
77
	full := filepath.Join(root, entry)
78
	if strings.HasSuffix(entry, ".git") {
79
		if _, err := os.Stat(filepath.Join(full, "HEAD")); err == nil {
80
			return full, strings.TrimSuffix(entry, ".git"), true
81
		}
82
		return "", "", false
83
	}
84
	if _, err := os.Stat(filepath.Join(full, ".git", "HEAD")); err == nil {
85
		return full, entry, true
86
	}
87
	if _, err := os.Stat(filepath.Join(full, "HEAD")); err == nil {
88
		return full, entry, true
89
	}
90
	return "", "", false
91
}
92
93
func findRepoByName(root, name string) (string, string, bool) {
94
	candidates := []string{name + ".git", name}
95
	for _, c := range candidates {
96
		if p, n, ok := resolveRepoDir(root, c); ok {
97
			return p, n, true
98
		}
99
	}
100
	return "", "", false
101
}
102
103
func readDescription(repoPath string) string {
104
	data, err := os.ReadFile(filepath.Join(repoPath, "description"))
105
	if err != nil {
106
		return ""
107
	}
108
	s := strings.TrimSpace(string(data))
109
	if s == defaultDescriptionPlaceholder {
110
		return ""
111
	}
112
	return s
113
}
114
115
func defaultBranchName(repo *git.Repository) string {
116
	head, err := repo.Reference(plumbing.HEAD, false)
117
	if err != nil {
118
		return "main"
119
	}
120
	if head.Type() == plumbing.SymbolicReference {
121
		return head.Target().Short()
122
	}
123
	return "main"
124
}