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