This repository has no description
1package models
2
3import (
4 "fmt"
5 "strings"
6 "time"
7
8 "github.com/bluesky-social/indigo/atproto/syntax"
9 securejoin "github.com/cyphar/filepath-securejoin"
10 enry "github.com/go-enry/go-enry/v2"
11 "tangled.org/core/api/tangled"
12 "tangled.org/core/hostutil"
13)
14
15type Repo struct {
16 Id int64
17 Did string
18 Name string
19 Knot string
20 Rkey string
21 Created time.Time
22 Description string
23 Website string
24 Topics []string
25 Spindle string
26 Labels []string
27 RepoDid string
28
29 // optionally, populate this when querying for reverse mappings
30 RepoStats *RepoStats
31
32 // optional
33 Source string
34}
35
36func (r *Repo) AsRecord() tangled.Repo {
37 var source, spindle, description, website *string
38
39 if r.Source != "" {
40 source = &r.Source
41 }
42
43 if r.Spindle != "" {
44 spindle = &r.Spindle
45 }
46
47 if r.Description != "" {
48 description = &r.Description
49 }
50
51 if r.Website != "" {
52 website = &r.Website
53 }
54
55 return tangled.Repo{
56 Knot: r.Knot,
57 Name: r.cosmeticName(),
58 Description: description,
59 Website: website,
60 Topics: r.Topics,
61 CreatedAt: r.Created.Format(time.RFC3339),
62 Source: source,
63 Spindle: spindle,
64 Labels: r.Labels,
65 RepoDid: r.RepoDidPtr(),
66 }
67}
68
69func (r *Repo) cosmeticName() *string {
70 if r.Name == "" || r.Name == r.Rkey {
71 return nil
72 }
73 return &r.Name
74}
75
76func (r Repo) RepoAt() syntax.ATURI {
77 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.Did, tangled.RepoNSID, r.Rkey))
78}
79
80func (r Repo) Slug() string {
81 if r.Name != "" {
82 return r.Name
83 }
84 return r.Rkey
85}
86
87func (r Repo) RepoIdentifier() string {
88 if r.RepoDid != "" {
89 return r.RepoDid
90 }
91 p, _ := securejoin.SecureJoin(r.Did, r.Rkey)
92 return p
93}
94
95func (r Repo) PinIdentifier() string {
96 if r.RepoDid != "" {
97 return r.RepoDid
98 }
99 return string(r.RepoAt())
100}
101
102func (r Repo) RepoDidPtr() *string {
103 if r.RepoDid == "" {
104 return nil
105 }
106 return &r.RepoDid
107}
108
109func (r Repo) TopicStr() string {
110 return strings.Join(r.Topics, " ")
111}
112
113type RepoStats struct {
114 Language string
115 StarCount int
116 IssueCount IssueCount
117 PullCount PullCount
118 ForkCount int
119}
120
121// returns the first file extension for the language ("ts" for typescript) as
122// an uppercase string
123func (s *RepoStats) LangShortName() string {
124 if s == nil || s.Language == "" {
125 return ""
126 }
127 exts := enry.GetLanguageExtensions(s.Language)
128 if len(exts) > 0 {
129 // extensions include the leading dot, e.g. ".ts" -> "TS"
130 return strings.ToUpper(strings.TrimPrefix(exts[0], "."))
131 }
132 return s.Language
133}
134
135type IssueCount struct {
136 Open int
137 Closed int
138}
139
140type PullCount struct {
141 Open int
142 Merged int
143 Closed int
144 Deleted int
145}
146
147type RepoLabel struct {
148 Id int64
149 RepoDid syntax.DID
150 LabelAt syntax.ATURI
151}
152
153var reservedRepoNames = map[string]struct{}{
154 "self": {},
155}
156
157func ValidateRepoName(name string) error {
158 if len(name) == 0 {
159 return fmt.Errorf("Repository name cannot be empty")
160 }
161 if len(name) > 100 {
162 return fmt.Errorf("Repository name must be 100 characters or fewer")
163 }
164
165 // check for path traversal attempts
166 if strings.Contains(name, "/") || strings.Contains(name, "\\") {
167 return fmt.Errorf("Repository name contains invalid path characters")
168 }
169
170 // check for sequences that could be used for traversal when normalized
171 if strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") {
172 return fmt.Errorf("Repository name contains invalid path sequence")
173 }
174
175 // then continue with character validation
176 for _, char := range name {
177 if !((char >= 'a' && char <= 'z') ||
178 (char >= 'A' && char <= 'Z') ||
179 (char >= '0' && char <= '9') ||
180 char == '-' || char == '_' || char == '.') {
181 return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores")
182 }
183 }
184
185 // additional check to prevent multiple sequential dots
186 if strings.Contains(name, "..") {
187 return fmt.Errorf("Repository name cannot contain sequential dots")
188 }
189
190 if _, reserved := reservedRepoNames[strings.ToLower(name)]; reserved {
191 return fmt.Errorf("Repository name %q is reserved", name)
192 }
193
194 // if all checks pass
195 return nil
196}
197
198func StripGitExt(name string) string {
199 return strings.TrimSuffix(name, ".git")
200}
201
202// ValidateSpindle normalizes a user-typed spindle host. Empty means "no spindle".
203//
204// Membership is enforced by the spindle itself, so this only checks that the value
205// is a host the appview can safely send service-auth requests to.
206func ValidateSpindle(raw string, dev bool) (string, error) {
207 raw = strings.TrimSpace(raw)
208 if raw == "" {
209 return "", nil
210 }
211
212 host, noTLS, err := hostutil.ParseHostname(raw)
213 if err != nil {
214 return "", fmt.Errorf("%q is not a valid spindle host", raw)
215 }
216
217 // ParseHostname allows localhost:PORT, which would make the appview dial itself
218 if noTLS && !dev {
219 return "", fmt.Errorf("spindle must be a public https host")
220 }
221
222 return host, nil
223}
224
225type RepoGroup struct {
226 Repo *Repo
227 Issues []Issue
228}
229
230type BlobContentType int
231
232const (
233 BlobContentTypeCode BlobContentType = iota
234 BlobContentTypeMarkup
235 BlobContentTypeImage
236 BlobContentTypeSvg
237 BlobContentTypeVideo
238 BlobContentTypeSubmodule
239 BlobContentTypeOther
240)
241
242func (ty BlobContentType) IsCode() bool { return ty == BlobContentTypeCode }
243func (ty BlobContentType) IsMarkup() bool { return ty == BlobContentTypeMarkup }
244func (ty BlobContentType) IsImage() bool { return ty == BlobContentTypeImage }
245func (ty BlobContentType) IsSvg() bool { return ty == BlobContentTypeSvg }
246func (ty BlobContentType) IsVideo() bool { return ty == BlobContentTypeVideo }
247func (ty BlobContentType) IsSubmodule() bool { return ty == BlobContentTypeSubmodule }
248func (ty BlobContentType) HasTextView() bool {
249 return ty == BlobContentTypeCode || ty == BlobContentTypeMarkup || ty == BlobContentTypeSvg
250}
251func (ty BlobContentType) HasRenderedView() bool {
252 return ty != BlobContentTypeCode && ty != BlobContentTypeOther
253}
254func (ty BlobContentType) HasRawView() bool {
255 return ty != BlobContentTypeSubmodule
256}
257
258type BlobView struct {
259 // content type flags
260 ContentType BlobContentType
261
262 // Content data
263 ContentSrc string // URL to raw content
264 Contents string // textual content
265 FileTooLarge bool // textual content is too large
266 Lines int // line count of textual content
267 SizeHint uint64
268}