This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / models / repo.go
6.3 kB 268 lines
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}