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