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