forked from tangled.org/core
this repo has no description

Compare changes

Choose any two refs to compare.

Changed files
+224 -832
appview
notify
db
pages
templates
state
hook
knotserver
nix
sets
types
+57 -67
appview/notify/db/db.go
··· 3 3 import ( 4 4 "context" 5 5 "log" 6 + "maps" 6 7 "slices" 7 8 8 9 "github.com/bluesky-social/indigo/atproto/syntax" ··· 12 13 "tangled.org/core/appview/notify" 13 14 "tangled.org/core/idresolver" 14 15 "tangled.org/core/orm" 15 - "tangled.org/core/sets" 16 16 ) 17 17 18 18 const ( 19 - maxMentions = 8 19 + maxMentions = 5 20 20 ) 21 21 22 22 type databaseNotifier struct { ··· 50 50 } 51 51 52 52 actorDid := syntax.DID(star.Did) 53 - recipients := sets.Singleton(syntax.DID(repo.Did)) 53 + recipients := []syntax.DID{syntax.DID(repo.Did)} 54 54 eventType := models.NotificationTypeRepoStarred 55 55 entityType := "repo" 56 56 entityId := star.RepoAt.String() ··· 75 75 } 76 76 77 77 func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 78 + 79 + // build the recipients list 80 + // - owner of the repo 81 + // - collaborators in the repo 82 + var recipients []syntax.DID 83 + recipients = append(recipients, syntax.DID(issue.Repo.Did)) 78 84 collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt())) 79 85 if err != nil { 80 86 log.Printf("failed to fetch collaborators: %v", err) 81 87 return 82 88 } 83 - 84 - // build the recipients list 85 - // - owner of the repo 86 - // - collaborators in the repo 87 - // - remove users already mentioned 88 - recipients := sets.Singleton(syntax.DID(issue.Repo.Did)) 89 89 for _, c := range collaborators { 90 - recipients.Insert(c.SubjectDid) 91 - } 92 - for _, m := range mentions { 93 - recipients.Remove(m) 90 + recipients = append(recipients, c.SubjectDid) 94 91 } 95 92 96 93 actorDid := syntax.DID(issue.Did) ··· 112 109 ) 113 110 n.notifyEvent( 114 111 actorDid, 115 - sets.Collect(slices.Values(mentions)), 112 + mentions, 116 113 models.NotificationTypeUserMentioned, 117 114 entityType, 118 115 entityId, ··· 134 131 } 135 132 issue := issues[0] 136 133 137 - // built the recipients list: 138 - // - the owner of the repo 139 - // - | if the comment is a reply -> everybody on that thread 140 - // | if the comment is a top level -> just the issue owner 141 - // - remove mentioned users from the recipients list 142 - recipients := sets.Singleton(syntax.DID(issue.Repo.Did)) 134 + var recipients []syntax.DID 135 + recipients = append(recipients, syntax.DID(issue.Repo.Did)) 143 136 144 137 if comment.IsReply() { 145 138 // if this comment is a reply, then notify everybody in that thread 146 139 parentAtUri := *comment.ReplyTo 140 + allThreads := issue.CommentList() 147 141 148 142 // find the parent thread, and add all DIDs from here to the recipient list 149 - for _, t := range issue.CommentList() { 143 + for _, t := range allThreads { 150 144 if t.Self.AtUri().String() == parentAtUri { 151 - for _, p := range t.Participants() { 152 - recipients.Insert(p) 153 - } 145 + recipients = append(recipients, t.Participants()...) 154 146 } 155 147 } 156 148 } else { 157 149 // not a reply, notify just the issue author 158 - recipients.Insert(syntax.DID(issue.Did)) 159 - } 160 - 161 - for _, m := range mentions { 162 - recipients.Remove(m) 150 + recipients = append(recipients, syntax.DID(issue.Did)) 163 151 } 164 152 165 153 actorDid := syntax.DID(comment.Did) ··· 181 169 ) 182 170 n.notifyEvent( 183 171 actorDid, 184 - sets.Collect(slices.Values(mentions)), 172 + mentions, 185 173 models.NotificationTypeUserMentioned, 186 174 entityType, 187 175 entityId, ··· 197 185 198 186 func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) { 199 187 actorDid := syntax.DID(follow.UserDid) 200 - recipients := sets.Singleton(syntax.DID(follow.SubjectDid)) 188 + recipients := []syntax.DID{syntax.DID(follow.SubjectDid)} 201 189 eventType := models.NotificationTypeFollowed 202 190 entityType := "follow" 203 191 entityId := follow.UserDid ··· 225 213 log.Printf("NewPull: failed to get repos: %v", err) 226 214 return 227 215 } 216 + 217 + // build the recipients list 218 + // - owner of the repo 219 + // - collaborators in the repo 220 + var recipients []syntax.DID 221 + recipients = append(recipients, syntax.DID(repo.Did)) 228 222 collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt())) 229 223 if err != nil { 230 224 log.Printf("failed to fetch collaborators: %v", err) 231 225 return 232 226 } 233 - 234 - // build the recipients list 235 - // - owner of the repo 236 - // - collaborators in the repo 237 - recipients := sets.Singleton(syntax.DID(repo.Did)) 238 227 for _, c := range collaborators { 239 - recipients.Insert(c.SubjectDid) 228 + recipients = append(recipients, c.SubjectDid) 240 229 } 241 230 242 231 actorDid := syntax.DID(pull.OwnerDid) ··· 279 268 // build up the recipients list: 280 269 // - repo owner 281 270 // - all pull participants 282 - // - remove those already mentioned 283 - recipients := sets.Singleton(syntax.DID(repo.Did)) 271 + var recipients []syntax.DID 272 + recipients = append(recipients, syntax.DID(repo.Did)) 284 273 for _, p := range pull.Participants() { 285 - recipients.Insert(syntax.DID(p)) 286 - } 287 - for _, m := range mentions { 288 - recipients.Remove(m) 274 + recipients = append(recipients, syntax.DID(p)) 289 275 } 290 276 291 277 actorDid := syntax.DID(comment.OwnerDid) ··· 309 295 ) 310 296 n.notifyEvent( 311 297 actorDid, 312 - sets.Collect(slices.Values(mentions)), 298 + mentions, 313 299 models.NotificationTypeUserMentioned, 314 300 entityType, 315 301 entityId, ··· 336 322 } 337 323 338 324 func (n *databaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) { 325 + // build up the recipients list: 326 + // - repo owner 327 + // - repo collaborators 328 + // - all issue participants 329 + var recipients []syntax.DID 330 + recipients = append(recipients, syntax.DID(issue.Repo.Did)) 339 331 collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt())) 340 332 if err != nil { 341 333 log.Printf("failed to fetch collaborators: %v", err) 342 334 return 343 335 } 344 - 345 - // build up the recipients list: 346 - // - repo owner 347 - // - repo collaborators 348 - // - all issue participants 349 - recipients := sets.Singleton(syntax.DID(issue.Repo.Did)) 350 336 for _, c := range collaborators { 351 - recipients.Insert(c.SubjectDid) 337 + recipients = append(recipients, c.SubjectDid) 352 338 } 353 339 for _, p := range issue.Participants() { 354 - recipients.Insert(syntax.DID(p)) 340 + recipients = append(recipients, syntax.DID(p)) 355 341 } 356 342 357 343 entityType := "pull" ··· 387 373 return 388 374 } 389 375 376 + // build up the recipients list: 377 + // - repo owner 378 + // - all pull participants 379 + var recipients []syntax.DID 380 + recipients = append(recipients, syntax.DID(repo.Did)) 390 381 collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt())) 391 382 if err != nil { 392 383 log.Printf("failed to fetch collaborators: %v", err) 393 384 return 394 385 } 395 - 396 - // build up the recipients list: 397 - // - repo owner 398 - // - all pull participants 399 - recipients := sets.Singleton(syntax.DID(repo.Did)) 400 386 for _, c := range collaborators { 401 - recipients.Insert(c.SubjectDid) 387 + recipients = append(recipients, c.SubjectDid) 402 388 } 403 389 for _, p := range pull.Participants() { 404 - recipients.Insert(syntax.DID(p)) 390 + recipients = append(recipients, syntax.DID(p)) 405 391 } 406 392 407 393 entityType := "pull" ··· 437 423 438 424 func (n *databaseNotifier) notifyEvent( 439 425 actorDid syntax.DID, 440 - recipients sets.Set[syntax.DID], 426 + recipients []syntax.DID, 441 427 eventType models.NotificationType, 442 428 entityType string, 443 429 entityId string, ··· 445 431 issueId *int64, 446 432 pullId *int64, 447 433 ) { 448 - // if the user is attempting to mention >maxMentions users, this is probably spam, do not mention anybody 449 - if eventType == models.NotificationTypeUserMentioned && recipients.Len() > maxMentions { 450 - return 434 + if eventType == models.NotificationTypeUserMentioned && len(recipients) > maxMentions { 435 + recipients = recipients[:maxMentions] 436 + } 437 + recipientSet := make(map[syntax.DID]struct{}) 438 + for _, did := range recipients { 439 + // everybody except actor themselves 440 + if did != actorDid { 441 + recipientSet[did] = struct{}{} 442 + } 451 443 } 452 444 453 - recipients.Remove(actorDid) 454 - 455 445 prefMap, err := db.GetNotificationPreferences( 456 446 n.db, 457 - orm.FilterIn("user_did", slices.Collect(recipients.All())), 447 + orm.FilterIn("user_did", slices.Collect(maps.Keys(recipientSet))), 458 448 ) 459 449 if err != nil { 460 450 // failed to get prefs for users ··· 470 460 defer tx.Rollback() 471 461 472 462 // filter based on preferences 473 - for recipientDid := range recipients.All() { 463 + for recipientDid := range recipientSet { 474 464 prefs, ok := prefMap[recipientDid] 475 465 if !ok { 476 466 prefs = models.DefaultNotificationPreferences(recipientDid)
+6 -9
appview/pages/templates/user/signup.html
··· 43 43 page to complete your registration. 44 44 </span> 45 45 <div class="w-full mt-4 text-center"> 46 - <div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}" data-size="flexible"></div> 46 + <div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}"></div> 47 47 </div> 48 48 <button class="btn text-base w-full my-2 mt-6" type="submit" id="signup-button" tabindex="7" > 49 49 <span>join now</span> 50 50 </button> 51 - <p class="text-sm text-gray-500"> 52 - Already have an AT Protocol account? <a href="/login" class="underline">Login to Tangled</a>. 53 - </p> 51 + </form> 52 + <p class="text-sm text-gray-500"> 53 + Already have an AT Protocol account? <a href="/login" class="underline">Login to Tangled</a>. 54 + </p> 54 55 55 - <p id="signup-msg" class="error w-full"></p> 56 - <p class="text-sm text-gray-500 pt-4"> 57 - By signing up, you agree to our <a href="/terms" class="underline">Terms of Service</a> and <a href="/privacy" class="underline">Privacy Policy</a>. 58 - </p> 59 - </form> 56 + <p id="signup-msg" class="error w-full"></p> 60 57 </main> 61 58 </body> 62 59 </html>
+17
appview/state/git_http.go
··· 25 25 26 26 } 27 27 28 + func (s *State) UploadArchive(w http.ResponseWriter, r *http.Request) { 29 + user, ok := r.Context().Value("resolvedId").(identity.Identity) 30 + if !ok { 31 + http.Error(w, "failed to resolve user", http.StatusInternalServerError) 32 + return 33 + } 34 + repo := r.Context().Value("repo").(*models.Repo) 35 + 36 + scheme := "https" 37 + if s.config.Core.Dev { 38 + scheme = "http" 39 + } 40 + 41 + targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-archive?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 42 + s.proxyRequest(w, r, targetURL) 43 + } 44 + 28 45 func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) { 29 46 user, ok := r.Context().Value("resolvedId").(identity.Identity) 30 47 if !ok {
+1
appview/state/router.go
··· 101 101 102 102 // These routes get proxied to the knot 103 103 r.Get("/info/refs", s.InfoRefs) 104 + r.Post("/git-upload-archive", s.UploadArchive) 104 105 r.Post("/git-upload-pack", s.UploadPack) 105 106 r.Post("/git-receive-pack", s.ReceivePack) 106 107
+3 -3
flake.lock
··· 150 150 }, 151 151 "nixpkgs": { 152 152 "locked": { 153 - "lastModified": 1765186076, 154 - "narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=", 153 + "lastModified": 1751984180, 154 + "narHash": "sha256-LwWRsENAZJKUdD3SpLluwDmdXY9F45ZEgCb0X+xgOL0=", 155 155 "owner": "nixos", 156 156 "repo": "nixpkgs", 157 - "rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8", 157 + "rev": "9807714d6944a957c2e036f84b0ff8caf9930bc0", 158 158 "type": "github" 159 159 }, 160 160 "original": {
+2
flake.nix
··· 80 80 }).buildGoApplication; 81 81 modules = ./nix/gomod2nix.toml; 82 82 sqlite-lib = self.callPackage ./nix/pkgs/sqlite-lib.nix { 83 + inherit (pkgs) gcc; 83 84 inherit sqlite-lib-src; 84 85 }; 85 86 lexgen = self.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;}; ··· 155 156 nativeBuildInputs = [ 156 157 pkgs.go 157 158 pkgs.air 159 + pkgs.tilt 158 160 pkgs.gopls 159 161 pkgs.httpie 160 162 pkgs.litecli
+1 -1
go.mod
··· 1 1 module tangled.org/core 2 2 3 - go 1.25.0 3 + go 1.24.4 4 4 5 5 require ( 6 6 github.com/Blank-Xu/sql-adapter v1.1.1
+4 -4
hook/hook.go
··· 48 48 }, 49 49 Commands: []*cli.Command{ 50 50 { 51 - Name: "post-receive", 52 - Usage: "sends a post-receive hook to the knot (waits for stdin)", 53 - Action: postReceive, 51 + Name: "post-recieve", 52 + Usage: "sends a post-recieve hook to the knot (waits for stdin)", 53 + Action: postRecieve, 54 54 }, 55 55 }, 56 56 } 57 57 } 58 58 59 - func postReceive(ctx context.Context, cmd *cli.Command) error { 59 + func postRecieve(ctx context.Context, cmd *cli.Command) error { 60 60 gitDir := cmd.String("git-dir") 61 61 userDid := cmd.String("user-did") 62 62 userHandle := cmd.String("user-handle")
+1 -1
hook/setup.go
··· 138 138 option_var="GIT_PUSH_OPTION_$i" 139 139 push_options+=(-push-option "${!option_var}") 140 140 done 141 - %s hook -git-dir "$GIT_DIR" -user-did "$GIT_USER_DID" -user-handle "$GIT_USER_HANDLE" -internal-api "%s" "${push_options[@]}" post-receive 141 + %s hook -git-dir "$GIT_DIR" -user-did "$GIT_USER_DID" -user-handle "$GIT_USER_HANDLE" -internal-api "%s" "${push_options[@]}" post-recieve 142 142 `, executablePath, config.internalApi) 143 143 144 144 return os.WriteFile(hookPath, []byte(hookContent), 0755)
-81
knotserver/db/db.go
··· 1 - package db 2 - 3 - import ( 4 - "context" 5 - "database/sql" 6 - "log/slog" 7 - "strings" 8 - 9 - _ "github.com/mattn/go-sqlite3" 10 - "tangled.org/core/log" 11 - ) 12 - 13 - type DB struct { 14 - db *sql.DB 15 - logger *slog.Logger 16 - } 17 - 18 - func Setup(ctx context.Context, dbPath string) (*DB, error) { 19 - // https://github.com/mattn/go-sqlite3#connection-string 20 - opts := []string{ 21 - "_foreign_keys=1", 22 - "_journal_mode=WAL", 23 - "_synchronous=NORMAL", 24 - "_auto_vacuum=incremental", 25 - } 26 - 27 - logger := log.FromContext(ctx) 28 - logger = log.SubLogger(logger, "db") 29 - 30 - db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&")) 31 - if err != nil { 32 - return nil, err 33 - } 34 - 35 - conn, err := db.Conn(ctx) 36 - if err != nil { 37 - return nil, err 38 - } 39 - defer conn.Close() 40 - 41 - _, err = conn.ExecContext(ctx, ` 42 - create table if not exists known_dids ( 43 - did text primary key 44 - ); 45 - 46 - create table if not exists public_keys ( 47 - id integer primary key autoincrement, 48 - did text not null, 49 - key text not null, 50 - created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 51 - unique(did, key), 52 - foreign key (did) references known_dids(did) on delete cascade 53 - ); 54 - 55 - create table if not exists _jetstream ( 56 - id integer primary key autoincrement, 57 - last_time_us integer not null 58 - ); 59 - 60 - create table if not exists events ( 61 - rkey text not null, 62 - nsid text not null, 63 - event text not null, -- json 64 - created integer not null default (strftime('%s', 'now')), 65 - primary key (rkey, nsid) 66 - ); 67 - 68 - create table if not exists migrations ( 69 - id integer primary key autoincrement, 70 - name text unique 71 - ); 72 - `) 73 - if err != nil { 74 - return nil, err 75 - } 76 - 77 - return &DB{ 78 - db: db, 79 - logger: logger, 80 - }, nil 81 - }
+64
knotserver/db/init.go
··· 1 + package db 2 + 3 + import ( 4 + "database/sql" 5 + "strings" 6 + 7 + _ "github.com/mattn/go-sqlite3" 8 + ) 9 + 10 + type DB struct { 11 + db *sql.DB 12 + } 13 + 14 + func Setup(dbPath string) (*DB, error) { 15 + // https://github.com/mattn/go-sqlite3#connection-string 16 + opts := []string{ 17 + "_foreign_keys=1", 18 + "_journal_mode=WAL", 19 + "_synchronous=NORMAL", 20 + "_auto_vacuum=incremental", 21 + } 22 + 23 + db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&")) 24 + if err != nil { 25 + return nil, err 26 + } 27 + 28 + // NOTE: If any other migration is added here, you MUST 29 + // copy the pattern in appview: use a single sql.Conn 30 + // for every migration. 31 + 32 + _, err = db.Exec(` 33 + create table if not exists known_dids ( 34 + did text primary key 35 + ); 36 + 37 + create table if not exists public_keys ( 38 + id integer primary key autoincrement, 39 + did text not null, 40 + key text not null, 41 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 42 + unique(did, key), 43 + foreign key (did) references known_dids(did) on delete cascade 44 + ); 45 + 46 + create table if not exists _jetstream ( 47 + id integer primary key autoincrement, 48 + last_time_us integer not null 49 + ); 50 + 51 + create table if not exists events ( 52 + rkey text not null, 53 + nsid text not null, 54 + event text not null, -- json 55 + created integer not null default (strftime('%s', 'now')), 56 + primary key (rkey, nsid) 57 + ); 58 + `) 59 + if err != nil { 60 + return nil, err 61 + } 62 + 63 + return &DB{db: db}, nil 64 + }
+13 -1
knotserver/git/service/service.go
··· 95 95 return c.RunService(cmd) 96 96 } 97 97 98 + func (c *ServiceCommand) UploadArchive() error { 99 + cmd := exec.Command("git", []string{ 100 + "upload-archive", 101 + ".", 102 + }...) 103 + 104 + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 105 + cmd.Env = append(cmd.Env, fmt.Sprintf("GIT_PROTOCOL=%s", c.GitProtocol)) 106 + cmd.Dir = c.Dir 107 + 108 + return c.RunService(cmd) 109 + } 110 + 98 111 func (c *ServiceCommand) UploadPack() error { 99 112 cmd := exec.Command("git", []string{ 100 - "-c", "uploadpack.allowFilter=true", 101 113 "upload-pack", 102 114 "--stateless-rpc", 103 115 ".",
+47
knotserver/git.go
··· 56 56 } 57 57 } 58 58 59 + func (h *Knot) UploadArchive(w http.ResponseWriter, r *http.Request) { 60 + did := chi.URLParam(r, "did") 61 + name := chi.URLParam(r, "name") 62 + repo, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name)) 63 + if err != nil { 64 + gitError(w, err.Error(), http.StatusInternalServerError) 65 + h.l.Error("git: failed to secure join repo path", "handler", "UploadPack", "error", err) 66 + return 67 + } 68 + 69 + const expectedContentType = "application/x-git-upload-archive-request" 70 + contentType := r.Header.Get("Content-Type") 71 + if contentType != expectedContentType { 72 + gitError(w, fmt.Sprintf("Expected Content-Type: '%s', but received '%s'.", expectedContentType, contentType), http.StatusUnsupportedMediaType) 73 + } 74 + 75 + var bodyReader io.ReadCloser = r.Body 76 + if r.Header.Get("Content-Encoding") == "gzip" { 77 + gzipReader, err := gzip.NewReader(r.Body) 78 + if err != nil { 79 + gitError(w, err.Error(), http.StatusInternalServerError) 80 + h.l.Error("git: failed to create gzip reader", "handler", "UploadArchive", "error", err) 81 + return 82 + } 83 + defer gzipReader.Close() 84 + bodyReader = gzipReader 85 + } 86 + 87 + w.Header().Set("Content-Type", "application/x-git-upload-archive-result") 88 + 89 + h.l.Info("git: executing git-upload-archive", "handler", "UploadArchive", "repo", repo) 90 + 91 + cmd := service.ServiceCommand{ 92 + GitProtocol: r.Header.Get("Git-Protocol"), 93 + Dir: repo, 94 + Stdout: w, 95 + Stdin: bodyReader, 96 + } 97 + 98 + w.WriteHeader(http.StatusOK) 99 + 100 + if err := cmd.UploadArchive(); err != nil { 101 + h.l.Error("git: failed to execute git-upload-pack", "handler", "UploadPack", "error", err) 102 + return 103 + } 104 + } 105 + 59 106 func (h *Knot) UploadPack(w http.ResponseWriter, r *http.Request) { 60 107 did := chi.URLParam(r, "did") 61 108 name := chi.URLParam(r, "name")
+1
knotserver/router.go
··· 82 82 r.Route("/{name}", func(r chi.Router) { 83 83 // routes for git operations 84 84 r.Get("/info/refs", h.InfoRefs) 85 + r.Post("/git-upload-archive", h.UploadArchive) 85 86 r.Post("/git-upload-pack", h.UploadPack) 86 87 r.Post("/git-receive-pack", h.ReceivePack) 87 88 })
+1 -1
knotserver/server.go
··· 64 64 logger.Info("running in dev mode, signature verification is disabled") 65 65 } 66 66 67 - db, err := db.Setup(ctx, c.Server.DBPath) 67 + db, err := db.Setup(c.Server.DBPath) 68 68 if err != nil { 69 69 return fmt.Errorf("failed to load db: %w", err) 70 70 }
+5 -7
nix/pkgs/sqlite-lib.nix
··· 1 1 { 2 + gcc, 2 3 stdenv, 3 4 sqlite-lib-src, 4 5 }: 5 6 stdenv.mkDerivation { 6 7 name = "sqlite-lib"; 7 8 src = sqlite-lib-src; 8 - 9 + nativeBuildInputs = [gcc]; 9 10 buildPhase = '' 10 - $CC -c sqlite3.c 11 - $AR rcs libsqlite3.a sqlite3.o 12 - $RANLIB libsqlite3.a 13 - ''; 14 - 15 - installPhase = '' 11 + gcc -c sqlite3.c 12 + ar rcs libsqlite3.a sqlite3.o 13 + ranlib libsqlite3.a 16 14 mkdir -p $out/include $out/lib 17 15 cp *.h $out/include 18 16 cp libsqlite3.a $out/lib
-31
sets/gen.go
··· 1 - package sets 2 - 3 - import ( 4 - "math/rand" 5 - "reflect" 6 - "testing/quick" 7 - ) 8 - 9 - func (_ Set[T]) Generate(rand *rand.Rand, size int) reflect.Value { 10 - s := New[T]() 11 - 12 - var zero T 13 - itemType := reflect.TypeOf(zero) 14 - 15 - for { 16 - if s.Len() >= size { 17 - break 18 - } 19 - 20 - item, ok := quick.Value(itemType, rand) 21 - if !ok { 22 - continue 23 - } 24 - 25 - if val, ok := item.Interface().(T); ok { 26 - s.Insert(val) 27 - } 28 - } 29 - 30 - return reflect.ValueOf(s) 31 - }
-35
sets/readme.txt
··· 1 - sets 2 - ---- 3 - set datastructure for go with generics and iterators. the 4 - api is supposed to mimic rust's std::collections::HashSet api. 5 - 6 - s1 := sets.Collect(slices.Values([]int{1, 2, 3, 4})) 7 - s2 := sets.Collect(slices.Values([]int{1, 2, 3, 4, 5, 6})) 8 - 9 - union := sets.Collect(s1.Union(s2)) 10 - intersect := sets.Collect(s1.Intersection(s2)) 11 - diff := sets.Collect(s1.Difference(s2)) 12 - symdiff := sets.Collect(s1.SymmetricDifference(s2)) 13 - 14 - s1.Len() // 4 15 - s1.Contains(1) // true 16 - s1.IsEmpty() // false 17 - s1.IsSubset(s2) // true 18 - s1.IsSuperset(s2) // false 19 - s1.IsDisjoint(s2) // false 20 - 21 - if exists := s1.Insert(1); exists { 22 - // already existed in set 23 - } 24 - 25 - if existed := s1.Remove(1); existed { 26 - // existed in set, now removed 27 - } 28 - 29 - 30 - testing 31 - ------- 32 - includes property-based tests using the wonderful 33 - testing/quick module! 34 - 35 - go test -v
-174
sets/set.go
··· 1 - package sets 2 - 3 - import ( 4 - "iter" 5 - "maps" 6 - ) 7 - 8 - type Set[T comparable] struct { 9 - data map[T]struct{} 10 - } 11 - 12 - func New[T comparable]() Set[T] { 13 - return Set[T]{ 14 - data: make(map[T]struct{}), 15 - } 16 - } 17 - 18 - func (s *Set[T]) Insert(item T) bool { 19 - _, exists := s.data[item] 20 - s.data[item] = struct{}{} 21 - return !exists 22 - } 23 - 24 - func Singleton[T comparable](item T) Set[T] { 25 - n := New[T]() 26 - _ = n.Insert(item) 27 - return n 28 - } 29 - 30 - func (s *Set[T]) Remove(item T) bool { 31 - _, exists := s.data[item] 32 - if exists { 33 - delete(s.data, item) 34 - } 35 - return exists 36 - } 37 - 38 - func (s Set[T]) Contains(item T) bool { 39 - _, exists := s.data[item] 40 - return exists 41 - } 42 - 43 - func (s Set[T]) Len() int { 44 - return len(s.data) 45 - } 46 - 47 - func (s Set[T]) IsEmpty() bool { 48 - return len(s.data) == 0 49 - } 50 - 51 - func (s *Set[T]) Clear() { 52 - s.data = make(map[T]struct{}) 53 - } 54 - 55 - func (s Set[T]) All() iter.Seq[T] { 56 - return func(yield func(T) bool) { 57 - for item := range s.data { 58 - if !yield(item) { 59 - return 60 - } 61 - } 62 - } 63 - } 64 - 65 - func (s Set[T]) Clone() Set[T] { 66 - return Set[T]{ 67 - data: maps.Clone(s.data), 68 - } 69 - } 70 - 71 - func (s Set[T]) Union(other Set[T]) iter.Seq[T] { 72 - if s.Len() >= other.Len() { 73 - return chain(s.All(), other.Difference(s)) 74 - } else { 75 - return chain(other.All(), s.Difference(other)) 76 - } 77 - } 78 - 79 - func chain[T any](seqs ...iter.Seq[T]) iter.Seq[T] { 80 - return func(yield func(T) bool) { 81 - for _, seq := range seqs { 82 - for item := range seq { 83 - if !yield(item) { 84 - return 85 - } 86 - } 87 - } 88 - } 89 - } 90 - 91 - func (s Set[T]) Intersection(other Set[T]) iter.Seq[T] { 92 - return func(yield func(T) bool) { 93 - for item := range s.data { 94 - if other.Contains(item) { 95 - if !yield(item) { 96 - return 97 - } 98 - } 99 - } 100 - } 101 - } 102 - 103 - func (s Set[T]) Difference(other Set[T]) iter.Seq[T] { 104 - return func(yield func(T) bool) { 105 - for item := range s.data { 106 - if !other.Contains(item) { 107 - if !yield(item) { 108 - return 109 - } 110 - } 111 - } 112 - } 113 - } 114 - 115 - func (s Set[T]) SymmetricDifference(other Set[T]) iter.Seq[T] { 116 - return func(yield func(T) bool) { 117 - for item := range s.data { 118 - if !other.Contains(item) { 119 - if !yield(item) { 120 - return 121 - } 122 - } 123 - } 124 - for item := range other.data { 125 - if !s.Contains(item) { 126 - if !yield(item) { 127 - return 128 - } 129 - } 130 - } 131 - } 132 - } 133 - 134 - func (s Set[T]) IsSubset(other Set[T]) bool { 135 - for item := range s.data { 136 - if !other.Contains(item) { 137 - return false 138 - } 139 - } 140 - return true 141 - } 142 - 143 - func (s Set[T]) IsSuperset(other Set[T]) bool { 144 - return other.IsSubset(s) 145 - } 146 - 147 - func (s Set[T]) IsDisjoint(other Set[T]) bool { 148 - for item := range s.data { 149 - if other.Contains(item) { 150 - return false 151 - } 152 - } 153 - return true 154 - } 155 - 156 - func (s Set[T]) Equal(other Set[T]) bool { 157 - if s.Len() != other.Len() { 158 - return false 159 - } 160 - for item := range s.data { 161 - if !other.Contains(item) { 162 - return false 163 - } 164 - } 165 - return true 166 - } 167 - 168 - func Collect[T comparable](seq iter.Seq[T]) Set[T] { 169 - result := New[T]() 170 - for item := range seq { 171 - result.Insert(item) 172 - } 173 - return result 174 - }
-411
sets/set_test.go
··· 1 - package sets 2 - 3 - import ( 4 - "slices" 5 - "testing" 6 - "testing/quick" 7 - ) 8 - 9 - func TestNew(t *testing.T) { 10 - s := New[int]() 11 - if s.Len() != 0 { 12 - t.Errorf("New set should be empty, got length %d", s.Len()) 13 - } 14 - if !s.IsEmpty() { 15 - t.Error("New set should be empty") 16 - } 17 - } 18 - 19 - func TestFromSlice(t *testing.T) { 20 - s := Collect(slices.Values([]int{1, 2, 3, 2, 1})) 21 - if s.Len() != 3 { 22 - t.Errorf("Expected length 3, got %d", s.Len()) 23 - } 24 - if !s.Contains(1) || !s.Contains(2) || !s.Contains(3) { 25 - t.Error("Set should contain all unique elements from slice") 26 - } 27 - } 28 - 29 - func TestInsert(t *testing.T) { 30 - s := New[string]() 31 - 32 - if !s.Insert("hello") { 33 - t.Error("First insert should return true") 34 - } 35 - if s.Insert("hello") { 36 - t.Error("Duplicate insert should return false") 37 - } 38 - if s.Len() != 1 { 39 - t.Errorf("Expected length 1, got %d", s.Len()) 40 - } 41 - } 42 - 43 - func TestRemove(t *testing.T) { 44 - s := Collect(slices.Values([]int{1, 2, 3})) 45 - 46 - if !s.Remove(2) { 47 - t.Error("Remove existing element should return true") 48 - } 49 - if s.Remove(2) { 50 - t.Error("Remove non-existing element should return false") 51 - } 52 - if s.Contains(2) { 53 - t.Error("Element should be removed") 54 - } 55 - if s.Len() != 2 { 56 - t.Errorf("Expected length 2, got %d", s.Len()) 57 - } 58 - } 59 - 60 - func TestContains(t *testing.T) { 61 - s := Collect(slices.Values([]int{1, 2, 3})) 62 - 63 - if !s.Contains(1) { 64 - t.Error("Should contain 1") 65 - } 66 - if s.Contains(4) { 67 - t.Error("Should not contain 4") 68 - } 69 - } 70 - 71 - func TestClear(t *testing.T) { 72 - s := Collect(slices.Values([]int{1, 2, 3})) 73 - s.Clear() 74 - 75 - if !s.IsEmpty() { 76 - t.Error("Set should be empty after clear") 77 - } 78 - if s.Len() != 0 { 79 - t.Errorf("Expected length 0, got %d", s.Len()) 80 - } 81 - } 82 - 83 - func TestIterator(t *testing.T) { 84 - s := Collect(slices.Values([]int{1, 2, 3})) 85 - var items []int 86 - 87 - for item := range s.All() { 88 - items = append(items, item) 89 - } 90 - 91 - slices.Sort(items) 92 - expected := []int{1, 2, 3} 93 - if !slices.Equal(items, expected) { 94 - t.Errorf("Expected %v, got %v", expected, items) 95 - } 96 - } 97 - 98 - func TestClone(t *testing.T) { 99 - s1 := Collect(slices.Values([]int{1, 2, 3})) 100 - s2 := s1.Clone() 101 - 102 - if !s1.Equal(s2) { 103 - t.Error("Cloned set should be equal to original") 104 - } 105 - 106 - s2.Insert(4) 107 - if s1.Contains(4) { 108 - t.Error("Modifying clone should not affect original") 109 - } 110 - } 111 - 112 - func TestUnion(t *testing.T) { 113 - s1 := Collect(slices.Values([]int{1, 2})) 114 - s2 := Collect(slices.Values([]int{2, 3})) 115 - 116 - result := Collect(s1.Union(s2)) 117 - expected := Collect(slices.Values([]int{1, 2, 3})) 118 - 119 - if !result.Equal(expected) { 120 - t.Errorf("Expected %v, got %v", expected, result) 121 - } 122 - } 123 - 124 - func TestIntersection(t *testing.T) { 125 - s1 := Collect(slices.Values([]int{1, 2, 3})) 126 - s2 := Collect(slices.Values([]int{2, 3, 4})) 127 - 128 - expected := Collect(slices.Values([]int{2, 3})) 129 - result := Collect(s1.Intersection(s2)) 130 - 131 - if !result.Equal(expected) { 132 - t.Errorf("Expected %v, got %v", expected, result) 133 - } 134 - } 135 - 136 - func TestDifference(t *testing.T) { 137 - s1 := Collect(slices.Values([]int{1, 2, 3})) 138 - s2 := Collect(slices.Values([]int{2, 3, 4})) 139 - 140 - expected := Collect(slices.Values([]int{1})) 141 - result := Collect(s1.Difference(s2)) 142 - 143 - if !result.Equal(expected) { 144 - t.Errorf("Expected %v, got %v", expected, result) 145 - } 146 - } 147 - 148 - func TestSymmetricDifference(t *testing.T) { 149 - s1 := Collect(slices.Values([]int{1, 2, 3})) 150 - s2 := Collect(slices.Values([]int{2, 3, 4})) 151 - 152 - expected := Collect(slices.Values([]int{1, 4})) 153 - result := Collect(s1.SymmetricDifference(s2)) 154 - 155 - if !result.Equal(expected) { 156 - t.Errorf("Expected %v, got %v", expected, result) 157 - } 158 - } 159 - 160 - func TestSymmetricDifferenceCommutativeProperty(t *testing.T) { 161 - s1 := Collect(slices.Values([]int{1, 2, 3})) 162 - s2 := Collect(slices.Values([]int{2, 3, 4})) 163 - 164 - result1 := Collect(s1.SymmetricDifference(s2)) 165 - result2 := Collect(s2.SymmetricDifference(s1)) 166 - 167 - if !result1.Equal(result2) { 168 - t.Errorf("Expected %v, got %v", result1, result2) 169 - } 170 - } 171 - 172 - func TestIsSubset(t *testing.T) { 173 - s1 := Collect(slices.Values([]int{1, 2})) 174 - s2 := Collect(slices.Values([]int{1, 2, 3})) 175 - 176 - if !s1.IsSubset(s2) { 177 - t.Error("s1 should be subset of s2") 178 - } 179 - if s2.IsSubset(s1) { 180 - t.Error("s2 should not be subset of s1") 181 - } 182 - } 183 - 184 - func TestIsSuperset(t *testing.T) { 185 - s1 := Collect(slices.Values([]int{1, 2, 3})) 186 - s2 := Collect(slices.Values([]int{1, 2})) 187 - 188 - if !s1.IsSuperset(s2) { 189 - t.Error("s1 should be superset of s2") 190 - } 191 - if s2.IsSuperset(s1) { 192 - t.Error("s2 should not be superset of s1") 193 - } 194 - } 195 - 196 - func TestIsDisjoint(t *testing.T) { 197 - s1 := Collect(slices.Values([]int{1, 2})) 198 - s2 := Collect(slices.Values([]int{3, 4})) 199 - s3 := Collect(slices.Values([]int{2, 3})) 200 - 201 - if !s1.IsDisjoint(s2) { 202 - t.Error("s1 and s2 should be disjoint") 203 - } 204 - if s1.IsDisjoint(s3) { 205 - t.Error("s1 and s3 should not be disjoint") 206 - } 207 - } 208 - 209 - func TestEqual(t *testing.T) { 210 - s1 := Collect(slices.Values([]int{1, 2, 3})) 211 - s2 := Collect(slices.Values([]int{3, 2, 1})) 212 - s3 := Collect(slices.Values([]int{1, 2})) 213 - 214 - if !s1.Equal(s2) { 215 - t.Error("s1 and s2 should be equal") 216 - } 217 - if s1.Equal(s3) { 218 - t.Error("s1 and s3 should not be equal") 219 - } 220 - } 221 - 222 - func TestCollect(t *testing.T) { 223 - s1 := Collect(slices.Values([]int{1, 2})) 224 - s2 := Collect(slices.Values([]int{2, 3})) 225 - 226 - unionSet := Collect(s1.Union(s2)) 227 - if unionSet.Len() != 3 { 228 - t.Errorf("Expected union set length 3, got %d", unionSet.Len()) 229 - } 230 - if !unionSet.Contains(1) || !unionSet.Contains(2) || !unionSet.Contains(3) { 231 - t.Error("Union set should contain 1, 2, and 3") 232 - } 233 - 234 - diffSet := Collect(s1.Difference(s2)) 235 - if diffSet.Len() != 1 { 236 - t.Errorf("Expected difference set length 1, got %d", diffSet.Len()) 237 - } 238 - if !diffSet.Contains(1) { 239 - t.Error("Difference set should contain 1") 240 - } 241 - } 242 - 243 - func TestPropertySingleonLen(t *testing.T) { 244 - f := func(item int) bool { 245 - single := Singleton(item) 246 - return single.Len() == 1 247 - } 248 - 249 - if err := quick.Check(f, nil); err != nil { 250 - t.Error(err) 251 - } 252 - } 253 - 254 - func TestPropertyInsertIdempotent(t *testing.T) { 255 - f := func(s Set[int], item int) bool { 256 - clone := s.Clone() 257 - 258 - clone.Insert(item) 259 - firstLen := clone.Len() 260 - 261 - clone.Insert(item) 262 - secondLen := clone.Len() 263 - 264 - return firstLen == secondLen 265 - } 266 - 267 - if err := quick.Check(f, nil); err != nil { 268 - t.Error(err) 269 - } 270 - } 271 - 272 - func TestPropertyUnionCommutative(t *testing.T) { 273 - f := func(s1 Set[int], s2 Set[int]) bool { 274 - union1 := Collect(s1.Union(s2)) 275 - union2 := Collect(s2.Union(s1)) 276 - return union1.Equal(union2) 277 - } 278 - 279 - if err := quick.Check(f, nil); err != nil { 280 - t.Error(err) 281 - } 282 - } 283 - 284 - func TestPropertyIntersectionCommutative(t *testing.T) { 285 - f := func(s1 Set[int], s2 Set[int]) bool { 286 - inter1 := Collect(s1.Intersection(s2)) 287 - inter2 := Collect(s2.Intersection(s1)) 288 - return inter1.Equal(inter2) 289 - } 290 - 291 - if err := quick.Check(f, nil); err != nil { 292 - t.Error(err) 293 - } 294 - } 295 - 296 - func TestPropertyCloneEquals(t *testing.T) { 297 - f := func(s Set[int]) bool { 298 - clone := s.Clone() 299 - return s.Equal(clone) 300 - } 301 - 302 - if err := quick.Check(f, nil); err != nil { 303 - t.Error(err) 304 - } 305 - } 306 - 307 - func TestPropertyIntersectionIsSubset(t *testing.T) { 308 - f := func(s1 Set[int], s2 Set[int]) bool { 309 - inter := Collect(s1.Intersection(s2)) 310 - return inter.IsSubset(s1) && inter.IsSubset(s2) 311 - } 312 - 313 - if err := quick.Check(f, nil); err != nil { 314 - t.Error(err) 315 - } 316 - } 317 - 318 - func TestPropertyUnionIsSuperset(t *testing.T) { 319 - f := func(s1 Set[int], s2 Set[int]) bool { 320 - union := Collect(s1.Union(s2)) 321 - return union.IsSuperset(s1) && union.IsSuperset(s2) 322 - } 323 - 324 - if err := quick.Check(f, nil); err != nil { 325 - t.Error(err) 326 - } 327 - } 328 - 329 - func TestPropertyDifferenceDisjoint(t *testing.T) { 330 - f := func(s1 Set[int], s2 Set[int]) bool { 331 - diff := Collect(s1.Difference(s2)) 332 - return diff.IsDisjoint(s2) 333 - } 334 - 335 - if err := quick.Check(f, nil); err != nil { 336 - t.Error(err) 337 - } 338 - } 339 - 340 - func TestPropertySymmetricDifferenceCommutative(t *testing.T) { 341 - f := func(s1 Set[int], s2 Set[int]) bool { 342 - symDiff1 := Collect(s1.SymmetricDifference(s2)) 343 - symDiff2 := Collect(s2.SymmetricDifference(s1)) 344 - return symDiff1.Equal(symDiff2) 345 - } 346 - 347 - if err := quick.Check(f, nil); err != nil { 348 - t.Error(err) 349 - } 350 - } 351 - 352 - func TestPropertyRemoveWorks(t *testing.T) { 353 - f := func(s Set[int], item int) bool { 354 - clone := s.Clone() 355 - clone.Insert(item) 356 - clone.Remove(item) 357 - return !clone.Contains(item) 358 - } 359 - 360 - if err := quick.Check(f, nil); err != nil { 361 - t.Error(err) 362 - } 363 - } 364 - 365 - func TestPropertyClearEmpty(t *testing.T) { 366 - f := func(s Set[int]) bool { 367 - s.Clear() 368 - return s.IsEmpty() && s.Len() == 0 369 - } 370 - 371 - if err := quick.Check(f, nil); err != nil { 372 - t.Error(err) 373 - } 374 - } 375 - 376 - func TestPropertyIsSubsetReflexive(t *testing.T) { 377 - f := func(s Set[int]) bool { 378 - return s.IsSubset(s) 379 - } 380 - 381 - if err := quick.Check(f, nil); err != nil { 382 - t.Error(err) 383 - } 384 - } 385 - 386 - func TestPropertyDeMorganUnion(t *testing.T) { 387 - f := func(s1 Set[int], s2 Set[int], universe Set[int]) bool { 388 - // create a universe that contains both sets 389 - u := universe.Clone() 390 - for item := range s1.All() { 391 - u.Insert(item) 392 - } 393 - for item := range s2.All() { 394 - u.Insert(item) 395 - } 396 - 397 - // (A u B)' = A' n B' 398 - union := Collect(s1.Union(s2)) 399 - complementUnion := Collect(u.Difference(union)) 400 - 401 - complementS1 := Collect(u.Difference(s1)) 402 - complementS2 := Collect(u.Difference(s2)) 403 - intersectionComplements := Collect(complementS1.Intersection(complementS2)) 404 - 405 - return complementUnion.Equal(intersectionComplements) 406 - } 407 - 408 - if err := quick.Check(f, nil); err != nil { 409 - t.Error(err) 410 - } 411 - }
+1 -6
types/commit.go
··· 174 174 175 175 func (commit Commit) CoAuthors() []object.Signature { 176 176 var coAuthors []object.Signature 177 - seen := make(map[string]bool) 177 + 178 178 matches := coAuthorRegex.FindAllStringSubmatch(commit.Message, -1) 179 179 180 180 for _, match := range matches { 181 181 if len(match) >= 3 { 182 182 name := strings.TrimSpace(match[1]) 183 183 email := strings.TrimSpace(match[2]) 184 - 185 - if seen[email] { 186 - continue 187 - } 188 - seen[email] = true 189 184 190 185 coAuthors = append(coAuthors, object.Signature{ 191 186 Name: name,