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

Compare changes

Choose any two refs to compare.

Changed files
+219 -827
appview
notify
db
pages
templates
state
knotserver
nix
sets
types
+57 -67
appview/notify/db/db.go
··· 3 import ( 4 "context" 5 "log" 6 "slices" 7 8 "github.com/bluesky-social/indigo/atproto/syntax" ··· 12 "tangled.org/core/appview/notify" 13 "tangled.org/core/idresolver" 14 "tangled.org/core/orm" 15 - "tangled.org/core/sets" 16 ) 17 18 const ( 19 - maxMentions = 8 20 ) 21 22 type databaseNotifier struct { ··· 50 } 51 52 actorDid := syntax.DID(star.Did) 53 - recipients := sets.Singleton(syntax.DID(repo.Did)) 54 eventType := models.NotificationTypeRepoStarred 55 entityType := "repo" 56 entityId := star.RepoAt.String() ··· 75 } 76 77 func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 78 collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt())) 79 if err != nil { 80 log.Printf("failed to fetch collaborators: %v", err) 81 return 82 } 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 for _, c := range collaborators { 90 - recipients.Insert(c.SubjectDid) 91 - } 92 - for _, m := range mentions { 93 - recipients.Remove(m) 94 } 95 96 actorDid := syntax.DID(issue.Did) ··· 112 ) 113 n.notifyEvent( 114 actorDid, 115 - sets.Collect(slices.Values(mentions)), 116 models.NotificationTypeUserMentioned, 117 entityType, 118 entityId, ··· 134 } 135 issue := issues[0] 136 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)) 143 144 if comment.IsReply() { 145 // if this comment is a reply, then notify everybody in that thread 146 parentAtUri := *comment.ReplyTo 147 148 // find the parent thread, and add all DIDs from here to the recipient list 149 - for _, t := range issue.CommentList() { 150 if t.Self.AtUri().String() == parentAtUri { 151 - for _, p := range t.Participants() { 152 - recipients.Insert(p) 153 - } 154 } 155 } 156 } else { 157 // 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) 163 } 164 165 actorDid := syntax.DID(comment.Did) ··· 181 ) 182 n.notifyEvent( 183 actorDid, 184 - sets.Collect(slices.Values(mentions)), 185 models.NotificationTypeUserMentioned, 186 entityType, 187 entityId, ··· 197 198 func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) { 199 actorDid := syntax.DID(follow.UserDid) 200 - recipients := sets.Singleton(syntax.DID(follow.SubjectDid)) 201 eventType := models.NotificationTypeFollowed 202 entityType := "follow" 203 entityId := follow.UserDid ··· 225 log.Printf("NewPull: failed to get repos: %v", err) 226 return 227 } 228 collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt())) 229 if err != nil { 230 log.Printf("failed to fetch collaborators: %v", err) 231 return 232 } 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 for _, c := range collaborators { 239 - recipients.Insert(c.SubjectDid) 240 } 241 242 actorDid := syntax.DID(pull.OwnerDid) ··· 279 // build up the recipients list: 280 // - repo owner 281 // - all pull participants 282 - // - remove those already mentioned 283 - recipients := sets.Singleton(syntax.DID(repo.Did)) 284 for _, p := range pull.Participants() { 285 - recipients.Insert(syntax.DID(p)) 286 - } 287 - for _, m := range mentions { 288 - recipients.Remove(m) 289 } 290 291 actorDid := syntax.DID(comment.OwnerDid) ··· 309 ) 310 n.notifyEvent( 311 actorDid, 312 - sets.Collect(slices.Values(mentions)), 313 models.NotificationTypeUserMentioned, 314 entityType, 315 entityId, ··· 336 } 337 338 func (n *databaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) { 339 collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt())) 340 if err != nil { 341 log.Printf("failed to fetch collaborators: %v", err) 342 return 343 } 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 for _, c := range collaborators { 351 - recipients.Insert(c.SubjectDid) 352 } 353 for _, p := range issue.Participants() { 354 - recipients.Insert(syntax.DID(p)) 355 } 356 357 entityType := "pull" ··· 387 return 388 } 389 390 collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt())) 391 if err != nil { 392 log.Printf("failed to fetch collaborators: %v", err) 393 return 394 } 395 - 396 - // build up the recipients list: 397 - // - repo owner 398 - // - all pull participants 399 - recipients := sets.Singleton(syntax.DID(repo.Did)) 400 for _, c := range collaborators { 401 - recipients.Insert(c.SubjectDid) 402 } 403 for _, p := range pull.Participants() { 404 - recipients.Insert(syntax.DID(p)) 405 } 406 407 entityType := "pull" ··· 437 438 func (n *databaseNotifier) notifyEvent( 439 actorDid syntax.DID, 440 - recipients sets.Set[syntax.DID], 441 eventType models.NotificationType, 442 entityType string, 443 entityId string, ··· 445 issueId *int64, 446 pullId *int64, 447 ) { 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 451 } 452 453 - recipients.Remove(actorDid) 454 - 455 prefMap, err := db.GetNotificationPreferences( 456 n.db, 457 - orm.FilterIn("user_did", slices.Collect(recipients.All())), 458 ) 459 if err != nil { 460 // failed to get prefs for users ··· 470 defer tx.Rollback() 471 472 // filter based on preferences 473 - for recipientDid := range recipients.All() { 474 prefs, ok := prefMap[recipientDid] 475 if !ok { 476 prefs = models.DefaultNotificationPreferences(recipientDid)
··· 3 import ( 4 "context" 5 "log" 6 + "maps" 7 "slices" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" ··· 13 "tangled.org/core/appview/notify" 14 "tangled.org/core/idresolver" 15 "tangled.org/core/orm" 16 ) 17 18 const ( 19 + maxMentions = 5 20 ) 21 22 type databaseNotifier struct { ··· 50 } 51 52 actorDid := syntax.DID(star.Did) 53 + recipients := []syntax.DID{syntax.DID(repo.Did)} 54 eventType := models.NotificationTypeRepoStarred 55 entityType := "repo" 56 entityId := star.RepoAt.String() ··· 75 } 76 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)) 84 collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt())) 85 if err != nil { 86 log.Printf("failed to fetch collaborators: %v", err) 87 return 88 } 89 for _, c := range collaborators { 90 + recipients = append(recipients, c.SubjectDid) 91 } 92 93 actorDid := syntax.DID(issue.Did) ··· 109 ) 110 n.notifyEvent( 111 actorDid, 112 + mentions, 113 models.NotificationTypeUserMentioned, 114 entityType, 115 entityId, ··· 131 } 132 issue := issues[0] 133 134 + var recipients []syntax.DID 135 + recipients = append(recipients, syntax.DID(issue.Repo.Did)) 136 137 if comment.IsReply() { 138 // if this comment is a reply, then notify everybody in that thread 139 parentAtUri := *comment.ReplyTo 140 + allThreads := issue.CommentList() 141 142 // find the parent thread, and add all DIDs from here to the recipient list 143 + for _, t := range allThreads { 144 if t.Self.AtUri().String() == parentAtUri { 145 + recipients = append(recipients, t.Participants()...) 146 } 147 } 148 } else { 149 // not a reply, notify just the issue author 150 + recipients = append(recipients, syntax.DID(issue.Did)) 151 } 152 153 actorDid := syntax.DID(comment.Did) ··· 169 ) 170 n.notifyEvent( 171 actorDid, 172 + mentions, 173 models.NotificationTypeUserMentioned, 174 entityType, 175 entityId, ··· 185 186 func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) { 187 actorDid := syntax.DID(follow.UserDid) 188 + recipients := []syntax.DID{syntax.DID(follow.SubjectDid)} 189 eventType := models.NotificationTypeFollowed 190 entityType := "follow" 191 entityId := follow.UserDid ··· 213 log.Printf("NewPull: failed to get repos: %v", err) 214 return 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)) 222 collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt())) 223 if err != nil { 224 log.Printf("failed to fetch collaborators: %v", err) 225 return 226 } 227 for _, c := range collaborators { 228 + recipients = append(recipients, c.SubjectDid) 229 } 230 231 actorDid := syntax.DID(pull.OwnerDid) ··· 268 // build up the recipients list: 269 // - repo owner 270 // - all pull participants 271 + var recipients []syntax.DID 272 + recipients = append(recipients, syntax.DID(repo.Did)) 273 for _, p := range pull.Participants() { 274 + recipients = append(recipients, syntax.DID(p)) 275 } 276 277 actorDid := syntax.DID(comment.OwnerDid) ··· 295 ) 296 n.notifyEvent( 297 actorDid, 298 + mentions, 299 models.NotificationTypeUserMentioned, 300 entityType, 301 entityId, ··· 322 } 323 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)) 331 collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt())) 332 if err != nil { 333 log.Printf("failed to fetch collaborators: %v", err) 334 return 335 } 336 for _, c := range collaborators { 337 + recipients = append(recipients, c.SubjectDid) 338 } 339 for _, p := range issue.Participants() { 340 + recipients = append(recipients, syntax.DID(p)) 341 } 342 343 entityType := "pull" ··· 373 return 374 } 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)) 381 collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt())) 382 if err != nil { 383 log.Printf("failed to fetch collaborators: %v", err) 384 return 385 } 386 for _, c := range collaborators { 387 + recipients = append(recipients, c.SubjectDid) 388 } 389 for _, p := range pull.Participants() { 390 + recipients = append(recipients, syntax.DID(p)) 391 } 392 393 entityType := "pull" ··· 423 424 func (n *databaseNotifier) notifyEvent( 425 actorDid syntax.DID, 426 + recipients []syntax.DID, 427 eventType models.NotificationType, 428 entityType string, 429 entityId string, ··· 431 issueId *int64, 432 pullId *int64, 433 ) { 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 + } 443 } 444 445 prefMap, err := db.GetNotificationPreferences( 446 n.db, 447 + orm.FilterIn("user_did", slices.Collect(maps.Keys(recipientSet))), 448 ) 449 if err != nil { 450 // failed to get prefs for users ··· 460 defer tx.Rollback() 461 462 // filter based on preferences 463 + for recipientDid := range recipientSet { 464 prefs, ok := prefMap[recipientDid] 465 if !ok { 466 prefs = models.DefaultNotificationPreferences(recipientDid)
+6 -9
appview/pages/templates/user/signup.html
··· 43 page to complete your registration. 44 </span> 45 <div class="w-full mt-4 text-center"> 46 - <div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}" data-size="flexible"></div> 47 </div> 48 <button class="btn text-base w-full my-2 mt-6" type="submit" id="signup-button" tabindex="7" > 49 <span>join now</span> 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> 54 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> 60 </main> 61 </body> 62 </html>
··· 43 page to complete your registration. 44 </span> 45 <div class="w-full mt-4 text-center"> 46 + <div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}"></div> 47 </div> 48 <button class="btn text-base w-full my-2 mt-6" type="submit" id="signup-button" tabindex="7" > 49 <span>join now</span> 50 </button> 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> 55 56 + <p id="signup-msg" class="error w-full"></p> 57 </main> 58 </body> 59 </html>
+17
appview/state/git_http.go
··· 25 26 } 27 28 func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) { 29 user, ok := r.Context().Value("resolvedId").(identity.Identity) 30 if !ok {
··· 25 26 } 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 + 45 func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) { 46 user, ok := r.Context().Value("resolvedId").(identity.Identity) 47 if !ok {
+1
appview/state/router.go
··· 101 102 // These routes get proxied to the knot 103 r.Get("/info/refs", s.InfoRefs) 104 r.Post("/git-upload-pack", s.UploadPack) 105 r.Post("/git-receive-pack", s.ReceivePack) 106
··· 101 102 // These routes get proxied to the knot 103 r.Get("/info/refs", s.InfoRefs) 104 + r.Post("/git-upload-archive", s.UploadArchive) 105 r.Post("/git-upload-pack", s.UploadPack) 106 r.Post("/git-receive-pack", s.ReceivePack) 107
+3 -3
flake.lock
··· 150 }, 151 "nixpkgs": { 152 "locked": { 153 - "lastModified": 1765186076, 154 - "narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=", 155 "owner": "nixos", 156 "repo": "nixpkgs", 157 - "rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8", 158 "type": "github" 159 }, 160 "original": {
··· 150 }, 151 "nixpkgs": { 152 "locked": { 153 + "lastModified": 1751984180, 154 + "narHash": "sha256-LwWRsENAZJKUdD3SpLluwDmdXY9F45ZEgCb0X+xgOL0=", 155 "owner": "nixos", 156 "repo": "nixpkgs", 157 + "rev": "9807714d6944a957c2e036f84b0ff8caf9930bc0", 158 "type": "github" 159 }, 160 "original": {
+2
flake.nix
··· 80 }).buildGoApplication; 81 modules = ./nix/gomod2nix.toml; 82 sqlite-lib = self.callPackage ./nix/pkgs/sqlite-lib.nix { 83 inherit sqlite-lib-src; 84 }; 85 lexgen = self.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;}; ··· 155 nativeBuildInputs = [ 156 pkgs.go 157 pkgs.air 158 pkgs.gopls 159 pkgs.httpie 160 pkgs.litecli
··· 80 }).buildGoApplication; 81 modules = ./nix/gomod2nix.toml; 82 sqlite-lib = self.callPackage ./nix/pkgs/sqlite-lib.nix { 83 + inherit (pkgs) gcc; 84 inherit sqlite-lib-src; 85 }; 86 lexgen = self.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;}; ··· 156 nativeBuildInputs = [ 157 pkgs.go 158 pkgs.air 159 + pkgs.tilt 160 pkgs.gopls 161 pkgs.httpie 162 pkgs.litecli
+1 -1
go.mod
··· 1 module tangled.org/core 2 3 - go 1.25.0 4 5 require ( 6 github.com/Blank-Xu/sql-adapter v1.1.1
··· 1 module tangled.org/core 2 3 + go 1.24.4 4 5 require ( 6 github.com/Blank-Xu/sql-adapter v1.1.1
-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 return c.RunService(cmd) 96 } 97 98 func (c *ServiceCommand) UploadPack() error { 99 cmd := exec.Command("git", []string{ 100 - "-c", "uploadpack.allowFilter=true", 101 "upload-pack", 102 "--stateless-rpc", 103 ".",
··· 95 return c.RunService(cmd) 96 } 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 + 111 func (c *ServiceCommand) UploadPack() error { 112 cmd := exec.Command("git", []string{ 113 "upload-pack", 114 "--stateless-rpc", 115 ".",
+47
knotserver/git.go
··· 56 } 57 } 58 59 func (h *Knot) UploadPack(w http.ResponseWriter, r *http.Request) { 60 did := chi.URLParam(r, "did") 61 name := chi.URLParam(r, "name")
··· 56 } 57 } 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 + 106 func (h *Knot) UploadPack(w http.ResponseWriter, r *http.Request) { 107 did := chi.URLParam(r, "did") 108 name := chi.URLParam(r, "name")
+1
knotserver/router.go
··· 82 r.Route("/{name}", func(r chi.Router) { 83 // routes for git operations 84 r.Get("/info/refs", h.InfoRefs) 85 r.Post("/git-upload-pack", h.UploadPack) 86 r.Post("/git-receive-pack", h.ReceivePack) 87 })
··· 82 r.Route("/{name}", func(r chi.Router) { 83 // routes for git operations 84 r.Get("/info/refs", h.InfoRefs) 85 + r.Post("/git-upload-archive", h.UploadArchive) 86 r.Post("/git-upload-pack", h.UploadPack) 87 r.Post("/git-receive-pack", h.ReceivePack) 88 })
+1 -1
knotserver/server.go
··· 64 logger.Info("running in dev mode, signature verification is disabled") 65 } 66 67 - db, err := db.Setup(ctx, c.Server.DBPath) 68 if err != nil { 69 return fmt.Errorf("failed to load db: %w", err) 70 }
··· 64 logger.Info("running in dev mode, signature verification is disabled") 65 } 66 67 + db, err := db.Setup(c.Server.DBPath) 68 if err != nil { 69 return fmt.Errorf("failed to load db: %w", err) 70 }
+5 -7
nix/pkgs/sqlite-lib.nix
··· 1 { 2 stdenv, 3 sqlite-lib-src, 4 }: 5 stdenv.mkDerivation { 6 name = "sqlite-lib"; 7 src = sqlite-lib-src; 8 - 9 buildPhase = '' 10 - $CC -c sqlite3.c 11 - $AR rcs libsqlite3.a sqlite3.o 12 - $RANLIB libsqlite3.a 13 - ''; 14 - 15 - installPhase = '' 16 mkdir -p $out/include $out/lib 17 cp *.h $out/include 18 cp libsqlite3.a $out/lib
··· 1 { 2 + gcc, 3 stdenv, 4 sqlite-lib-src, 5 }: 6 stdenv.mkDerivation { 7 name = "sqlite-lib"; 8 src = sqlite-lib-src; 9 + nativeBuildInputs = [gcc]; 10 buildPhase = '' 11 + gcc -c sqlite3.c 12 + ar rcs libsqlite3.a sqlite3.o 13 + ranlib libsqlite3.a 14 mkdir -p $out/include $out/lib 15 cp *.h $out/include 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 175 func (commit Commit) CoAuthors() []object.Signature { 176 var coAuthors []object.Signature 177 - seen := make(map[string]bool) 178 matches := coAuthorRegex.FindAllStringSubmatch(commit.Message, -1) 179 180 for _, match := range matches { 181 if len(match) >= 3 { 182 name := strings.TrimSpace(match[1]) 183 email := strings.TrimSpace(match[2]) 184 - 185 - if seen[email] { 186 - continue 187 - } 188 - seen[email] = true 189 190 coAuthors = append(coAuthors, object.Signature{ 191 Name: name,
··· 174 175 func (commit Commit) CoAuthors() []object.Signature { 176 var coAuthors []object.Signature 177 + 178 matches := coAuthorRegex.FindAllStringSubmatch(commit.Message, -1) 179 180 for _, match := range matches { 181 if len(match) >= 3 { 182 name := strings.TrimSpace(match[1]) 183 email := strings.TrimSpace(match[2]) 184 185 coAuthors = append(coAuthors, object.Signature{ 186 Name: name,