Monorepo for Tangled
0

Configure Feed

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

core / appview / models / repo.go
5.6 kB 244 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) 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}