forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

Compare changes

Choose any two refs to compare.

+613 -353
+36 -28
appview/db/db.go
··· 4 "context" 5 "database/sql" 6 "fmt" 7 - "log" 8 "reflect" 9 "strings" 10 11 _ "github.com/mattn/go-sqlite3" 12 ) 13 14 type DB struct { 15 *sql.DB 16 } 17 18 type Execer interface { ··· 26 PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) 27 } 28 29 - func Make(dbPath string) (*DB, error) { 30 // https://github.com/mattn/go-sqlite3#connection-string 31 opts := []string{ 32 "_foreign_keys=1", ··· 35 "_auto_vacuum=incremental", 36 } 37 38 db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&")) 39 if err != nil { 40 return nil, err 41 } 42 - 43 - ctx := context.Background() 44 45 conn, err := db.Conn(ctx) 46 if err != nil { ··· 574 } 575 576 // run migrations 577 - runMigration(conn, "add-description-to-repos", func(tx *sql.Tx) error { 578 tx.Exec(` 579 alter table repos add column description text check (length(description) <= 200); 580 `) 581 return nil 582 }) 583 584 - runMigration(conn, "add-rkey-to-pubkeys", func(tx *sql.Tx) error { 585 // add unconstrained column 586 _, err := tx.Exec(` 587 alter table public_keys ··· 604 return nil 605 }) 606 607 - runMigration(conn, "add-rkey-to-comments", func(tx *sql.Tx) error { 608 _, err := tx.Exec(` 609 alter table comments drop column comment_at; 610 alter table comments add column rkey text; ··· 612 return err 613 }) 614 615 - runMigration(conn, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error { 616 _, err := tx.Exec(` 617 alter table comments add column deleted text; -- timestamp 618 alter table comments add column edited text; -- timestamp ··· 620 return err 621 }) 622 623 - runMigration(conn, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error { 624 _, err := tx.Exec(` 625 alter table pulls add column source_branch text; 626 alter table pulls add column source_repo_at text; ··· 629 return err 630 }) 631 632 - runMigration(conn, "add-source-to-repos", func(tx *sql.Tx) error { 633 _, err := tx.Exec(` 634 alter table repos add column source text; 635 `) ··· 641 // 642 // [0]: https://sqlite.org/pragma.html#pragma_foreign_keys 643 conn.ExecContext(ctx, "pragma foreign_keys = off;") 644 - runMigration(conn, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error { 645 _, err := tx.Exec(` 646 create table pulls_new ( 647 -- identifiers ··· 698 }) 699 conn.ExecContext(ctx, "pragma foreign_keys = on;") 700 701 - runMigration(conn, "add-spindle-to-repos", func(tx *sql.Tx) error { 702 tx.Exec(` 703 alter table repos add column spindle text; 704 `) ··· 708 // drop all knot secrets, add unique constraint to knots 709 // 710 // knots will henceforth use service auth for signed requests 711 - runMigration(conn, "no-more-secrets", func(tx *sql.Tx) error { 712 _, err := tx.Exec(` 713 create table registrations_new ( 714 id integer primary key autoincrement, ··· 731 }) 732 733 // recreate and add rkey + created columns with default constraint 734 - runMigration(conn, "rework-collaborators-table", func(tx *sql.Tx) error { 735 // create new table 736 // - repo_at instead of repo integer 737 // - rkey field ··· 785 return err 786 }) 787 788 - runMigration(conn, "add-rkey-to-issues", func(tx *sql.Tx) error { 789 _, err := tx.Exec(` 790 alter table issues add column rkey text not null default ''; 791 ··· 797 }) 798 799 // repurpose the read-only column to "needs-upgrade" 800 - runMigration(conn, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error { 801 _, err := tx.Exec(` 802 alter table registrations rename column read_only to needs_upgrade; 803 `) ··· 805 }) 806 807 // require all knots to upgrade after the release of total xrpc 808 - runMigration(conn, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error { 809 _, err := tx.Exec(` 810 update registrations set needs_upgrade = 1; 811 `) ··· 813 }) 814 815 // require all knots to upgrade after the release of total xrpc 816 - runMigration(conn, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error { 817 _, err := tx.Exec(` 818 alter table spindles add column needs_upgrade integer not null default 0; 819 `) ··· 831 // 832 // disable foreign-keys for the next migration 833 conn.ExecContext(ctx, "pragma foreign_keys = off;") 834 - runMigration(conn, "remove-issue-at-from-issues", func(tx *sql.Tx) error { 835 _, err := tx.Exec(` 836 create table if not exists issues_new ( 837 -- identifiers ··· 901 // - new columns 902 // * column "reply_to" which can be any other comment 903 // * column "at-uri" which is a generated column 904 - runMigration(conn, "rework-issue-comments", func(tx *sql.Tx) error { 905 _, err := tx.Exec(` 906 create table if not exists issue_comments ( 907 -- identifiers ··· 961 // 962 // disable foreign-keys for the next migration 963 conn.ExecContext(ctx, "pragma foreign_keys = off;") 964 - runMigration(conn, "add-at-uri-to-pulls", func(tx *sql.Tx) error { 965 _, err := tx.Exec(` 966 create table if not exists pulls_new ( 967 -- identifiers ··· 1042 // 1043 // disable foreign-keys for the next migration 1044 conn.ExecContext(ctx, "pragma foreign_keys = off;") 1045 - runMigration(conn, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error { 1046 _, err := tx.Exec(` 1047 create table if not exists pull_submissions_new ( 1048 -- identifiers ··· 1094 }) 1095 conn.ExecContext(ctx, "pragma foreign_keys = on;") 1096 1097 - return &DB{db}, nil 1098 } 1099 1100 type migrationFn = func(*sql.Tx) error 1101 1102 - func runMigration(c *sql.Conn, name string, migrationFn migrationFn) error { 1103 tx, err := c.BeginTx(context.Background(), nil) 1104 if err != nil { 1105 return err ··· 1116 // run migration 1117 err = migrationFn(tx) 1118 if err != nil { 1119 - log.Printf("Failed to run migration %s: %v", name, err) 1120 return err 1121 } 1122 1123 // mark migration as complete 1124 _, err = tx.Exec("insert into migrations (name) values (?)", name) 1125 if err != nil { 1126 - log.Printf("Failed to mark migration %s as complete: %v", name, err) 1127 return err 1128 } 1129 ··· 1132 return err 1133 } 1134 1135 - log.Printf("migration %s applied successfully", name) 1136 } else { 1137 - log.Printf("skipped migration %s, already applied", name) 1138 } 1139 1140 return nil
··· 4 "context" 5 "database/sql" 6 "fmt" 7 + "log/slog" 8 "reflect" 9 "strings" 10 11 _ "github.com/mattn/go-sqlite3" 12 + "tangled.org/core/log" 13 ) 14 15 type DB struct { 16 *sql.DB 17 + logger *slog.Logger 18 } 19 20 type Execer interface { ··· 28 PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) 29 } 30 31 + func Make(ctx context.Context, dbPath string) (*DB, error) { 32 // https://github.com/mattn/go-sqlite3#connection-string 33 opts := []string{ 34 "_foreign_keys=1", ··· 37 "_auto_vacuum=incremental", 38 } 39 40 + logger := log.FromContext(ctx) 41 + logger = log.SubLogger(logger, "db") 42 + 43 db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&")) 44 if err != nil { 45 return nil, err 46 } 47 48 conn, err := db.Conn(ctx) 49 if err != nil { ··· 577 } 578 579 // run migrations 580 + runMigration(conn, logger, "add-description-to-repos", func(tx *sql.Tx) error { 581 tx.Exec(` 582 alter table repos add column description text check (length(description) <= 200); 583 `) 584 return nil 585 }) 586 587 + runMigration(conn, logger, "add-rkey-to-pubkeys", func(tx *sql.Tx) error { 588 // add unconstrained column 589 _, err := tx.Exec(` 590 alter table public_keys ··· 607 return nil 608 }) 609 610 + runMigration(conn, logger, "add-rkey-to-comments", func(tx *sql.Tx) error { 611 _, err := tx.Exec(` 612 alter table comments drop column comment_at; 613 alter table comments add column rkey text; ··· 615 return err 616 }) 617 618 + runMigration(conn, logger, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error { 619 _, err := tx.Exec(` 620 alter table comments add column deleted text; -- timestamp 621 alter table comments add column edited text; -- timestamp ··· 623 return err 624 }) 625 626 + runMigration(conn, logger, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error { 627 _, err := tx.Exec(` 628 alter table pulls add column source_branch text; 629 alter table pulls add column source_repo_at text; ··· 632 return err 633 }) 634 635 + runMigration(conn, logger, "add-source-to-repos", func(tx *sql.Tx) error { 636 _, err := tx.Exec(` 637 alter table repos add column source text; 638 `) ··· 644 // 645 // [0]: https://sqlite.org/pragma.html#pragma_foreign_keys 646 conn.ExecContext(ctx, "pragma foreign_keys = off;") 647 + runMigration(conn, logger, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error { 648 _, err := tx.Exec(` 649 create table pulls_new ( 650 -- identifiers ··· 701 }) 702 conn.ExecContext(ctx, "pragma foreign_keys = on;") 703 704 + runMigration(conn, logger, "add-spindle-to-repos", func(tx *sql.Tx) error { 705 tx.Exec(` 706 alter table repos add column spindle text; 707 `) ··· 711 // drop all knot secrets, add unique constraint to knots 712 // 713 // knots will henceforth use service auth for signed requests 714 + runMigration(conn, logger, "no-more-secrets", func(tx *sql.Tx) error { 715 _, err := tx.Exec(` 716 create table registrations_new ( 717 id integer primary key autoincrement, ··· 734 }) 735 736 // recreate and add rkey + created columns with default constraint 737 + runMigration(conn, logger, "rework-collaborators-table", func(tx *sql.Tx) error { 738 // create new table 739 // - repo_at instead of repo integer 740 // - rkey field ··· 788 return err 789 }) 790 791 + runMigration(conn, logger, "add-rkey-to-issues", func(tx *sql.Tx) error { 792 _, err := tx.Exec(` 793 alter table issues add column rkey text not null default ''; 794 ··· 800 }) 801 802 // repurpose the read-only column to "needs-upgrade" 803 + runMigration(conn, logger, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error { 804 _, err := tx.Exec(` 805 alter table registrations rename column read_only to needs_upgrade; 806 `) ··· 808 }) 809 810 // require all knots to upgrade after the release of total xrpc 811 + runMigration(conn, logger, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error { 812 _, err := tx.Exec(` 813 update registrations set needs_upgrade = 1; 814 `) ··· 816 }) 817 818 // require all knots to upgrade after the release of total xrpc 819 + runMigration(conn, logger, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error { 820 _, err := tx.Exec(` 821 alter table spindles add column needs_upgrade integer not null default 0; 822 `) ··· 834 // 835 // disable foreign-keys for the next migration 836 conn.ExecContext(ctx, "pragma foreign_keys = off;") 837 + runMigration(conn, logger, "remove-issue-at-from-issues", func(tx *sql.Tx) error { 838 _, err := tx.Exec(` 839 create table if not exists issues_new ( 840 -- identifiers ··· 904 // - new columns 905 // * column "reply_to" which can be any other comment 906 // * column "at-uri" which is a generated column 907 + runMigration(conn, logger, "rework-issue-comments", func(tx *sql.Tx) error { 908 _, err := tx.Exec(` 909 create table if not exists issue_comments ( 910 -- identifiers ··· 964 // 965 // disable foreign-keys for the next migration 966 conn.ExecContext(ctx, "pragma foreign_keys = off;") 967 + runMigration(conn, logger, "add-at-uri-to-pulls", func(tx *sql.Tx) error { 968 _, err := tx.Exec(` 969 create table if not exists pulls_new ( 970 -- identifiers ··· 1045 // 1046 // disable foreign-keys for the next migration 1047 conn.ExecContext(ctx, "pragma foreign_keys = off;") 1048 + runMigration(conn, logger, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error { 1049 _, err := tx.Exec(` 1050 create table if not exists pull_submissions_new ( 1051 -- identifiers ··· 1097 }) 1098 conn.ExecContext(ctx, "pragma foreign_keys = on;") 1099 1100 + return &DB{ 1101 + db, 1102 + logger, 1103 + }, nil 1104 } 1105 1106 type migrationFn = func(*sql.Tx) error 1107 1108 + func runMigration(c *sql.Conn, logger *slog.Logger, name string, migrationFn migrationFn) error { 1109 + logger = logger.With("migration", name) 1110 + 1111 tx, err := c.BeginTx(context.Background(), nil) 1112 if err != nil { 1113 return err ··· 1124 // run migration 1125 err = migrationFn(tx) 1126 if err != nil { 1127 + logger.Error("failed to run migration", "err", err) 1128 return err 1129 } 1130 1131 // mark migration as complete 1132 _, err = tx.Exec("insert into migrations (name) values (?)", name) 1133 if err != nil { 1134 + logger.Error("failed to mark migration as complete", "err", err) 1135 return err 1136 } 1137 ··· 1140 return err 1141 } 1142 1143 + logger.Info("migration applied successfully") 1144 } else { 1145 + logger.Warn("skipped migration, already applied") 1146 } 1147 1148 return nil
+1 -1
appview/ingester.go
··· 89 } 90 91 if err != nil { 92 - l.Debug("error ingesting record", "err", err) 93 } 94 95 return nil
··· 89 } 90 91 if err != nil { 92 + l.Warn("refused to ingest record", "err", err) 93 } 94 95 return nil
+27 -26
appview/issues/issues.go
··· 5 "database/sql" 6 "errors" 7 "fmt" 8 - "log" 9 "log/slog" 10 "net/http" 11 "slices" ··· 28 "tangled.org/core/appview/reporesolver" 29 "tangled.org/core/appview/validator" 30 "tangled.org/core/idresolver" 31 - tlog "tangled.org/core/log" 32 "tangled.org/core/tid" 33 ) 34 ··· 53 config *config.Config, 54 notifier notify.Notifier, 55 validator *validator.Validator, 56 ) *Issues { 57 return &Issues{ 58 oauth: oauth, ··· 62 db: db, 63 config: config, 64 notifier: notifier, 65 - logger: tlog.New("issues"), 66 validator: validator, 67 } 68 } ··· 72 user := rp.oauth.GetUser(r) 73 f, err := rp.repoResolver.Resolve(r) 74 if err != nil { 75 - log.Println("failed to get repo and knot", err) 76 return 77 } 78 ··· 99 db.FilterContains("scope", tangled.RepoIssueNSID), 100 ) 101 if err != nil { 102 - log.Println("failed to fetch labels", err) 103 rp.pages.Error503(w) 104 return 105 } ··· 126 user := rp.oauth.GetUser(r) 127 f, err := rp.repoResolver.Resolve(r) 128 if err != nil { 129 - log.Println("failed to get repo and knot", err) 130 return 131 } 132 ··· 199 200 err = db.PutIssue(tx, newIssue) 201 if err != nil { 202 - log.Println("failed to edit issue", err) 203 rp.pages.Notice(w, "issues", "Failed to edit issue.") 204 return 205 } ··· 237 // delete from PDS 238 client, err := rp.oauth.AuthorizedClient(r) 239 if err != nil { 240 - log.Println("failed to get authorized client", err) 241 rp.pages.Notice(w, "issue-comment", "Failed to delete comment.") 242 return 243 } ··· 282 283 collaborators, err := f.Collaborators(r.Context()) 284 if err != nil { 285 - log.Println("failed to fetch repo collaborators: %w", err) 286 } 287 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool { 288 return user.Did == collab.Did ··· 296 db.FilterEq("id", issue.Id), 297 ) 298 if err != nil { 299 - log.Println("failed to close issue", err) 300 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 301 return 302 } ··· 307 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issue.IssueId)) 308 return 309 } else { 310 - log.Println("user is not permitted to close issue") 311 http.Error(w, "for biden", http.StatusUnauthorized) 312 return 313 } ··· 318 user := rp.oauth.GetUser(r) 319 f, err := rp.repoResolver.Resolve(r) 320 if err != nil { 321 - log.Println("failed to get repo and knot", err) 322 return 323 } 324 ··· 331 332 collaborators, err := f.Collaborators(r.Context()) 333 if err != nil { 334 - log.Println("failed to fetch repo collaborators: %w", err) 335 } 336 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool { 337 return user.Did == collab.Did ··· 344 db.FilterEq("id", issue.Id), 345 ) 346 if err != nil { 347 - log.Println("failed to reopen issue", err) 348 rp.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.") 349 return 350 } 351 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issue.IssueId)) 352 return 353 } else { 354 - log.Println("user is not the owner of the repo") 355 http.Error(w, "forbidden", http.StatusUnauthorized) 356 return 357 } ··· 538 newBody := r.FormValue("body") 539 client, err := rp.oauth.AuthorizedClient(r) 540 if err != nil { 541 - log.Println("failed to get authorized client", err) 542 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 543 return 544 } ··· 551 552 _, err = db.AddIssueComment(rp.db, newComment) 553 if err != nil { 554 - log.Println("failed to perferom update-description query", err) 555 rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 556 return 557 } ··· 561 // update the record on pds 562 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, comment.Rkey) 563 if err != nil { 564 - log.Println("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey) 565 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") 566 return 567 } ··· 729 if comment.Rkey != "" { 730 client, err := rp.oauth.AuthorizedClient(r) 731 if err != nil { 732 - log.Println("failed to get authorized client", err) 733 rp.pages.Notice(w, "issue-comment", "Failed to delete comment.") 734 return 735 } ··· 739 Rkey: comment.Rkey, 740 }) 741 if err != nil { 742 - log.Println(err) 743 } 744 } 745 ··· 757 } 758 759 func (rp *Issues) RepoIssues(w http.ResponseWriter, r *http.Request) { 760 params := r.URL.Query() 761 state := params.Get("state") 762 isOpen := true ··· 771 772 page, ok := r.Context().Value("page").(pagination.Page) 773 if !ok { 774 - log.Println("failed to get page") 775 page = pagination.FirstPage() 776 } 777 778 user := rp.oauth.GetUser(r) 779 f, err := rp.repoResolver.Resolve(r) 780 if err != nil { 781 - log.Println("failed to get repo and knot", err) 782 return 783 } 784 ··· 793 db.FilterEq("open", openVal), 794 ) 795 if err != nil { 796 - log.Println("failed to get issues", err) 797 rp.pages.Notice(w, "issues", "Failed to load issues. Try again later.") 798 return 799 } ··· 804 db.FilterContains("scope", tangled.RepoIssueNSID), 805 ) 806 if err != nil { 807 - log.Println("failed to fetch labels", err) 808 rp.pages.Error503(w) 809 return 810 } ··· 901 902 err = db.PutIssue(tx, issue) 903 if err != nil { 904 - log.Println("failed to create issue", err) 905 rp.pages.Notice(w, "issues", "Failed to create issue.") 906 return 907 } 908 909 if err = tx.Commit(); err != nil { 910 - log.Println("failed to create issue", err) 911 rp.pages.Notice(w, "issues", "Failed to create issue.") 912 return 913 }
··· 5 "database/sql" 6 "errors" 7 "fmt" 8 "log/slog" 9 "net/http" 10 "slices" ··· 27 "tangled.org/core/appview/reporesolver" 28 "tangled.org/core/appview/validator" 29 "tangled.org/core/idresolver" 30 "tangled.org/core/tid" 31 ) 32 ··· 51 config *config.Config, 52 notifier notify.Notifier, 53 validator *validator.Validator, 54 + logger *slog.Logger, 55 ) *Issues { 56 return &Issues{ 57 oauth: oauth, ··· 61 db: db, 62 config: config, 63 notifier: notifier, 64 + logger: logger, 65 validator: validator, 66 } 67 } ··· 71 user := rp.oauth.GetUser(r) 72 f, err := rp.repoResolver.Resolve(r) 73 if err != nil { 74 + l.Error("failed to get repo and knot", "err", err) 75 return 76 } 77 ··· 98 db.FilterContains("scope", tangled.RepoIssueNSID), 99 ) 100 if err != nil { 101 + l.Error("failed to fetch labels", "err", err) 102 rp.pages.Error503(w) 103 return 104 } ··· 125 user := rp.oauth.GetUser(r) 126 f, err := rp.repoResolver.Resolve(r) 127 if err != nil { 128 + l.Error("failed to get repo and knot", "err", err) 129 return 130 } 131 ··· 198 199 err = db.PutIssue(tx, newIssue) 200 if err != nil { 201 + l.Error("failed to edit issue", "err", err) 202 rp.pages.Notice(w, "issues", "Failed to edit issue.") 203 return 204 } ··· 236 // delete from PDS 237 client, err := rp.oauth.AuthorizedClient(r) 238 if err != nil { 239 + l.Error("failed to get authorized client", "err", err) 240 rp.pages.Notice(w, "issue-comment", "Failed to delete comment.") 241 return 242 } ··· 281 282 collaborators, err := f.Collaborators(r.Context()) 283 if err != nil { 284 + l.Error("failed to fetch repo collaborators", "err", err) 285 } 286 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool { 287 return user.Did == collab.Did ··· 295 db.FilterEq("id", issue.Id), 296 ) 297 if err != nil { 298 + l.Error("failed to close issue", "err", err) 299 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 300 return 301 } ··· 306 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issue.IssueId)) 307 return 308 } else { 309 + l.Error("user is not permitted to close issue") 310 http.Error(w, "for biden", http.StatusUnauthorized) 311 return 312 } ··· 317 user := rp.oauth.GetUser(r) 318 f, err := rp.repoResolver.Resolve(r) 319 if err != nil { 320 + l.Error("failed to get repo and knot", "err", err) 321 return 322 } 323 ··· 330 331 collaborators, err := f.Collaborators(r.Context()) 332 if err != nil { 333 + l.Error("failed to fetch repo collaborators", "err", err) 334 } 335 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool { 336 return user.Did == collab.Did ··· 343 db.FilterEq("id", issue.Id), 344 ) 345 if err != nil { 346 + l.Error("failed to reopen issue", "err", err) 347 rp.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.") 348 return 349 } 350 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issue.IssueId)) 351 return 352 } else { 353 + l.Error("user is not the owner of the repo") 354 http.Error(w, "forbidden", http.StatusUnauthorized) 355 return 356 } ··· 537 newBody := r.FormValue("body") 538 client, err := rp.oauth.AuthorizedClient(r) 539 if err != nil { 540 + l.Error("failed to get authorized client", "err", err) 541 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 542 return 543 } ··· 550 551 _, err = db.AddIssueComment(rp.db, newComment) 552 if err != nil { 553 + l.Error("failed to perferom update-description query", "err", err) 554 rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 555 return 556 } ··· 560 // update the record on pds 561 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, comment.Rkey) 562 if err != nil { 563 + l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey) 564 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") 565 return 566 } ··· 728 if comment.Rkey != "" { 729 client, err := rp.oauth.AuthorizedClient(r) 730 if err != nil { 731 + l.Error("failed to get authorized client", "err", err) 732 rp.pages.Notice(w, "issue-comment", "Failed to delete comment.") 733 return 734 } ··· 738 Rkey: comment.Rkey, 739 }) 740 if err != nil { 741 + l.Error("failed to delete from PDS", "err", err) 742 } 743 } 744 ··· 756 } 757 758 func (rp *Issues) RepoIssues(w http.ResponseWriter, r *http.Request) { 759 + l := rp.logger.With("handler", "RepoIssues") 760 + 761 params := r.URL.Query() 762 state := params.Get("state") 763 isOpen := true ··· 772 773 page, ok := r.Context().Value("page").(pagination.Page) 774 if !ok { 775 + l.Error("failed to get page") 776 page = pagination.FirstPage() 777 } 778 779 user := rp.oauth.GetUser(r) 780 f, err := rp.repoResolver.Resolve(r) 781 if err != nil { 782 + l.Error("failed to get repo and knot", "err", err) 783 return 784 } 785 ··· 794 db.FilterEq("open", openVal), 795 ) 796 if err != nil { 797 + l.Error("failed to get issues", "err", err) 798 rp.pages.Notice(w, "issues", "Failed to load issues. Try again later.") 799 return 800 } ··· 805 db.FilterContains("scope", tangled.RepoIssueNSID), 806 ) 807 if err != nil { 808 + l.Error("failed to fetch labels", "err", err) 809 rp.pages.Error503(w) 810 return 811 } ··· 902 903 err = db.PutIssue(tx, issue) 904 if err != nil { 905 + l.Error("failed to create issue", "err", err) 906 rp.pages.Notice(w, "issues", "Failed to create issue.") 907 return 908 } 909 910 if err = tx.Commit(); err != nil { 911 + l.Error("failed to create issue", "err", err) 912 rp.pages.Notice(w, "issues", "Failed to create issue.") 913 return 914 }
+1 -3
appview/labels/labels.go
··· 16 "tangled.org/core/appview/oauth" 17 "tangled.org/core/appview/pages" 18 "tangled.org/core/appview/validator" 19 - "tangled.org/core/log" 20 "tangled.org/core/rbac" 21 "tangled.org/core/tid" 22 ··· 42 db *db.DB, 43 validator *validator.Validator, 44 enforcer *rbac.Enforcer, 45 ) *Labels { 46 - logger := log.New("labels") 47 - 48 return &Labels{ 49 oauth: oauth, 50 pages: pages,
··· 16 "tangled.org/core/appview/oauth" 17 "tangled.org/core/appview/pages" 18 "tangled.org/core/appview/validator" 19 "tangled.org/core/rbac" 20 "tangled.org/core/tid" 21 ··· 41 db *db.DB, 42 validator *validator.Validator, 43 enforcer *rbac.Enforcer, 44 + logger *slog.Logger, 45 ) *Labels { 46 return &Labels{ 47 oauth: oauth, 48 pages: pages,
+15 -12
appview/notifications/notifications.go
··· 1 package notifications 2 3 import ( 4 - "log" 5 "net/http" 6 "strconv" 7 ··· 14 ) 15 16 type Notifications struct { 17 - db *db.DB 18 - oauth *oauth.OAuth 19 - pages *pages.Pages 20 } 21 22 - func New(database *db.DB, oauthHandler *oauth.OAuth, pagesHandler *pages.Pages) *Notifications { 23 return &Notifications{ 24 - db: database, 25 - oauth: oauthHandler, 26 - pages: pagesHandler, 27 } 28 } 29 ··· 44 } 45 46 func (n *Notifications) notificationsPage(w http.ResponseWriter, r *http.Request) { 47 user := n.oauth.GetUser(r) 48 49 page, ok := r.Context().Value("page").(pagination.Page) 50 if !ok { 51 - log.Println("failed to get page") 52 page = pagination.FirstPage() 53 } 54 ··· 57 db.FilterEq("recipient_did", user.Did), 58 ) 59 if err != nil { 60 - log.Println("failed to get total notifications:", err) 61 n.pages.Error500(w) 62 return 63 } ··· 68 db.FilterEq("recipient_did", user.Did), 69 ) 70 if err != nil { 71 - log.Println("failed to get notifications:", err) 72 n.pages.Error500(w) 73 return 74 } 75 76 err = n.db.MarkAllNotificationsRead(r.Context(), user.Did) 77 if err != nil { 78 - log.Println("failed to mark notifications as read:", err) 79 } 80 81 unreadCount := 0
··· 1 package notifications 2 3 import ( 4 + "log/slog" 5 "net/http" 6 "strconv" 7 ··· 14 ) 15 16 type Notifications struct { 17 + db *db.DB 18 + oauth *oauth.OAuth 19 + pages *pages.Pages 20 + logger *slog.Logger 21 } 22 23 + func New(database *db.DB, oauthHandler *oauth.OAuth, pagesHandler *pages.Pages, logger *slog.Logger) *Notifications { 24 return &Notifications{ 25 + db: database, 26 + oauth: oauthHandler, 27 + pages: pagesHandler, 28 + logger: logger, 29 } 30 } 31 ··· 46 } 47 48 func (n *Notifications) notificationsPage(w http.ResponseWriter, r *http.Request) { 49 + l := n.logger.With("handler", "notificationsPage") 50 user := n.oauth.GetUser(r) 51 52 page, ok := r.Context().Value("page").(pagination.Page) 53 if !ok { 54 + l.Error("failed to get page") 55 page = pagination.FirstPage() 56 } 57 ··· 60 db.FilterEq("recipient_did", user.Did), 61 ) 62 if err != nil { 63 + l.Error("failed to get total notifications", "err", err) 64 n.pages.Error500(w) 65 return 66 } ··· 71 db.FilterEq("recipient_did", user.Did), 72 ) 73 if err != nil { 74 + l.Error("failed to get notifications", "err", err) 75 n.pages.Error500(w) 76 return 77 } 78 79 err = n.db.MarkAllNotificationsRead(r.Context(), user.Did) 80 if err != nil { 81 + l.Error("failed to mark notifications as read", "err", err) 82 } 83 84 unreadCount := 0
+2 -2
appview/pages/pages.go
··· 54 logger *slog.Logger 55 } 56 57 - func NewPages(config *config.Config, res *idresolver.Resolver) *Pages { 58 // initialized with safe defaults, can be overriden per use 59 rctx := &markup.RenderContext{ 60 IsDev: config.Core.Dev, ··· 72 rctx: rctx, 73 resolver: res, 74 templateDir: "appview/pages", 75 - logger: slog.Default().With("component", "pages"), 76 } 77 78 if p.dev {
··· 54 logger *slog.Logger 55 } 56 57 + func NewPages(config *config.Config, res *idresolver.Resolver, logger *slog.Logger) *Pages { 58 // initialized with safe defaults, can be overriden per use 59 rctx := &markup.RenderContext{ 60 IsDev: config.Core.Dev, ··· 72 rctx: rctx, 73 resolver: res, 74 templateDir: "appview/pages", 75 + logger: logger, 76 } 77 78 if p.dev {
+1 -1
appview/pages/templates/layouts/fragments/topbar.html
··· 1 {{ define "layouts/fragments/topbar" }} 2 - <nav class="mx-auto space-x-4 px-6 py-2 dark:text-white drop-shadow-sm bg-white dark:bg-gray-800"> 3 <div class="flex justify-between p-0 items-center"> 4 <div id="left-items"> 5 <a href="/" hx-boost="true" class="text-2xl no-underline hover:no-underline flex items-center gap-2">
··· 1 {{ define "layouts/fragments/topbar" }} 2 + <nav class="mx-auto space-x-4 px-6 py-2 rounded-b dark:text-white drop-shadow-sm bg-white dark:bg-gray-800"> 3 <div class="flex justify-between p-0 items-center"> 4 <div id="left-items"> 5 <a href="/" hx-boost="true" class="text-2xl no-underline hover:no-underline flex items-center gap-2">
+1 -3
appview/pipelines/pipelines.go
··· 16 "tangled.org/core/appview/reporesolver" 17 "tangled.org/core/eventconsumer" 18 "tangled.org/core/idresolver" 19 - "tangled.org/core/log" 20 "tangled.org/core/rbac" 21 spindlemodel "tangled.org/core/spindle/models" 22 ··· 45 db *db.DB, 46 config *config.Config, 47 enforcer *rbac.Enforcer, 48 ) *Pipelines { 49 - logger := log.New("pipelines") 50 - 51 return &Pipelines{ 52 oauth: oauth, 53 repoResolver: repoResolver,
··· 16 "tangled.org/core/appview/reporesolver" 17 "tangled.org/core/eventconsumer" 18 "tangled.org/core/idresolver" 19 "tangled.org/core/rbac" 20 spindlemodel "tangled.org/core/spindle/models" 21 ··· 44 db *db.DB, 45 config *config.Config, 46 enforcer *rbac.Enforcer, 47 + logger *slog.Logger, 48 ) *Pipelines { 49 return &Pipelines{ 50 oauth: oauth, 51 repoResolver: repoResolver,
+4 -5
appview/pulls/pulls.go
··· 6 "errors" 7 "fmt" 8 "log" 9 "net/http" 10 "slices" 11 "sort" ··· 46 config *config.Config 47 notifier notify.Notifier 48 enforcer *rbac.Enforcer 49 } 50 51 func New( ··· 57 config *config.Config, 58 notifier notify.Notifier, 59 enforcer *rbac.Enforcer, 60 ) *Pulls { 61 return &Pulls{ 62 oauth: oauth, ··· 67 config: config, 68 notifier: notifier, 69 enforcer: enforcer, 70 } 71 } 72 ··· 331 branch = pull.PullSource.Branch 332 repo = pull.PullSource.Repo 333 } else { 334 - return nil 335 - } 336 - 337 - // deleted fork 338 - if repo == nil { 339 return nil 340 } 341
··· 6 "errors" 7 "fmt" 8 "log" 9 + "log/slog" 10 "net/http" 11 "slices" 12 "sort" ··· 47 config *config.Config 48 notifier notify.Notifier 49 enforcer *rbac.Enforcer 50 + logger *slog.Logger 51 } 52 53 func New( ··· 59 config *config.Config, 60 notifier notify.Notifier, 61 enforcer *rbac.Enforcer, 62 + logger *slog.Logger, 63 ) *Pulls { 64 return &Pulls{ 65 oauth: oauth, ··· 70 config: config, 71 notifier: notifier, 72 enforcer: enforcer, 73 + logger: logger, 74 } 75 } 76 ··· 335 branch = pull.PullSource.Branch 336 repo = pull.PullSource.Repo 337 } else { 338 return nil 339 } 340
+14 -11
appview/repo/index.go
··· 3 import ( 4 "errors" 5 "fmt" 6 - "log" 7 "net/http" 8 "net/url" 9 "slices" ··· 31 ) 32 33 func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) { 34 ref := chi.URLParam(r, "ref") 35 ref, _ = url.PathUnescape(ref) 36 37 f, err := rp.repoResolver.Resolve(r) 38 if err != nil { 39 - log.Println("failed to fully resolve repo", err) 40 return 41 } 42 ··· 56 result, err := rp.buildIndexResponse(r.Context(), xrpcc, f, ref) 57 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 58 if errors.Is(xrpcerr, xrpcclient.ErrXrpcUnsupported) { 59 - log.Println("failed to call XRPC repo.index", err) 60 rp.pages.RepoIndexPage(w, pages.RepoIndexParams{ 61 LoggedInUser: user, 62 NeedsKnotUpgrade: true, ··· 66 } 67 68 rp.pages.Error503(w) 69 - log.Println("failed to build index response", err) 70 return 71 } 72 ··· 119 emails := uniqueEmails(commitsTrunc) 120 emailToDidMap, err := db.GetEmailToDid(rp.db, emails, true) 121 if err != nil { 122 - log.Println("failed to get email to did map", err) 123 } 124 125 vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, commitsTrunc) 126 if err != nil { 127 - log.Println(err) 128 } 129 130 // TODO: a bit dirty 131 - languageInfo, err := rp.getLanguageInfo(r.Context(), f, xrpcc, result.Ref, ref == "") 132 if err != nil { 133 - log.Printf("failed to compute language percentages: %s", err) 134 // non-fatal 135 } 136 ··· 140 } 141 pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas) 142 if err != nil { 143 - log.Printf("failed to fetch pipeline statuses: %s", err) 144 // non-fatal 145 } 146 ··· 162 163 func (rp *Repo) getLanguageInfo( 164 ctx context.Context, 165 f *reporesolver.ResolvedRepo, 166 xrpcc *indigoxrpc.Client, 167 currentRef string, ··· 180 ls, err := tangled.RepoLanguages(ctx, xrpcc, currentRef, repo) 181 if err != nil { 182 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 183 - log.Println("failed to call XRPC repo.languages", xrpcerr) 184 return nil, xrpcerr 185 } 186 return nil, err ··· 210 err = db.UpdateRepoLanguages(tx, f.RepoAt(), currentRef, langs) 211 if err != nil { 212 // non-fatal 213 - log.Println("failed to cache lang results", err) 214 } 215 216 err = tx.Commit()
··· 3 import ( 4 "errors" 5 "fmt" 6 + "log/slog" 7 "net/http" 8 "net/url" 9 "slices" ··· 31 ) 32 33 func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) { 34 + l := rp.logger.With("handler", "RepoIndex") 35 + 36 ref := chi.URLParam(r, "ref") 37 ref, _ = url.PathUnescape(ref) 38 39 f, err := rp.repoResolver.Resolve(r) 40 if err != nil { 41 + l.Error("failed to fully resolve repo", "err", err) 42 return 43 } 44 ··· 58 result, err := rp.buildIndexResponse(r.Context(), xrpcc, f, ref) 59 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 60 if errors.Is(xrpcerr, xrpcclient.ErrXrpcUnsupported) { 61 + l.Error("failed to call XRPC repo.index", "err", err) 62 rp.pages.RepoIndexPage(w, pages.RepoIndexParams{ 63 LoggedInUser: user, 64 NeedsKnotUpgrade: true, ··· 68 } 69 70 rp.pages.Error503(w) 71 + l.Error("failed to build index response", "err", err) 72 return 73 } 74 ··· 121 emails := uniqueEmails(commitsTrunc) 122 emailToDidMap, err := db.GetEmailToDid(rp.db, emails, true) 123 if err != nil { 124 + l.Error("failed to get email to did map", "err", err) 125 } 126 127 vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, commitsTrunc) 128 if err != nil { 129 + l.Error("failed to GetVerifiedObjectCommits", "err", err) 130 } 131 132 // TODO: a bit dirty 133 + languageInfo, err := rp.getLanguageInfo(r.Context(), l, f, xrpcc, result.Ref, ref == "") 134 if err != nil { 135 + l.Warn("failed to compute language percentages", "err", err) 136 // non-fatal 137 } 138 ··· 142 } 143 pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas) 144 if err != nil { 145 + l.Error("failed to fetch pipeline statuses", "err", err) 146 // non-fatal 147 } 148 ··· 164 165 func (rp *Repo) getLanguageInfo( 166 ctx context.Context, 167 + l *slog.Logger, 168 f *reporesolver.ResolvedRepo, 169 xrpcc *indigoxrpc.Client, 170 currentRef string, ··· 183 ls, err := tangled.RepoLanguages(ctx, xrpcc, currentRef, repo) 184 if err != nil { 185 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 186 + l.Error("failed to call XRPC repo.languages", "err", xrpcerr) 187 return nil, xrpcerr 188 } 189 return nil, err ··· 213 err = db.UpdateRepoLanguages(tx, f.RepoAt(), currentRef, langs) 214 if err != nil { 215 // non-fatal 216 + l.Error("failed to cache lang results", "err", err) 217 } 218 219 err = tx.Commit()
+1 -1
appview/repo/ogcard/card.go
··· 46 47 // DefaultSize returns the default size for a card 48 func DefaultSize() (int, int) { 49 - return 1200, 630 50 } 51 52 // NewCard creates a new card with the given dimensions in pixels
··· 46 47 // DefaultSize returns the default size for a card 48 func DefaultSize() (int, int) { 49 + return 1200, 600 50 } 51 52 // NewCard creates a new card with the given dimensions in pixels
+28 -54
appview/repo/opengraph.go
··· 35 // Split content horizontally: main content (80%) and avatar area (20%) 36 mainContent, avatarArea := contentCard.Split(true, 80) 37 38 - // Use main content area for both repo name and description to allow dynamic wrapping. 39 - mainContent.SetMargin(10) 40 41 var ownerHandle string 42 owner, err := rp.idResolver.ResolveIdent(context.Background(), repo.Did) 43 if err != nil { ··· 46 ownerHandle = "@" + owner.Handle.String() 47 } 48 49 - bounds := mainContent.Img.Bounds() 50 - startX := bounds.Min.X + mainContent.Margin 51 - startY := bounds.Min.Y + mainContent.Margin 52 currentX := startX 53 - currentY := startY 54 - lineHeight := 64 // Font size 54 + padding 55 textColor := color.RGBA{88, 96, 105, 255} 56 57 - // Draw owner handle 58 - ownerWidth, err := mainContent.DrawTextAtWithWidth(ownerHandle, currentX, currentY, textColor, 54, ogcard.Top, ogcard.Left) 59 if err != nil { 60 return nil, err 61 } 62 currentX += ownerWidth 63 64 // Draw separator 65 - sepWidth, err := mainContent.DrawTextAtWithWidth(" / ", currentX, currentY, textColor, 54, ogcard.Top, ogcard.Left) 66 if err != nil { 67 return nil, err 68 } 69 currentX += sepWidth 70 71 - words := strings.Fields(repo.Name) 72 - spaceWidth, _ := mainContent.DrawTextAtWithWidth(" ", -1000, -1000, color.Black, 54, ogcard.Top, ogcard.Left) 73 - if spaceWidth == 0 { 74 - spaceWidth = 15 75 } 76 77 - for _, word := range words { 78 - // estimate bold width by measuring regular width and adding a multiplier 79 - regularWidth, _ := mainContent.DrawTextAtWithWidth(word, -1000, -1000, color.Black, 54, ogcard.Top, ogcard.Left) 80 - estimatedBoldWidth := int(float64(regularWidth) * 1.15) // Heuristic for bold text 81 - 82 - if currentX+estimatedBoldWidth > (bounds.Max.X - mainContent.Margin) { 83 - currentX = startX 84 - currentY += lineHeight 85 - } 86 - 87 - _, err := mainContent.DrawBoldText(word, currentX, currentY, color.Black, 54, ogcard.Top, ogcard.Left) 88 - if err != nil { 89 - return nil, err 90 - } 91 - currentX += estimatedBoldWidth + spaceWidth 92 } 93 94 - // update Y position for the description 95 - currentY += lineHeight 96 - 97 - // draw description 98 - if currentY < bounds.Max.Y-mainContent.Margin { 99 - totalHeight := float64(bounds.Dy()) 100 - repoNameHeight := float64(currentY - bounds.Min.Y) 101 - 102 - if totalHeight > 0 && repoNameHeight < totalHeight { 103 - repoNamePercent := (repoNameHeight / totalHeight) * 100 104 - if repoNamePercent < 95 { // Ensure there's space left for description 105 - _, descriptionCard := mainContent.Split(false, int(repoNamePercent)) 106 - descriptionCard.SetMargin(8) 107 - 108 - description := repo.Description 109 - if len(description) > 70 { 110 - description = description[:70] + "…" 111 - } 112 - 113 - _, err = descriptionCard.DrawText(description, color.RGBA{88, 96, 105, 255}, 36, ogcard.Top, ogcard.Left) 114 - if err != nil { 115 - log.Printf("failed to draw description: %v", err) 116 - } 117 - } 118 - } 119 } 120 121 // Draw avatar circle on the right side
··· 35 // Split content horizontally: main content (80%) and avatar area (20%) 36 mainContent, avatarArea := contentCard.Split(true, 80) 37 38 + // Split main content: 50% for name/description, 50% for spacing 39 + topSection, _ := mainContent.Split(false, 50) 40 + 41 + // Split top section: 40% for repo name, 60% for description 42 + repoNameCard, descriptionCard := topSection.Split(false, 50) 43 44 + // Draw repo name with owner in regular and repo name in bold 45 + repoNameCard.SetMargin(10) 46 var ownerHandle string 47 owner, err := rp.idResolver.ResolveIdent(context.Background(), repo.Did) 48 if err != nil { ··· 51 ownerHandle = "@" + owner.Handle.String() 52 } 53 54 + // Draw repo name with wrapping support 55 + repoNameCard.SetMargin(10) 56 + bounds := repoNameCard.Img.Bounds() 57 + startX := bounds.Min.X + repoNameCard.Margin 58 + startY := bounds.Min.Y + repoNameCard.Margin 59 currentX := startX 60 textColor := color.RGBA{88, 96, 105, 255} 61 62 + // Draw owner handle in gray 63 + ownerWidth, err := repoNameCard.DrawTextAtWithWidth(ownerHandle, currentX, startY, textColor, 54, ogcard.Top, ogcard.Left) 64 if err != nil { 65 return nil, err 66 } 67 currentX += ownerWidth 68 69 // Draw separator 70 + sepWidth, err := repoNameCard.DrawTextAtWithWidth(" / ", currentX, startY, textColor, 54, ogcard.Top, ogcard.Left) 71 if err != nil { 72 return nil, err 73 } 74 currentX += sepWidth 75 76 + // Draw repo name in bold 77 + _, err = repoNameCard.DrawBoldText(repo.Name, currentX, startY, color.Black, 54, ogcard.Top, ogcard.Left) 78 + if err != nil { 79 + return nil, err 80 } 81 82 + // Draw description (DrawText handles multi-line wrapping automatically) 83 + descriptionCard.SetMargin(10) 84 + description := repo.Description 85 + if len(description) > 70 { 86 + description = description[:70] + "…" 87 } 88 89 + _, err = descriptionCard.DrawText(description, color.RGBA{88, 96, 105, 255}, 36, ogcard.Top, ogcard.Left) 90 + if err != nil { 91 + log.Printf("failed to draw description: %v", err) 92 + return nil, err 93 } 94 95 // Draw avatar circle on the right side
+130 -92
appview/repo/repo.go
··· 7 "errors" 8 "fmt" 9 "io" 10 - "log" 11 "log/slog" 12 "net/http" 13 "net/url" ··· 90 } 91 92 func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) { 93 ref := chi.URLParam(r, "ref") 94 ref, _ = url.PathUnescape(ref) 95 96 f, err := rp.repoResolver.Resolve(r) 97 if err != nil { 98 - log.Println("failed to get repo and knot", err) 99 return 100 } 101 ··· 111 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 112 archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo) 113 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 114 - log.Println("failed to call XRPC repo.archive", xrpcerr) 115 rp.pages.Error503(w) 116 return 117 } ··· 128 } 129 130 func (rp *Repo) RepoLog(w http.ResponseWriter, r *http.Request) { 131 f, err := rp.repoResolver.Resolve(r) 132 if err != nil { 133 - log.Println("failed to fully resolve repo", err) 134 return 135 } 136 ··· 165 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 166 xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo) 167 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 168 - log.Println("failed to call XRPC repo.log", xrpcerr) 169 rp.pages.Error503(w) 170 return 171 } 172 173 var xrpcResp types.RepoLogResponse 174 if err := json.Unmarshal(xrpcBytes, &xrpcResp); err != nil { 175 - log.Println("failed to decode XRPC response", err) 176 rp.pages.Error503(w) 177 return 178 } 179 180 tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 181 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 182 - log.Println("failed to call XRPC repo.tags", xrpcerr) 183 rp.pages.Error503(w) 184 return 185 } ··· 196 197 branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 198 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 199 - log.Println("failed to call XRPC repo.branches", xrpcerr) 200 rp.pages.Error503(w) 201 return 202 } ··· 214 215 emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(xrpcResp.Commits), true) 216 if err != nil { 217 - log.Println("failed to fetch email to did mapping", err) 218 } 219 220 vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, xrpcResp.Commits) 221 if err != nil { 222 - log.Println(err) 223 } 224 225 repoInfo := f.RepoInfo(user) ··· 230 } 231 pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas) 232 if err != nil { 233 - log.Println(err) 234 // non-fatal 235 } 236 ··· 246 } 247 248 func (rp *Repo) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) { 249 f, err := rp.repoResolver.Resolve(r) 250 if err != nil { 251 - log.Println("failed to get repo and knot", err) 252 w.WriteHeader(http.StatusBadRequest) 253 return 254 } ··· 260 } 261 262 func (rp *Repo) RepoDescription(w http.ResponseWriter, r *http.Request) { 263 f, err := rp.repoResolver.Resolve(r) 264 if err != nil { 265 - log.Println("failed to get repo and knot", err) 266 w.WriteHeader(http.StatusBadRequest) 267 return 268 } ··· 270 repoAt := f.RepoAt() 271 rkey := repoAt.RecordKey().String() 272 if rkey == "" { 273 - log.Println("invalid aturi for repo", err) 274 w.WriteHeader(http.StatusInternalServerError) 275 return 276 } ··· 287 newDescription := r.FormValue("description") 288 client, err := rp.oauth.AuthorizedClient(r) 289 if err != nil { 290 - log.Println("failed to get client") 291 rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 292 return 293 } ··· 295 // optimistic update 296 err = db.UpdateDescription(rp.db, string(repoAt), newDescription) 297 if err != nil { 298 - log.Println("failed to perferom update-description query", err) 299 rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 300 return 301 } ··· 324 }) 325 326 if err != nil { 327 - log.Println("failed to perferom update-description query", err) 328 // failed to get record 329 rp.pages.Notice(w, "repo-notice", "Failed to update description, unable to save to PDS.") 330 return ··· 341 } 342 343 func (rp *Repo) RepoCommit(w http.ResponseWriter, r *http.Request) { 344 f, err := rp.repoResolver.Resolve(r) 345 if err != nil { 346 - log.Println("failed to fully resolve repo", err) 347 return 348 } 349 ref := chi.URLParam(r, "ref") ··· 371 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 372 xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo) 373 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 374 - log.Println("failed to call XRPC repo.diff", xrpcerr) 375 rp.pages.Error503(w) 376 return 377 } 378 379 var result types.RepoCommitResponse 380 if err := json.Unmarshal(xrpcBytes, &result); err != nil { 381 - log.Println("failed to decode XRPC response", err) 382 rp.pages.Error503(w) 383 return 384 } 385 386 emailToDidMap, err := db.GetEmailToDid(rp.db, []string{result.Diff.Commit.Committer.Email, result.Diff.Commit.Author.Email}, true) 387 if err != nil { 388 - log.Println("failed to get email to did mapping:", err) 389 } 390 391 vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.NiceDiff{*result.Diff}) 392 if err != nil { 393 - log.Println(err) 394 } 395 396 user := rp.oauth.GetUser(r) 397 repoInfo := f.RepoInfo(user) 398 pipelines, err := getPipelineStatuses(rp.db, repoInfo, []string{result.Diff.Commit.This}) 399 if err != nil { 400 - log.Println(err) 401 // non-fatal 402 } 403 var pipeline *models.Pipeline ··· 417 } 418 419 func (rp *Repo) RepoTree(w http.ResponseWriter, r *http.Request) { 420 f, err := rp.repoResolver.Resolve(r) 421 if err != nil { 422 - log.Println("failed to fully resolve repo", err) 423 return 424 } 425 ··· 444 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 445 xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo) 446 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 447 - log.Println("failed to call XRPC repo.tree", xrpcerr) 448 rp.pages.Error503(w) 449 return 450 } ··· 519 } 520 521 func (rp *Repo) RepoTags(w http.ResponseWriter, r *http.Request) { 522 f, err := rp.repoResolver.Resolve(r) 523 if err != nil { 524 - log.Println("failed to get repo and knot", err) 525 return 526 } 527 ··· 537 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 538 xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 539 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 540 - log.Println("failed to call XRPC repo.tags", xrpcerr) 541 rp.pages.Error503(w) 542 return 543 } 544 545 var result types.RepoTagsResponse 546 if err := json.Unmarshal(xrpcBytes, &result); err != nil { 547 - log.Println("failed to decode XRPC response", err) 548 rp.pages.Error503(w) 549 return 550 } 551 552 artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt())) 553 if err != nil { 554 - log.Println("failed grab artifacts", err) 555 return 556 } 557 ··· 588 } 589 590 func (rp *Repo) RepoBranches(w http.ResponseWriter, r *http.Request) { 591 f, err := rp.repoResolver.Resolve(r) 592 if err != nil { 593 - log.Println("failed to get repo and knot", err) 594 return 595 } 596 ··· 606 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 607 xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 608 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 609 - log.Println("failed to call XRPC repo.branches", xrpcerr) 610 rp.pages.Error503(w) 611 return 612 } 613 614 var result types.RepoBranchesResponse 615 if err := json.Unmarshal(xrpcBytes, &result); err != nil { 616 - log.Println("failed to decode XRPC response", err) 617 rp.pages.Error503(w) 618 return 619 } ··· 629 } 630 631 func (rp *Repo) DeleteBranch(w http.ResponseWriter, r *http.Request) { 632 f, err := rp.repoResolver.Resolve(r) 633 if err != nil { 634 - log.Println("failed to get repo and knot", err) 635 return 636 } 637 638 noticeId := "delete-branch-error" 639 fail := func(msg string, err error) { 640 - log.Println(msg, "err", err) 641 rp.pages.Notice(w, noticeId, msg) 642 } 643 ··· 670 fail(fmt.Sprintf("Failed to delete branch: %s", err), err) 671 return 672 } 673 - log.Println("deleted branch from knot", "branch", branch, "repo", f.RepoAt()) 674 675 rp.pages.HxRefresh(w) 676 } 677 678 func (rp *Repo) RepoBlob(w http.ResponseWriter, r *http.Request) { 679 f, err := rp.repoResolver.Resolve(r) 680 if err != nil { 681 - log.Println("failed to get repo and knot", err) 682 return 683 } 684 ··· 700 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name) 701 resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo) 702 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 703 - log.Println("failed to call XRPC repo.blob", xrpcerr) 704 rp.pages.Error503(w) 705 return 706 } ··· 800 } 801 802 func (rp *Repo) RepoBlobRaw(w http.ResponseWriter, r *http.Request) { 803 f, err := rp.repoResolver.Resolve(r) 804 if err != nil { 805 - log.Println("failed to get repo and knot", err) 806 w.WriteHeader(http.StatusBadRequest) 807 return 808 } ··· 834 835 req, err := http.NewRequest("GET", blobURL, nil) 836 if err != nil { 837 - log.Println("failed to create request", err) 838 return 839 } 840 ··· 846 client := &http.Client{} 847 resp, err := client.Do(req) 848 if err != nil { 849 - log.Println("failed to reach knotserver", err) 850 rp.pages.Error503(w) 851 return 852 } ··· 859 } 860 861 if resp.StatusCode != http.StatusOK { 862 - log.Printf("knotserver returned non-OK status for raw blob %s: %d", blobURL, resp.StatusCode) 863 w.WriteHeader(resp.StatusCode) 864 _, _ = io.Copy(w, resp.Body) 865 return ··· 868 contentType := resp.Header.Get("Content-Type") 869 body, err := io.ReadAll(resp.Body) 870 if err != nil { 871 - log.Printf("error reading response body from knotserver: %v", err) 872 w.WriteHeader(http.StatusInternalServerError) 873 return 874 } ··· 1443 db.FilterContains("scope", subject.Collection().String()), 1444 ) 1445 if err != nil { 1446 - log.Println("failed to fetch label defs", err) 1447 return 1448 } 1449 ··· 1454 1455 states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject)) 1456 if err != nil { 1457 - log.Println("failed to build label state", err) 1458 return 1459 } 1460 state := states[subject] ··· 1491 db.FilterContains("scope", subject.Collection().String()), 1492 ) 1493 if err != nil { 1494 - log.Println("failed to fetch labels", err) 1495 return 1496 } 1497 ··· 1502 1503 states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject)) 1504 if err != nil { 1505 - log.Println("failed to build label state", err) 1506 return 1507 } 1508 state := states[subject] ··· 1649 1650 func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) { 1651 user := rp.oauth.GetUser(r) 1652 1653 noticeId := "operation-error" 1654 f, err := rp.repoResolver.Resolve(r) 1655 if err != nil { 1656 - log.Println("failed to get repo and knot", err) 1657 return 1658 } 1659 1660 // remove record from pds 1661 atpClient, err := rp.oauth.AuthorizedClient(r) 1662 if err != nil { 1663 - log.Println("failed to get authorized client", err) 1664 return 1665 } 1666 _, err = comatproto.RepoDeleteRecord(r.Context(), atpClient, &comatproto.RepoDeleteRecord_Input{ ··· 1669 Rkey: f.Rkey, 1670 }) 1671 if err != nil { 1672 - log.Printf("failed to delete record: %s", err) 1673 rp.pages.Notice(w, noticeId, "Failed to delete repository from PDS.") 1674 return 1675 } 1676 - log.Println("removed repo record ", f.RepoAt().String()) 1677 1678 client, err := rp.oauth.ServiceClient( 1679 r, ··· 1682 oauth.WithDev(rp.config.Core.Dev), 1683 ) 1684 if err != nil { 1685 - log.Println("failed to connect to knot server:", err) 1686 return 1687 } 1688 ··· 1699 rp.pages.Notice(w, noticeId, err.Error()) 1700 return 1701 } 1702 - log.Println("deleted repo from knot") 1703 1704 tx, err := rp.db.BeginTx(r.Context(), nil) 1705 if err != nil { 1706 - log.Println("failed to start tx") 1707 w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 1708 return 1709 } ··· 1711 tx.Rollback() 1712 err = rp.enforcer.E.LoadPolicy() 1713 if err != nil { 1714 - log.Println("failed to rollback policies") 1715 } 1716 }() 1717 ··· 1725 did := c[0] 1726 rp.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo()) 1727 } 1728 - log.Println("removed collaborators") 1729 1730 // remove repo RBAC 1731 err = rp.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo()) ··· 1740 rp.pages.Notice(w, noticeId, "Failed to update appview") 1741 return 1742 } 1743 - log.Println("removed repo from db") 1744 1745 err = tx.Commit() 1746 if err != nil { 1747 - log.Println("failed to commit changes", err) 1748 http.Error(w, err.Error(), http.StatusInternalServerError) 1749 return 1750 } 1751 1752 err = rp.enforcer.E.SavePolicy() 1753 if err != nil { 1754 - log.Println("failed to update ACLs", err) 1755 http.Error(w, err.Error(), http.StatusInternalServerError) 1756 return 1757 } ··· 1760 } 1761 1762 func (rp *Repo) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 1763 f, err := rp.repoResolver.Resolve(r) 1764 if err != nil { 1765 - log.Println("failed to get repo and knot", err) 1766 return 1767 } 1768 ··· 1780 oauth.WithDev(rp.config.Core.Dev), 1781 ) 1782 if err != nil { 1783 - log.Println("failed to connect to knot server:", err) 1784 rp.pages.Notice(w, noticeId, "Failed to connect to knot server.") 1785 return 1786 } ··· 1794 }, 1795 ) 1796 if err := xrpcclient.HandleXrpcErr(xe); err != nil { 1797 - log.Println("xrpc failed", "err", xe) 1798 rp.pages.Notice(w, noticeId, err.Error()) 1799 return 1800 } ··· 1809 1810 f, err := rp.repoResolver.Resolve(r) 1811 if err != nil { 1812 - log.Println("failed to get repo and knot", err) 1813 return 1814 } 1815 1816 if f.Spindle == "" { 1817 - log.Println("empty spindle cannot add/rm secret", err) 1818 return 1819 } 1820 ··· 1831 oauth.WithDev(rp.config.Core.Dev), 1832 ) 1833 if err != nil { 1834 - log.Println("failed to create spindle client", err) 1835 return 1836 } 1837 ··· 1917 } 1918 1919 func (rp *Repo) generalSettings(w http.ResponseWriter, r *http.Request) { 1920 f, err := rp.repoResolver.Resolve(r) 1921 user := rp.oauth.GetUser(r) 1922 ··· 1932 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 1933 xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 1934 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1935 - log.Println("failed to call XRPC repo.branches", xrpcerr) 1936 rp.pages.Error503(w) 1937 return 1938 } 1939 1940 var result types.RepoBranchesResponse 1941 if err := json.Unmarshal(xrpcBytes, &result); err != nil { 1942 - log.Println("failed to decode XRPC response", err) 1943 rp.pages.Error503(w) 1944 return 1945 } 1946 1947 defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", models.DefaultLabelDefs())) 1948 if err != nil { 1949 - log.Println("failed to fetch labels", err) 1950 rp.pages.Error503(w) 1951 return 1952 } 1953 1954 labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels)) 1955 if err != nil { 1956 - log.Println("failed to fetch labels", err) 1957 rp.pages.Error503(w) 1958 return 1959 } ··· 2001 } 2002 2003 func (rp *Repo) accessSettings(w http.ResponseWriter, r *http.Request) { 2004 f, err := rp.repoResolver.Resolve(r) 2005 user := rp.oauth.GetUser(r) 2006 2007 repoCollaborators, err := f.Collaborators(r.Context()) 2008 if err != nil { 2009 - log.Println("failed to get collaborators", err) 2010 } 2011 2012 rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{ ··· 2019 } 2020 2021 func (rp *Repo) pipelineSettings(w http.ResponseWriter, r *http.Request) { 2022 f, err := rp.repoResolver.Resolve(r) 2023 user := rp.oauth.GetUser(r) 2024 2025 // all spindles that the repo owner is a member of 2026 spindles, err := rp.enforcer.GetSpindlesForUser(f.OwnerDid()) 2027 if err != nil { 2028 - log.Println("failed to fetch spindles", err) 2029 return 2030 } 2031 ··· 2038 oauth.WithExp(60), 2039 oauth.WithDev(rp.config.Core.Dev), 2040 ); err != nil { 2041 - log.Println("failed to create spindle client", err) 2042 } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt().String()); err != nil { 2043 - log.Println("failed to fetch secrets", err) 2044 } else { 2045 secrets = resp.Secrets 2046 } ··· 2080 } 2081 2082 func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) { 2083 ref := chi.URLParam(r, "ref") 2084 ref, _ = url.PathUnescape(ref) 2085 2086 user := rp.oauth.GetUser(r) 2087 f, err := rp.repoResolver.Resolve(r) 2088 if err != nil { 2089 - log.Printf("failed to resolve source repo: %v", err) 2090 return 2091 } 2092 ··· 2130 } 2131 2132 func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) { 2133 user := rp.oauth.GetUser(r) 2134 f, err := rp.repoResolver.Resolve(r) 2135 if err != nil { 2136 - log.Printf("failed to resolve source repo: %v", err) 2137 return 2138 } 2139 ··· 2184 ) 2185 if err != nil { 2186 if !errors.Is(err, sql.ErrNoRows) { 2187 - log.Println("error fetching existing repo from db", "err", err) 2188 rp.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.") 2189 return 2190 } ··· 2299 2300 err = db.AddRepo(tx, repo) 2301 if err != nil { 2302 - log.Println(err) 2303 rp.pages.Notice(w, "repo", "Failed to save repository information.") 2304 return 2305 } ··· 2308 p, _ := securejoin.SecureJoin(user.Did, forkName) 2309 err = rp.enforcer.AddRepo(user.Did, targetKnot, p) 2310 if err != nil { 2311 - log.Println(err) 2312 rp.pages.Notice(w, "repo", "Failed to set up repository permissions.") 2313 return 2314 } 2315 2316 err = tx.Commit() 2317 if err != nil { 2318 - log.Println("failed to commit changes", err) 2319 http.Error(w, err.Error(), http.StatusInternalServerError) 2320 return 2321 } 2322 2323 err = rp.enforcer.E.SavePolicy() 2324 if err != nil { 2325 - log.Println("failed to update ACLs", err) 2326 http.Error(w, err.Error(), http.StatusInternalServerError) 2327 return 2328 } ··· 2358 } 2359 2360 func (rp *Repo) RepoCompareNew(w http.ResponseWriter, r *http.Request) { 2361 user := rp.oauth.GetUser(r) 2362 f, err := rp.repoResolver.Resolve(r) 2363 if err != nil { 2364 - log.Println("failed to get repo and knot", err) 2365 return 2366 } 2367 ··· 2377 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 2378 branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 2379 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 2380 - log.Println("failed to call XRPC repo.branches", xrpcerr) 2381 rp.pages.Error503(w) 2382 return 2383 } 2384 2385 var branchResult types.RepoBranchesResponse 2386 if err := json.Unmarshal(branchBytes, &branchResult); err != nil { 2387 - log.Println("failed to decode XRPC branches response", err) 2388 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2389 return 2390 } ··· 2414 2415 tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 2416 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 2417 - log.Println("failed to call XRPC repo.tags", xrpcerr) 2418 rp.pages.Error503(w) 2419 return 2420 } 2421 2422 var tags types.RepoTagsResponse 2423 if err := json.Unmarshal(tagBytes, &tags); err != nil { 2424 - log.Println("failed to decode XRPC tags response", err) 2425 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2426 return 2427 } ··· 2439 } 2440 2441 func (rp *Repo) RepoCompare(w http.ResponseWriter, r *http.Request) { 2442 user := rp.oauth.GetUser(r) 2443 f, err := rp.repoResolver.Resolve(r) 2444 if err != nil { 2445 - log.Println("failed to get repo and knot", err) 2446 return 2447 } 2448 ··· 2469 head, _ = url.PathUnescape(head) 2470 2471 if base == "" || head == "" { 2472 - log.Printf("invalid comparison") 2473 rp.pages.Error404(w) 2474 return 2475 } ··· 2487 2488 branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 2489 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 2490 - log.Println("failed to call XRPC repo.branches", xrpcerr) 2491 rp.pages.Error503(w) 2492 return 2493 } 2494 2495 var branches types.RepoBranchesResponse 2496 if err := json.Unmarshal(branchBytes, &branches); err != nil { 2497 - log.Println("failed to decode XRPC branches response", err) 2498 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2499 return 2500 } 2501 2502 tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 2503 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 2504 - log.Println("failed to call XRPC repo.tags", xrpcerr) 2505 rp.pages.Error503(w) 2506 return 2507 } 2508 2509 var tags types.RepoTagsResponse 2510 if err := json.Unmarshal(tagBytes, &tags); err != nil { 2511 - log.Println("failed to decode XRPC tags response", err) 2512 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2513 return 2514 } 2515 2516 compareBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, base, head) 2517 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 2518 - log.Println("failed to call XRPC repo.compare", xrpcerr) 2519 rp.pages.Error503(w) 2520 return 2521 } 2522 2523 var formatPatch types.RepoFormatPatchResponse 2524 if err := json.Unmarshal(compareBytes, &formatPatch); err != nil { 2525 - log.Println("failed to decode XRPC compare response", err) 2526 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2527 return 2528 }
··· 7 "errors" 8 "fmt" 9 "io" 10 "log/slog" 11 "net/http" 12 "net/url" ··· 89 } 90 91 func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) { 92 + l := rp.logger.With("handler", "DownloadArchive") 93 + 94 ref := chi.URLParam(r, "ref") 95 ref, _ = url.PathUnescape(ref) 96 97 f, err := rp.repoResolver.Resolve(r) 98 if err != nil { 99 + l.Error("failed to get repo and knot", "err", err) 100 return 101 } 102 ··· 112 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 113 archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo) 114 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 115 + l.Error("failed to call XRPC repo.archive", "err", xrpcerr) 116 rp.pages.Error503(w) 117 return 118 } ··· 129 } 130 131 func (rp *Repo) RepoLog(w http.ResponseWriter, r *http.Request) { 132 + l := rp.logger.With("handler", "RepoLog") 133 + 134 f, err := rp.repoResolver.Resolve(r) 135 if err != nil { 136 + l.Error("failed to fully resolve repo", "err", err) 137 return 138 } 139 ··· 168 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 169 xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo) 170 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 171 + l.Error("failed to call XRPC repo.log", "err", xrpcerr) 172 rp.pages.Error503(w) 173 return 174 } 175 176 var xrpcResp types.RepoLogResponse 177 if err := json.Unmarshal(xrpcBytes, &xrpcResp); err != nil { 178 + l.Error("failed to decode XRPC response", "err", err) 179 rp.pages.Error503(w) 180 return 181 } 182 183 tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 184 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 185 + l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 186 rp.pages.Error503(w) 187 return 188 } ··· 199 200 branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 201 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 202 + l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 203 rp.pages.Error503(w) 204 return 205 } ··· 217 218 emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(xrpcResp.Commits), true) 219 if err != nil { 220 + l.Error("failed to fetch email to did mapping", "err", err) 221 } 222 223 vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, xrpcResp.Commits) 224 if err != nil { 225 + l.Error("failed to GetVerifiedObjectCommits", "err", err) 226 } 227 228 repoInfo := f.RepoInfo(user) ··· 233 } 234 pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas) 235 if err != nil { 236 + l.Error("failed to getPipelineStatuses", "err", err) 237 // non-fatal 238 } 239 ··· 249 } 250 251 func (rp *Repo) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) { 252 + l := rp.logger.With("handler", "RepoDescriptionEdit") 253 + 254 f, err := rp.repoResolver.Resolve(r) 255 if err != nil { 256 + l.Error("failed to get repo and knot", "err", err) 257 w.WriteHeader(http.StatusBadRequest) 258 return 259 } ··· 265 } 266 267 func (rp *Repo) RepoDescription(w http.ResponseWriter, r *http.Request) { 268 + l := rp.logger.With("handler", "RepoDescription") 269 + 270 f, err := rp.repoResolver.Resolve(r) 271 if err != nil { 272 + l.Error("failed to get repo and knot", "err", err) 273 w.WriteHeader(http.StatusBadRequest) 274 return 275 } ··· 277 repoAt := f.RepoAt() 278 rkey := repoAt.RecordKey().String() 279 if rkey == "" { 280 + l.Error("invalid aturi for repo", "err", err) 281 w.WriteHeader(http.StatusInternalServerError) 282 return 283 } ··· 294 newDescription := r.FormValue("description") 295 client, err := rp.oauth.AuthorizedClient(r) 296 if err != nil { 297 + l.Error("failed to get client") 298 rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 299 return 300 } ··· 302 // optimistic update 303 err = db.UpdateDescription(rp.db, string(repoAt), newDescription) 304 if err != nil { 305 + l.Error("failed to perform update-description query", "err", err) 306 rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 307 return 308 } ··· 331 }) 332 333 if err != nil { 334 + l.Error("failed to perferom update-description query", "err", err) 335 // failed to get record 336 rp.pages.Notice(w, "repo-notice", "Failed to update description, unable to save to PDS.") 337 return ··· 348 } 349 350 func (rp *Repo) RepoCommit(w http.ResponseWriter, r *http.Request) { 351 + l := rp.logger.With("handler", "RepoCommit") 352 + 353 f, err := rp.repoResolver.Resolve(r) 354 if err != nil { 355 + l.Error("failed to fully resolve repo", "err", err) 356 return 357 } 358 ref := chi.URLParam(r, "ref") ··· 380 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 381 xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo) 382 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 383 + l.Error("failed to call XRPC repo.diff", "err", xrpcerr) 384 rp.pages.Error503(w) 385 return 386 } 387 388 var result types.RepoCommitResponse 389 if err := json.Unmarshal(xrpcBytes, &result); err != nil { 390 + l.Error("failed to decode XRPC response", "err", err) 391 rp.pages.Error503(w) 392 return 393 } 394 395 emailToDidMap, err := db.GetEmailToDid(rp.db, []string{result.Diff.Commit.Committer.Email, result.Diff.Commit.Author.Email}, true) 396 if err != nil { 397 + l.Error("failed to get email to did mapping", "err", err) 398 } 399 400 vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.NiceDiff{*result.Diff}) 401 if err != nil { 402 + l.Error("failed to GetVerifiedCommits", "err", err) 403 } 404 405 user := rp.oauth.GetUser(r) 406 repoInfo := f.RepoInfo(user) 407 pipelines, err := getPipelineStatuses(rp.db, repoInfo, []string{result.Diff.Commit.This}) 408 if err != nil { 409 + l.Error("failed to getPipelineStatuses", "err", err) 410 // non-fatal 411 } 412 var pipeline *models.Pipeline ··· 426 } 427 428 func (rp *Repo) RepoTree(w http.ResponseWriter, r *http.Request) { 429 + l := rp.logger.With("handler", "RepoTree") 430 + 431 f, err := rp.repoResolver.Resolve(r) 432 if err != nil { 433 + l.Error("failed to fully resolve repo", "err", err) 434 return 435 } 436 ··· 455 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 456 xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo) 457 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 458 + l.Error("failed to call XRPC repo.tree", "err", xrpcerr) 459 rp.pages.Error503(w) 460 return 461 } ··· 530 } 531 532 func (rp *Repo) RepoTags(w http.ResponseWriter, r *http.Request) { 533 + l := rp.logger.With("handler", "RepoTags") 534 + 535 f, err := rp.repoResolver.Resolve(r) 536 if err != nil { 537 + l.Error("failed to get repo and knot", "err", err) 538 return 539 } 540 ··· 550 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 551 xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 552 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 553 + l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 554 rp.pages.Error503(w) 555 return 556 } 557 558 var result types.RepoTagsResponse 559 if err := json.Unmarshal(xrpcBytes, &result); err != nil { 560 + l.Error("failed to decode XRPC response", "err", err) 561 rp.pages.Error503(w) 562 return 563 } 564 565 artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt())) 566 if err != nil { 567 + l.Error("failed grab artifacts", "err", err) 568 return 569 } 570 ··· 601 } 602 603 func (rp *Repo) RepoBranches(w http.ResponseWriter, r *http.Request) { 604 + l := rp.logger.With("handler", "RepoBranches") 605 + 606 f, err := rp.repoResolver.Resolve(r) 607 if err != nil { 608 + l.Error("failed to get repo and knot", "err", err) 609 return 610 } 611 ··· 621 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 622 xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 623 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 624 + l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 625 rp.pages.Error503(w) 626 return 627 } 628 629 var result types.RepoBranchesResponse 630 if err := json.Unmarshal(xrpcBytes, &result); err != nil { 631 + l.Error("failed to decode XRPC response", "err", err) 632 rp.pages.Error503(w) 633 return 634 } ··· 644 } 645 646 func (rp *Repo) DeleteBranch(w http.ResponseWriter, r *http.Request) { 647 + l := rp.logger.With("handler", "DeleteBranch") 648 + 649 f, err := rp.repoResolver.Resolve(r) 650 if err != nil { 651 + l.Error("failed to get repo and knot", "err", err) 652 return 653 } 654 655 noticeId := "delete-branch-error" 656 fail := func(msg string, err error) { 657 + l.Error(msg, "err", err) 658 rp.pages.Notice(w, noticeId, msg) 659 } 660 ··· 687 fail(fmt.Sprintf("Failed to delete branch: %s", err), err) 688 return 689 } 690 + l.Error("deleted branch from knot", "branch", branch, "repo", f.RepoAt()) 691 692 rp.pages.HxRefresh(w) 693 } 694 695 func (rp *Repo) RepoBlob(w http.ResponseWriter, r *http.Request) { 696 + l := rp.logger.With("handler", "RepoBlob") 697 + 698 f, err := rp.repoResolver.Resolve(r) 699 if err != nil { 700 + l.Error("failed to get repo and knot", "err", err) 701 return 702 } 703 ··· 719 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name) 720 resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo) 721 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 722 + l.Error("failed to call XRPC repo.blob", "err", xrpcerr) 723 rp.pages.Error503(w) 724 return 725 } ··· 819 } 820 821 func (rp *Repo) RepoBlobRaw(w http.ResponseWriter, r *http.Request) { 822 + l := rp.logger.With("handler", "RepoBlobRaw") 823 + 824 f, err := rp.repoResolver.Resolve(r) 825 if err != nil { 826 + l.Error("failed to get repo and knot", err) 827 w.WriteHeader(http.StatusBadRequest) 828 return 829 } ··· 855 856 req, err := http.NewRequest("GET", blobURL, nil) 857 if err != nil { 858 + l.Error("failed to create request", "err", err) 859 return 860 } 861 ··· 867 client := &http.Client{} 868 resp, err := client.Do(req) 869 if err != nil { 870 + l.Error("failed to reach knotserver", "err", err) 871 rp.pages.Error503(w) 872 return 873 } ··· 880 } 881 882 if resp.StatusCode != http.StatusOK { 883 + l.Error("knotserver returned non-OK status for raw blob", "url", blobURL, "statuscode", resp.StatusCode) 884 w.WriteHeader(resp.StatusCode) 885 _, _ = io.Copy(w, resp.Body) 886 return ··· 889 contentType := resp.Header.Get("Content-Type") 890 body, err := io.ReadAll(resp.Body) 891 if err != nil { 892 + l.Error("error reading response body from knotserver", "err", err) 893 w.WriteHeader(http.StatusInternalServerError) 894 return 895 } ··· 1464 db.FilterContains("scope", subject.Collection().String()), 1465 ) 1466 if err != nil { 1467 + l.Error("failed to fetch label defs", "err", err) 1468 return 1469 } 1470 ··· 1475 1476 states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject)) 1477 if err != nil { 1478 + l.Error("failed to build label state", "err", err) 1479 return 1480 } 1481 state := states[subject] ··· 1512 db.FilterContains("scope", subject.Collection().String()), 1513 ) 1514 if err != nil { 1515 + l.Error("failed to fetch labels", "err", err) 1516 return 1517 } 1518 ··· 1523 1524 states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject)) 1525 if err != nil { 1526 + l.Error("failed to build label state", "err", err) 1527 return 1528 } 1529 state := states[subject] ··· 1670 1671 func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) { 1672 user := rp.oauth.GetUser(r) 1673 + l := rp.logger.With("handler", "DeleteRepo") 1674 1675 noticeId := "operation-error" 1676 f, err := rp.repoResolver.Resolve(r) 1677 if err != nil { 1678 + l.Error("failed to get repo and knot", "err", err) 1679 return 1680 } 1681 1682 // remove record from pds 1683 atpClient, err := rp.oauth.AuthorizedClient(r) 1684 if err != nil { 1685 + l.Error("failed to get authorized client", "err", err) 1686 return 1687 } 1688 _, err = comatproto.RepoDeleteRecord(r.Context(), atpClient, &comatproto.RepoDeleteRecord_Input{ ··· 1691 Rkey: f.Rkey, 1692 }) 1693 if err != nil { 1694 + l.Error("failed to delete record", "err", err) 1695 rp.pages.Notice(w, noticeId, "Failed to delete repository from PDS.") 1696 return 1697 } 1698 + l.Info("removed repo record", "aturi", f.RepoAt().String()) 1699 1700 client, err := rp.oauth.ServiceClient( 1701 r, ··· 1704 oauth.WithDev(rp.config.Core.Dev), 1705 ) 1706 if err != nil { 1707 + l.Error("failed to connect to knot server", "err", err) 1708 return 1709 } 1710 ··· 1721 rp.pages.Notice(w, noticeId, err.Error()) 1722 return 1723 } 1724 + l.Info("deleted repo from knot") 1725 1726 tx, err := rp.db.BeginTx(r.Context(), nil) 1727 if err != nil { 1728 + l.Error("failed to start tx") 1729 w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 1730 return 1731 } ··· 1733 tx.Rollback() 1734 err = rp.enforcer.E.LoadPolicy() 1735 if err != nil { 1736 + l.Error("failed to rollback policies") 1737 } 1738 }() 1739 ··· 1747 did := c[0] 1748 rp.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo()) 1749 } 1750 + l.Info("removed collaborators") 1751 1752 // remove repo RBAC 1753 err = rp.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo()) ··· 1762 rp.pages.Notice(w, noticeId, "Failed to update appview") 1763 return 1764 } 1765 + l.Info("removed repo from db") 1766 1767 err = tx.Commit() 1768 if err != nil { 1769 + l.Error("failed to commit changes", "err", err) 1770 http.Error(w, err.Error(), http.StatusInternalServerError) 1771 return 1772 } 1773 1774 err = rp.enforcer.E.SavePolicy() 1775 if err != nil { 1776 + l.Error("failed to update ACLs", "err", err) 1777 http.Error(w, err.Error(), http.StatusInternalServerError) 1778 return 1779 } ··· 1782 } 1783 1784 func (rp *Repo) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 1785 + l := rp.logger.With("handler", "SetDefaultBranch") 1786 + 1787 f, err := rp.repoResolver.Resolve(r) 1788 if err != nil { 1789 + l.Error("failed to get repo and knot", "err", err) 1790 return 1791 } 1792 ··· 1804 oauth.WithDev(rp.config.Core.Dev), 1805 ) 1806 if err != nil { 1807 + l.Error("failed to connect to knot server", "err", err) 1808 rp.pages.Notice(w, noticeId, "Failed to connect to knot server.") 1809 return 1810 } ··· 1818 }, 1819 ) 1820 if err := xrpcclient.HandleXrpcErr(xe); err != nil { 1821 + l.Error("xrpc failed", "err", xe) 1822 rp.pages.Notice(w, noticeId, err.Error()) 1823 return 1824 } ··· 1833 1834 f, err := rp.repoResolver.Resolve(r) 1835 if err != nil { 1836 + l.Error("failed to get repo and knot", "err", err) 1837 return 1838 } 1839 1840 if f.Spindle == "" { 1841 + l.Error("empty spindle cannot add/rm secret", "err", err) 1842 return 1843 } 1844 ··· 1855 oauth.WithDev(rp.config.Core.Dev), 1856 ) 1857 if err != nil { 1858 + l.Error("failed to create spindle client", "err", err) 1859 return 1860 } 1861 ··· 1941 } 1942 1943 func (rp *Repo) generalSettings(w http.ResponseWriter, r *http.Request) { 1944 + l := rp.logger.With("handler", "generalSettings") 1945 + 1946 f, err := rp.repoResolver.Resolve(r) 1947 user := rp.oauth.GetUser(r) 1948 ··· 1958 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 1959 xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 1960 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1961 + l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 1962 rp.pages.Error503(w) 1963 return 1964 } 1965 1966 var result types.RepoBranchesResponse 1967 if err := json.Unmarshal(xrpcBytes, &result); err != nil { 1968 + l.Error("failed to decode XRPC response", "err", err) 1969 rp.pages.Error503(w) 1970 return 1971 } 1972 1973 defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", models.DefaultLabelDefs())) 1974 if err != nil { 1975 + l.Error("failed to fetch labels", "err", err) 1976 rp.pages.Error503(w) 1977 return 1978 } 1979 1980 labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels)) 1981 if err != nil { 1982 + l.Error("failed to fetch labels", "err", err) 1983 rp.pages.Error503(w) 1984 return 1985 } ··· 2027 } 2028 2029 func (rp *Repo) accessSettings(w http.ResponseWriter, r *http.Request) { 2030 + l := rp.logger.With("handler", "accessSettings") 2031 + 2032 f, err := rp.repoResolver.Resolve(r) 2033 user := rp.oauth.GetUser(r) 2034 2035 repoCollaborators, err := f.Collaborators(r.Context()) 2036 if err != nil { 2037 + l.Error("failed to get collaborators", "err", err) 2038 } 2039 2040 rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{ ··· 2047 } 2048 2049 func (rp *Repo) pipelineSettings(w http.ResponseWriter, r *http.Request) { 2050 + l := rp.logger.With("handler", "pipelineSettings") 2051 + 2052 f, err := rp.repoResolver.Resolve(r) 2053 user := rp.oauth.GetUser(r) 2054 2055 // all spindles that the repo owner is a member of 2056 spindles, err := rp.enforcer.GetSpindlesForUser(f.OwnerDid()) 2057 if err != nil { 2058 + l.Error("failed to fetch spindles", "err", err) 2059 return 2060 } 2061 ··· 2068 oauth.WithExp(60), 2069 oauth.WithDev(rp.config.Core.Dev), 2070 ); err != nil { 2071 + l.Error("failed to create spindle client", "err", err) 2072 } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt().String()); err != nil { 2073 + l.Error("failed to fetch secrets", "err", err) 2074 } else { 2075 secrets = resp.Secrets 2076 } ··· 2110 } 2111 2112 func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) { 2113 + l := rp.logger.With("handler", "SyncRepoFork") 2114 + 2115 ref := chi.URLParam(r, "ref") 2116 ref, _ = url.PathUnescape(ref) 2117 2118 user := rp.oauth.GetUser(r) 2119 f, err := rp.repoResolver.Resolve(r) 2120 if err != nil { 2121 + l.Error("failed to resolve source repo", "err", err) 2122 return 2123 } 2124 ··· 2162 } 2163 2164 func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) { 2165 + l := rp.logger.With("handler", "ForkRepo") 2166 + 2167 user := rp.oauth.GetUser(r) 2168 f, err := rp.repoResolver.Resolve(r) 2169 if err != nil { 2170 + l.Error("failed to resolve source repo", "err", err) 2171 return 2172 } 2173 ··· 2218 ) 2219 if err != nil { 2220 if !errors.Is(err, sql.ErrNoRows) { 2221 + l.Error("error fetching existing repo from db", "err", err) 2222 rp.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.") 2223 return 2224 } ··· 2333 2334 err = db.AddRepo(tx, repo) 2335 if err != nil { 2336 + l.Error("failed to AddRepo", "err", err) 2337 rp.pages.Notice(w, "repo", "Failed to save repository information.") 2338 return 2339 } ··· 2342 p, _ := securejoin.SecureJoin(user.Did, forkName) 2343 err = rp.enforcer.AddRepo(user.Did, targetKnot, p) 2344 if err != nil { 2345 + l.Error("failed to add ACLs", "err", err) 2346 rp.pages.Notice(w, "repo", "Failed to set up repository permissions.") 2347 return 2348 } 2349 2350 err = tx.Commit() 2351 if err != nil { 2352 + l.Error("failed to commit changes", "err", err) 2353 http.Error(w, err.Error(), http.StatusInternalServerError) 2354 return 2355 } 2356 2357 err = rp.enforcer.E.SavePolicy() 2358 if err != nil { 2359 + l.Error("failed to update ACLs", "err", err) 2360 http.Error(w, err.Error(), http.StatusInternalServerError) 2361 return 2362 } ··· 2392 } 2393 2394 func (rp *Repo) RepoCompareNew(w http.ResponseWriter, r *http.Request) { 2395 + l := rp.logger.With("handler", "RepoCompareNew") 2396 + 2397 user := rp.oauth.GetUser(r) 2398 f, err := rp.repoResolver.Resolve(r) 2399 if err != nil { 2400 + l.Error("failed to get repo and knot", "err", err) 2401 return 2402 } 2403 ··· 2413 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 2414 branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 2415 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 2416 + l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 2417 rp.pages.Error503(w) 2418 return 2419 } 2420 2421 var branchResult types.RepoBranchesResponse 2422 if err := json.Unmarshal(branchBytes, &branchResult); err != nil { 2423 + l.Error("failed to decode XRPC branches response", "err", err) 2424 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2425 return 2426 } ··· 2450 2451 tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 2452 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 2453 + l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 2454 rp.pages.Error503(w) 2455 return 2456 } 2457 2458 var tags types.RepoTagsResponse 2459 if err := json.Unmarshal(tagBytes, &tags); err != nil { 2460 + l.Error("failed to decode XRPC tags response", "err", err) 2461 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2462 return 2463 } ··· 2475 } 2476 2477 func (rp *Repo) RepoCompare(w http.ResponseWriter, r *http.Request) { 2478 + l := rp.logger.With("handler", "RepoCompare") 2479 + 2480 user := rp.oauth.GetUser(r) 2481 f, err := rp.repoResolver.Resolve(r) 2482 if err != nil { 2483 + l.Error("failed to get repo and knot", "err", err) 2484 return 2485 } 2486 ··· 2507 head, _ = url.PathUnescape(head) 2508 2509 if base == "" || head == "" { 2510 + l.Error("invalid comparison") 2511 rp.pages.Error404(w) 2512 return 2513 } ··· 2525 2526 branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 2527 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 2528 + l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 2529 rp.pages.Error503(w) 2530 return 2531 } 2532 2533 var branches types.RepoBranchesResponse 2534 if err := json.Unmarshal(branchBytes, &branches); err != nil { 2535 + l.Error("failed to decode XRPC branches response", "err", err) 2536 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2537 return 2538 } 2539 2540 tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 2541 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 2542 + l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 2543 rp.pages.Error503(w) 2544 return 2545 } 2546 2547 var tags types.RepoTagsResponse 2548 if err := json.Unmarshal(tagBytes, &tags); err != nil { 2549 + l.Error("failed to decode XRPC tags response", "err", err) 2550 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2551 return 2552 } 2553 2554 compareBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, base, head) 2555 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 2556 + l.Error("failed to call XRPC repo.compare", "err", xrpcerr) 2557 rp.pages.Error503(w) 2558 return 2559 } 2560 2561 var formatPatch types.RepoFormatPatchResponse 2562 if err := json.Unmarshal(compareBytes, &formatPatch); err != nil { 2563 + l.Error("failed to decode XRPC compare response", "err", err) 2564 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2565 return 2566 }
+1 -1
appview/signup/signup.go
··· 62 disallowed := make(map[string]bool) 63 64 if filepath == "" { 65 - logger.Debug("no disallowed nicknames file configured") 66 return disallowed 67 } 68
··· 62 disallowed := make(map[string]bool) 63 64 if filepath == "" { 65 + logger.Warn("no disallowed nicknames file configured") 66 return disallowed 67 } 68
+3 -1
appview/state/knotstream.go
··· 25 ) 26 27 func Knotstream(ctx context.Context, c *config.Config, d *db.DB, enforcer *rbac.Enforcer, posthog posthog.Client) (*ec.Consumer, error) { 28 knots, err := db.GetRegistrations( 29 d, 30 db.FilterIsNot("registered", "null"), ··· 39 srcs[s] = struct{}{} 40 } 41 42 - logger := log.New("knotstream") 43 cache := cache.New(c.Redis.Addr) 44 cursorStore := cursor.NewRedisCursorStore(cache) 45
··· 25 ) 26 27 func Knotstream(ctx context.Context, c *config.Config, d *db.DB, enforcer *rbac.Enforcer, posthog posthog.Client) (*ec.Consumer, error) { 28 + logger := log.FromContext(ctx) 29 + logger = log.SubLogger(logger, "knotstream") 30 + 31 knots, err := db.GetRegistrations( 32 d, 33 db.FilterIsNot("registered", "null"), ··· 42 srcs[s] = struct{}{} 43 } 44 45 cache := cache.New(c.Redis.Addr) 46 cursorStore := cursor.NewRedisCursorStore(cache) 47
+7 -4
appview/state/login.go
··· 2 3 import ( 4 "fmt" 5 - "log" 6 "net/http" 7 "strings" 8 ··· 10 ) 11 12 func (s *State) Login(w http.ResponseWriter, r *http.Request) { 13 switch r.Method { 14 case http.MethodGet: 15 returnURL := r.URL.Query().Get("return_url") ··· 32 33 // basic handle validation 34 if !strings.Contains(handle, ".") { 35 - log.Println("invalid handle format", "raw", handle) 36 s.pages.Notice( 37 w, 38 "login-msg", ··· 52 } 53 54 func (s *State) Logout(w http.ResponseWriter, r *http.Request) { 55 err := s.oauth.DeleteSession(w, r) 56 if err != nil { 57 - log.Println("failed to logout", "err", err) 58 } else { 59 - log.Println("logged out successfully") 60 } 61 62 s.pages.HxRedirect(w, "/login")
··· 2 3 import ( 4 "fmt" 5 "net/http" 6 "strings" 7 ··· 9 ) 10 11 func (s *State) Login(w http.ResponseWriter, r *http.Request) { 12 + l := s.logger.With("handler", "Login") 13 + 14 switch r.Method { 15 case http.MethodGet: 16 returnURL := r.URL.Query().Get("return_url") ··· 33 34 // basic handle validation 35 if !strings.Contains(handle, ".") { 36 + l.Error("invalid handle format", "raw", handle) 37 s.pages.Notice( 38 w, 39 "login-msg", ··· 53 } 54 55 func (s *State) Logout(w http.ResponseWriter, r *http.Request) { 56 + l := s.logger.With("handler", "Logout") 57 + 58 err := s.oauth.DeleteSession(w, r) 59 if err != nil { 60 + l.Error("failed to logout", "err", err) 61 } else { 62 + l.Info("logged out successfully") 63 } 64 65 s.pages.HxRedirect(w, "/login")
+59 -13
appview/state/router.go
··· 205 } 206 207 func (s *State) SpindlesRouter() http.Handler { 208 - logger := log.New("spindles") 209 210 spindles := &spindles.Spindles{ 211 Db: s.db, ··· 221 } 222 223 func (s *State) KnotsRouter() http.Handler { 224 - logger := log.New("knots") 225 226 knots := &knots.Knots{ 227 Db: s.db, ··· 238 } 239 240 func (s *State) StringsRouter(mw *middleware.Middleware) http.Handler { 241 - logger := log.New("strings") 242 243 strs := &avstrings.Strings{ 244 Db: s.db, ··· 253 } 254 255 func (s *State) IssuesRouter(mw *middleware.Middleware) http.Handler { 256 - issues := issues.New(s.oauth, s.repoResolver, s.pages, s.idResolver, s.db, s.config, s.notifier, s.validator) 257 return issues.Router(mw) 258 } 259 260 func (s *State) PullsRouter(mw *middleware.Middleware) http.Handler { 261 - pulls := pulls.New(s.oauth, s.repoResolver, s.pages, s.idResolver, s.db, s.config, s.notifier, s.enforcer) 262 return pulls.Router(mw) 263 } 264 265 func (s *State) RepoRouter(mw *middleware.Middleware) http.Handler { 266 - logger := log.New("repo") 267 - repo := repo.New(s.oauth, s.repoResolver, s.pages, s.spindlestream, s.idResolver, s.db, s.config, s.notifier, s.enforcer, logger, s.validator) 268 return repo.Router(mw) 269 } 270 271 func (s *State) PipelinesRouter(mw *middleware.Middleware) http.Handler { 272 - pipes := pipelines.New(s.oauth, s.repoResolver, s.pages, s.spindlestream, s.idResolver, s.db, s.config, s.enforcer) 273 return pipes.Router(mw) 274 } 275 276 func (s *State) LabelsRouter(mw *middleware.Middleware) http.Handler { 277 - ls := labels.New(s.oauth, s.pages, s.db, s.validator, s.enforcer) 278 return ls.Router(mw) 279 } 280 281 func (s *State) NotificationsRouter(mw *middleware.Middleware) http.Handler { 282 - notifs := notifications.New(s.db, s.oauth, s.pages) 283 return notifs.Router(mw) 284 } 285 286 func (s *State) SignupRouter() http.Handler { 287 - logger := log.New("signup") 288 - 289 - sig := signup.New(s.config, s.db, s.posthog, s.idResolver, s.pages, logger) 290 return sig.Router() 291 }
··· 205 } 206 207 func (s *State) SpindlesRouter() http.Handler { 208 + logger := log.SubLogger(s.logger, "spindles") 209 210 spindles := &spindles.Spindles{ 211 Db: s.db, ··· 221 } 222 223 func (s *State) KnotsRouter() http.Handler { 224 + logger := log.SubLogger(s.logger, "knots") 225 226 knots := &knots.Knots{ 227 Db: s.db, ··· 238 } 239 240 func (s *State) StringsRouter(mw *middleware.Middleware) http.Handler { 241 + logger := log.SubLogger(s.logger, "strings") 242 243 strs := &avstrings.Strings{ 244 Db: s.db, ··· 253 } 254 255 func (s *State) IssuesRouter(mw *middleware.Middleware) http.Handler { 256 + issues := issues.New( 257 + s.oauth, 258 + s.repoResolver, 259 + s.pages, 260 + s.idResolver, 261 + s.db, 262 + s.config, 263 + s.notifier, 264 + s.validator, 265 + log.SubLogger(s.logger, "issues"), 266 + ) 267 return issues.Router(mw) 268 } 269 270 func (s *State) PullsRouter(mw *middleware.Middleware) http.Handler { 271 + pulls := pulls.New( 272 + s.oauth, 273 + s.repoResolver, 274 + s.pages, 275 + s.idResolver, 276 + s.db, 277 + s.config, 278 + s.notifier, 279 + s.enforcer, 280 + log.SubLogger(s.logger, "pulls"), 281 + ) 282 return pulls.Router(mw) 283 } 284 285 func (s *State) RepoRouter(mw *middleware.Middleware) http.Handler { 286 + repo := repo.New( 287 + s.oauth, 288 + s.repoResolver, 289 + s.pages, 290 + s.spindlestream, 291 + s.idResolver, 292 + s.db, 293 + s.config, 294 + s.notifier, 295 + s.enforcer, 296 + log.SubLogger(s.logger, "repo"), 297 + s.validator, 298 + ) 299 return repo.Router(mw) 300 } 301 302 func (s *State) PipelinesRouter(mw *middleware.Middleware) http.Handler { 303 + pipes := pipelines.New( 304 + s.oauth, 305 + s.repoResolver, 306 + s.pages, 307 + s.spindlestream, 308 + s.idResolver, 309 + s.db, 310 + s.config, 311 + s.enforcer, 312 + log.SubLogger(s.logger, "pipelines"), 313 + ) 314 return pipes.Router(mw) 315 } 316 317 func (s *State) LabelsRouter(mw *middleware.Middleware) http.Handler { 318 + ls := labels.New( 319 + s.oauth, 320 + s.pages, 321 + s.db, 322 + s.validator, 323 + s.enforcer, 324 + log.SubLogger(s.logger, "labels"), 325 + ) 326 return ls.Router(mw) 327 } 328 329 func (s *State) NotificationsRouter(mw *middleware.Middleware) http.Handler { 330 + notifs := notifications.New(s.db, s.oauth, s.pages, log.SubLogger(s.logger, "notifications")) 331 return notifs.Router(mw) 332 } 333 334 func (s *State) SignupRouter() http.Handler { 335 + sig := signup.New(s.config, s.db, s.posthog, s.idResolver, s.pages, log.SubLogger(s.logger, "signup")) 336 return sig.Router() 337 }
+3 -1
appview/state/spindlestream.go
··· 22 ) 23 24 func Spindlestream(ctx context.Context, c *config.Config, d *db.DB, enforcer *rbac.Enforcer) (*ec.Consumer, error) { 25 spindles, err := db.GetSpindles( 26 d, 27 db.FilterIsNot("verified", "null"), ··· 36 srcs[src] = struct{}{} 37 } 38 39 - logger := log.New("spindlestream") 40 cache := cache.New(c.Redis.Addr) 41 cursorStore := cursor.NewRedisCursorStore(cache) 42
··· 22 ) 23 24 func Spindlestream(ctx context.Context, c *config.Config, d *db.DB, enforcer *rbac.Enforcer) (*ec.Consumer, error) { 25 + logger := log.FromContext(ctx) 26 + logger = log.SubLogger(logger, "spindlestream") 27 + 28 spindles, err := db.GetSpindles( 29 d, 30 db.FilterIsNot("verified", "null"), ··· 39 srcs[src] = struct{}{} 40 } 41 42 cache := cache.New(c.Redis.Addr) 43 cursorStore := cursor.NewRedisCursorStore(cache) 44
+18 -20
appview/state/state.go
··· 5 "database/sql" 6 "errors" 7 "fmt" 8 - "log" 9 "log/slog" 10 "net/http" 11 "strings" ··· 13 14 "tangled.org/core/api/tangled" 15 "tangled.org/core/appview" 16 - "tangled.org/core/appview/cache" 17 - "tangled.org/core/appview/cache/session" 18 "tangled.org/core/appview/config" 19 "tangled.org/core/appview/db" 20 "tangled.org/core/appview/models" ··· 29 "tangled.org/core/eventconsumer" 30 "tangled.org/core/idresolver" 31 "tangled.org/core/jetstream" 32 tlog "tangled.org/core/log" 33 "tangled.org/core/rbac" 34 "tangled.org/core/tid" ··· 48 oauth *oauth.OAuth 49 enforcer *rbac.Enforcer 50 pages *pages.Pages 51 - sess *session.SessionStore 52 idResolver *idresolver.Resolver 53 posthog posthog.Client 54 jc *jetstream.JetstreamClient ··· 61 } 62 63 func Make(ctx context.Context, config *config.Config) (*State, error) { 64 - d, err := db.Make(config.Core.DbPath) 65 if err != nil { 66 return nil, fmt.Errorf("failed to create db: %w", err) 67 } ··· 73 74 res, err := idresolver.RedisResolver(config.Redis.ToURL()) 75 if err != nil { 76 - log.Printf("failed to create redis resolver: %v", err) 77 res = idresolver.DefaultResolver() 78 } 79 80 - pages := pages.NewPages(config, res) 81 - cache := cache.New(config.Redis.Addr) 82 - sess := session.New(cache) 83 - oauth2, err := oauth.New(config) 84 if err != nil { 85 return nil, fmt.Errorf("failed to start oauth handler: %w", err) 86 } 87 validator := validator.New(d, res, enforcer) 88 89 - posthog, err := posthog.NewWithConfig(config.Posthog.ApiKey, posthog.Config{Endpoint: config.Posthog.Endpoint}) 90 if err != nil { 91 return nil, fmt.Errorf("failed to create posthog client: %w", err) 92 } ··· 112 tangled.LabelOpNSID, 113 }, 114 nil, 115 - slog.Default(), 116 wrapper, 117 false, 118 ··· 133 Enforcer: enforcer, 134 IdResolver: res, 135 Config: config, 136 - Logger: tlog.New("ingester"), 137 Validator: validator, 138 } 139 err = jc.StartJetstream(ctx, ingester.Ingest()) ··· 167 state := &State{ 168 d, 169 notifier, 170 - oauth2, 171 enforcer, 172 pages, 173 - sess, 174 res, 175 posthog, 176 jc, ··· 178 repoResolver, 179 knotstream, 180 spindlestream, 181 - slog.Default(), 182 validator, 183 } 184 ··· 277 } 278 timeline, err := db.MakeTimeline(s.db, 50, userDid, filtered) 279 if err != nil { 280 - log.Println(err) 281 s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.") 282 } 283 284 repos, err := db.GetTopStarredReposLastWeek(s.db) 285 if err != nil { 286 - log.Println(err) 287 s.pages.Notice(w, "topstarredrepos", "Unable to load.") 288 return 289 } ··· 344 345 timeline, err := db.MakeTimeline(s.db, 5, "", filtered) 346 if err != nil { 347 - log.Println(err) 348 s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.") 349 return 350 } 351 352 repos, err := db.GetTopStarredReposLastWeek(s.db) 353 if err != nil { 354 - log.Println(err) 355 s.pages.Notice(w, "topstarredrepos", "Unable to load.") 356 return 357 }
··· 5 "database/sql" 6 "errors" 7 "fmt" 8 "log/slog" 9 "net/http" 10 "strings" ··· 12 13 "tangled.org/core/api/tangled" 14 "tangled.org/core/appview" 15 "tangled.org/core/appview/config" 16 "tangled.org/core/appview/db" 17 "tangled.org/core/appview/models" ··· 26 "tangled.org/core/eventconsumer" 27 "tangled.org/core/idresolver" 28 "tangled.org/core/jetstream" 29 + "tangled.org/core/log" 30 tlog "tangled.org/core/log" 31 "tangled.org/core/rbac" 32 "tangled.org/core/tid" ··· 46 oauth *oauth.OAuth 47 enforcer *rbac.Enforcer 48 pages *pages.Pages 49 idResolver *idresolver.Resolver 50 posthog posthog.Client 51 jc *jetstream.JetstreamClient ··· 58 } 59 60 func Make(ctx context.Context, config *config.Config) (*State, error) { 61 + logger := tlog.FromContext(ctx) 62 + 63 + d, err := db.Make(ctx, config.Core.DbPath) 64 if err != nil { 65 return nil, fmt.Errorf("failed to create db: %w", err) 66 } ··· 72 73 res, err := idresolver.RedisResolver(config.Redis.ToURL()) 74 if err != nil { 75 + logger.Error("failed to create redis resolver", "err", err) 76 res = idresolver.DefaultResolver() 77 } 78 79 + pages := pages.NewPages(config, res, log.SubLogger(logger, "pages")) 80 + oauth, err := oauth.New(config) 81 if err != nil { 82 return nil, fmt.Errorf("failed to start oauth handler: %w", err) 83 } 84 validator := validator.New(d, res, enforcer) 85 86 + posthog, err := posthog.NewWithConfig(config.Posthog.ApiKey, posthog.Config{ 87 + Endpoint: config.Posthog.Endpoint, 88 + }) 89 if err != nil { 90 return nil, fmt.Errorf("failed to create posthog client: %w", err) 91 } ··· 111 tangled.LabelOpNSID, 112 }, 113 nil, 114 + tlog.SubLogger(logger, "jetstream"), 115 wrapper, 116 false, 117 ··· 132 Enforcer: enforcer, 133 IdResolver: res, 134 Config: config, 135 + Logger: log.SubLogger(logger, "ingester"), 136 Validator: validator, 137 } 138 err = jc.StartJetstream(ctx, ingester.Ingest()) ··· 166 state := &State{ 167 d, 168 notifier, 169 + oauth, 170 enforcer, 171 pages, 172 res, 173 posthog, 174 jc, ··· 176 repoResolver, 177 knotstream, 178 spindlestream, 179 + logger, 180 validator, 181 } 182 ··· 275 } 276 timeline, err := db.MakeTimeline(s.db, 50, userDid, filtered) 277 if err != nil { 278 + s.logger.Error("failed to make timeline", "err", err) 279 s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.") 280 } 281 282 repos, err := db.GetTopStarredReposLastWeek(s.db) 283 if err != nil { 284 + s.logger.Error("failed to get top starred repos", "err", err) 285 s.pages.Notice(w, "topstarredrepos", "Unable to load.") 286 return 287 } ··· 342 343 timeline, err := db.MakeTimeline(s.db, 5, "", filtered) 344 if err != nil { 345 + s.logger.Error("failed to make timeline", "err", err) 346 s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.") 347 return 348 } 349 350 repos, err := db.GetTopStarredReposLastWeek(s.db) 351 if err != nil { 352 + s.logger.Error("failed to get top starred repos", "err", err) 353 s.pages.Notice(w, "topstarredrepos", "Unable to load.") 354 return 355 }
+14 -9
cmd/appview/main.go
··· 2 3 import ( 4 "context" 5 - "log" 6 - "log/slog" 7 "net/http" 8 "os" 9 10 "tangled.org/core/appview/config" 11 "tangled.org/core/appview/state" 12 ) 13 14 func main() { 15 - slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, nil))) 16 - 17 ctx := context.Background() 18 19 c, err := config.LoadConfig(ctx) 20 if err != nil { 21 - log.Println("failed to load config", "error", err) 22 return 23 } 24 25 state, err := state.Make(ctx, c) 26 defer func() { 27 - log.Println(state.Close()) 28 }() 29 30 if err != nil { 31 - log.Fatal(err) 32 } 33 34 - log.Println("starting server on", c.Core.ListenAddr) 35 - log.Println(http.ListenAndServe(c.Core.ListenAddr, state.Router())) 36 }
··· 2 3 import ( 4 "context" 5 "net/http" 6 "os" 7 8 "tangled.org/core/appview/config" 9 "tangled.org/core/appview/state" 10 + tlog "tangled.org/core/log" 11 ) 12 13 func main() { 14 ctx := context.Background() 15 + logger := tlog.New("appview") 16 + ctx = tlog.IntoContext(ctx, logger) 17 18 c, err := config.LoadConfig(ctx) 19 if err != nil { 20 + logger.Error("failed to load config", "error", err) 21 return 22 } 23 24 state, err := state.Make(ctx, c) 25 defer func() { 26 + if err := state.Close(); err != nil { 27 + logger.Error("failed to close state", "err", err) 28 + } 29 }() 30 31 if err != nil { 32 + logger.Error("failed to start appview", "err", err) 33 + os.Exit(-1) 34 } 35 36 + logger.Info("starting server", "address", c.Core.ListenAddr) 37 + 38 + if err := http.ListenAndServe(c.Core.ListenAddr, state.Router()); err != nil { 39 + logger.Error("failed to start appview", "err", err) 40 + } 41 }
+6 -3
cmd/knot/main.go
··· 2 3 import ( 4 "context" 5 "os" 6 7 "github.com/urfave/cli/v3" ··· 9 "tangled.org/core/hook" 10 "tangled.org/core/keyfetch" 11 "tangled.org/core/knotserver" 12 - "tangled.org/core/log" 13 ) 14 15 func main() { ··· 24 }, 25 } 26 27 ctx := context.Background() 28 - logger := log.New("knot") 29 - ctx = log.IntoContext(ctx, logger.With("command", cmd.Name)) 30 31 if err := cmd.Run(ctx, os.Args); err != nil { 32 logger.Error(err.Error())
··· 2 3 import ( 4 "context" 5 + "log/slog" 6 "os" 7 8 "github.com/urfave/cli/v3" ··· 10 "tangled.org/core/hook" 11 "tangled.org/core/keyfetch" 12 "tangled.org/core/knotserver" 13 + tlog "tangled.org/core/log" 14 ) 15 16 func main() { ··· 25 }, 26 } 27 28 + logger := tlog.New("knot") 29 + slog.SetDefault(logger) 30 + 31 ctx := context.Background() 32 + ctx = tlog.IntoContext(ctx, logger) 33 34 if err := cmd.Run(ctx, os.Args); err != nil { 35 logger.Error(err.Error())
+9 -4
cmd/spindle/main.go
··· 2 3 import ( 4 "context" 5 "os" 6 7 - "tangled.org/core/log" 8 "tangled.org/core/spindle" 9 - _ "tangled.org/core/tid" 10 ) 11 12 func main() { 13 - ctx := log.NewContext(context.Background(), "spindle") 14 err := spindle.Run(ctx) 15 if err != nil { 16 - log.FromContext(ctx).Error("error running spindle", "error", err) 17 os.Exit(-1) 18 } 19 }
··· 2 3 import ( 4 "context" 5 + "log/slog" 6 "os" 7 8 + tlog "tangled.org/core/log" 9 "tangled.org/core/spindle" 10 ) 11 12 func main() { 13 + logger := tlog.New("spindle") 14 + slog.SetDefault(logger) 15 + 16 + ctx := context.Background() 17 + ctx = tlog.IntoContext(ctx, logger) 18 + 19 err := spindle.Run(ctx) 20 if err != nil { 21 + logger.Error("error running spindle", "error", err) 22 os.Exit(-1) 23 } 24 }
+13
go.mod
··· 60 github.com/ProtonMail/go-crypto v1.3.0 // indirect 61 github.com/alecthomas/repr v0.4.0 // indirect 62 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 63 github.com/aymerick/douceur v0.2.0 // indirect 64 github.com/beorn7/perks v1.0.1 // indirect 65 github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect 66 github.com/casbin/govaluate v1.3.0 // indirect 67 github.com/cenkalti/backoff/v4 v4.3.0 // indirect 68 github.com/cespare/xxhash/v2 v2.3.0 // indirect 69 github.com/cloudflare/circl v1.6.2-0.20250618153321-aa837fd1539d // indirect 70 github.com/containerd/errdefs v1.0.0 // indirect 71 github.com/containerd/errdefs/pkg v0.3.0 // indirect ··· 84 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 85 github.com/go-git/go-billy/v5 v5.6.2 // indirect 86 github.com/go-jose/go-jose/v3 v3.0.4 // indirect 87 github.com/go-logr/logr v1.4.3 // indirect 88 github.com/go-logr/stdr v1.2.2 // indirect 89 github.com/go-redis/cache/v9 v9.0.0 // indirect ··· 126 github.com/lestrrat-go/httprc v1.0.6 // indirect 127 github.com/lestrrat-go/iter v1.0.2 // indirect 128 github.com/lestrrat-go/option v1.0.1 // indirect 129 github.com/mattn/go-isatty v0.0.20 // indirect 130 github.com/minio/sha256-simd v1.0.1 // indirect 131 github.com/mitchellh/mapstructure v1.5.0 // indirect 132 github.com/moby/docker-image-spec v1.3.1 // indirect ··· 134 github.com/moby/term v0.5.2 // indirect 135 github.com/morikuni/aec v1.0.0 // indirect 136 github.com/mr-tron/base58 v1.2.0 // indirect 137 github.com/multiformats/go-base32 v0.1.0 // indirect 138 github.com/multiformats/go-base36 v0.2.0 // indirect 139 github.com/multiformats/go-multibase v0.2.0 // indirect ··· 152 github.com/prometheus/client_model v0.6.2 // indirect 153 github.com/prometheus/common v0.64.0 // indirect 154 github.com/prometheus/procfs v0.16.1 // indirect 155 github.com/ryanuber/go-glob v1.0.0 // indirect 156 github.com/segmentio/asm v1.2.0 // indirect 157 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect ··· 160 github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 161 github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 162 github.com/wyatt915/treeblood v0.1.15 // indirect 163 gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab // indirect 164 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 165 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
··· 60 github.com/ProtonMail/go-crypto v1.3.0 // indirect 61 github.com/alecthomas/repr v0.4.0 // indirect 62 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 63 + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 64 github.com/aymerick/douceur v0.2.0 // indirect 65 github.com/beorn7/perks v1.0.1 // indirect 66 github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect 67 github.com/casbin/govaluate v1.3.0 // indirect 68 github.com/cenkalti/backoff/v4 v4.3.0 // indirect 69 github.com/cespare/xxhash/v2 v2.3.0 // indirect 70 + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect 71 + github.com/charmbracelet/lipgloss v1.1.0 // indirect 72 + github.com/charmbracelet/log v0.4.2 // indirect 73 + github.com/charmbracelet/x/ansi v0.8.0 // indirect 74 + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect 75 + github.com/charmbracelet/x/term v0.2.1 // indirect 76 github.com/cloudflare/circl v1.6.2-0.20250618153321-aa837fd1539d // indirect 77 github.com/containerd/errdefs v1.0.0 // indirect 78 github.com/containerd/errdefs/pkg v0.3.0 // indirect ··· 91 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 92 github.com/go-git/go-billy/v5 v5.6.2 // indirect 93 github.com/go-jose/go-jose/v3 v3.0.4 // indirect 94 + github.com/go-logfmt/logfmt v0.6.0 // indirect 95 github.com/go-logr/logr v1.4.3 // indirect 96 github.com/go-logr/stdr v1.2.2 // indirect 97 github.com/go-redis/cache/v9 v9.0.0 // indirect ··· 134 github.com/lestrrat-go/httprc v1.0.6 // indirect 135 github.com/lestrrat-go/iter v1.0.2 // indirect 136 github.com/lestrrat-go/option v1.0.1 // indirect 137 + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 138 github.com/mattn/go-isatty v0.0.20 // indirect 139 + github.com/mattn/go-runewidth v0.0.16 // indirect 140 github.com/minio/sha256-simd v1.0.1 // indirect 141 github.com/mitchellh/mapstructure v1.5.0 // indirect 142 github.com/moby/docker-image-spec v1.3.1 // indirect ··· 144 github.com/moby/term v0.5.2 // indirect 145 github.com/morikuni/aec v1.0.0 // indirect 146 github.com/mr-tron/base58 v1.2.0 // indirect 147 + github.com/muesli/termenv v0.16.0 // indirect 148 github.com/multiformats/go-base32 v0.1.0 // indirect 149 github.com/multiformats/go-base36 v0.2.0 // indirect 150 github.com/multiformats/go-multibase v0.2.0 // indirect ··· 163 github.com/prometheus/client_model v0.6.2 // indirect 164 github.com/prometheus/common v0.64.0 // indirect 165 github.com/prometheus/procfs v0.16.1 // indirect 166 + github.com/rivo/uniseg v0.4.7 // indirect 167 github.com/ryanuber/go-glob v1.0.0 // indirect 168 github.com/segmentio/asm v1.2.0 // indirect 169 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect ··· 172 github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 173 github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 174 github.com/wyatt915/treeblood v0.1.15 // indirect 175 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 176 gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab // indirect 177 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 178 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
+27
go.sum
··· 19 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 20 github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk= 21 github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= 22 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 23 github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 24 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= ··· 48 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 49 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 50 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 51 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 52 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 53 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= ··· 120 github.com/go-git/go-git-fixtures/v5 v5.0.0-20241203230421-0753e18f8f03/go.mod h1:hMKrMnUE4W0SJ7bFyM00dyz/HoknZoptGWzrj6M+dEM= 121 github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= 122 github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= 123 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 124 github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 125 github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= ··· 276 github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= 277 github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 278 github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 279 github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 280 github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 281 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 282 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 283 github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= 284 github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 285 github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= ··· 300 github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 301 github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 302 github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 303 github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= 304 github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 305 github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= ··· 377 github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= 378 github.com/resend/resend-go/v2 v2.15.0 h1:B6oMEPf8IEQwn2Ovx/9yymkESLDSeNfLFaNMw+mzHhE= 379 github.com/resend/resend-go/v2 v2.15.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ= 380 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 381 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 382 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= ··· 434 github.com/wyatt915/goldmark-treeblood v0.0.0-20250825231212-5dcbdb2f4b57/go.mod h1:BxSCWByWSRSuembL3cDG1IBUbkBoO/oW/6tF19aA4hs= 435 github.com/wyatt915/treeblood v0.1.15 h1:3KZ3o2LpcKZAzOLqMoW9qeUzKEaKArKpbcPpTkNfQC8= 436 github.com/wyatt915/treeblood v0.1.15/go.mod h1:i7+yhhmzdDP17/97pIsOSffw74EK/xk+qJ0029cSXUY= 437 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 438 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 439 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
··· 19 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 20 github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk= 21 github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= 22 + github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 23 + github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 24 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 25 github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 26 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= ··· 50 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 51 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 52 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 53 + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= 54 + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= 55 + github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= 56 + github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= 57 + github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig= 58 + github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= 59 + github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= 60 + github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= 61 + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= 62 + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 63 + github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 64 + github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 65 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 66 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 67 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= ··· 134 github.com/go-git/go-git-fixtures/v5 v5.0.0-20241203230421-0753e18f8f03/go.mod h1:hMKrMnUE4W0SJ7bFyM00dyz/HoknZoptGWzrj6M+dEM= 135 github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= 136 github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= 137 + github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= 138 + github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 139 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 140 github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 141 github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= ··· 292 github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= 293 github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 294 github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 295 + github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 296 + github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 297 github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 298 github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 299 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 300 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 301 + github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 302 + github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 303 github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= 304 github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 305 github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= ··· 320 github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 321 github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 322 github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 323 + github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= 324 + github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 325 github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= 326 github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 327 github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= ··· 399 github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= 400 github.com/resend/resend-go/v2 v2.15.0 h1:B6oMEPf8IEQwn2Ovx/9yymkESLDSeNfLFaNMw+mzHhE= 401 github.com/resend/resend-go/v2 v2.15.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ= 402 + github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 403 + github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 404 + github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 405 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 406 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 407 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= ··· 459 github.com/wyatt915/goldmark-treeblood v0.0.0-20250825231212-5dcbdb2f4b57/go.mod h1:BxSCWByWSRSuembL3cDG1IBUbkBoO/oW/6tF19aA4hs= 460 github.com/wyatt915/treeblood v0.1.15 h1:3KZ3o2LpcKZAzOLqMoW9qeUzKEaKArKpbcPpTkNfQC8= 461 github.com/wyatt915/treeblood v0.1.15/go.mod h1:i7+yhhmzdDP17/97pIsOSffw74EK/xk+qJ0029cSXUY= 462 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 463 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 464 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 465 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 466 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+1 -1
jetstream/jetstream.go
··· 114 115 sched := sequential.NewScheduler(j.ident, logger, j.withDidFilter(processFunc)) 116 117 - client, err := client.NewClient(j.cfg, log.New("jetstream"), sched) 118 if err != nil { 119 return fmt.Errorf("failed to create jetstream client: %w", err) 120 }
··· 114 115 sched := sequential.NewScheduler(j.ident, logger, j.withDidFilter(processFunc)) 116 117 + client, err := client.NewClient(j.cfg, logger, sched) 118 if err != nil { 119 return fmt.Errorf("failed to create jetstream client: %w", err) 120 }
+2 -3
knotserver/events.go
··· 8 "time" 9 10 "github.com/gorilla/websocket" 11 ) 12 13 var upgrader = websocket.Upgrader{ ··· 16 } 17 18 func (h *Knot) Events(w http.ResponseWriter, r *http.Request) { 19 - l := h.l.With("handler", "OpLog") 20 l.Debug("received new connection") 21 22 conn, err := upgrader.Upgrade(w, r, nil) ··· 75 } 76 case <-time.After(30 * time.Second): 77 // send a keep-alive 78 - l.Debug("sent keepalive") 79 if err = conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(time.Second)); err != nil { 80 l.Error("failed to write control", "err", err) 81 } ··· 89 h.l.Error("failed to fetch events from db", "err", err, "cursor", cursor) 90 return err 91 } 92 - h.l.Debug("ops", "ops", events) 93 94 for _, event := range events { 95 // first extract the inner json into a map
··· 8 "time" 9 10 "github.com/gorilla/websocket" 11 + "tangled.org/core/log" 12 ) 13 14 var upgrader = websocket.Upgrader{ ··· 17 } 18 19 func (h *Knot) Events(w http.ResponseWriter, r *http.Request) { 20 + l := log.SubLogger(h.l, "eventstream") 21 l.Debug("received new connection") 22 23 conn, err := upgrader.Upgrade(w, r, nil) ··· 76 } 77 case <-time.After(30 * time.Second): 78 // send a keep-alive 79 if err = conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(time.Second)); err != nil { 80 l.Error("failed to write control", "err", err) 81 } ··· 89 h.l.Error("failed to fetch events from db", "err", err, "cursor", cursor) 90 return err 91 } 92 93 for _, event := range events { 94 // first extract the inner json into a map
+18 -18
knotserver/git.go
··· 13 "tangled.org/core/knotserver/git/service" 14 ) 15 16 - func (d *Knot) InfoRefs(w http.ResponseWriter, r *http.Request) { 17 did := chi.URLParam(r, "did") 18 name := chi.URLParam(r, "name") 19 repoName, err := securejoin.SecureJoin(did, name) 20 if err != nil { 21 gitError(w, "repository not found", http.StatusNotFound) 22 - d.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err) 23 return 24 } 25 26 - repoPath, err := securejoin.SecureJoin(d.c.Repo.ScanPath, repoName) 27 if err != nil { 28 gitError(w, "repository not found", http.StatusNotFound) 29 - d.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err) 30 return 31 } 32 ··· 46 47 if err := cmd.InfoRefs(); err != nil { 48 gitError(w, err.Error(), http.StatusInternalServerError) 49 - d.l.Error("git: process failed", "handler", "InfoRefs", "service", serviceName, "error", err) 50 return 51 } 52 case "git-receive-pack": 53 - d.RejectPush(w, r, name) 54 default: 55 gitError(w, fmt.Sprintf("service unsupported: '%s'", serviceName), http.StatusForbidden) 56 } 57 } 58 59 - func (d *Knot) UploadPack(w http.ResponseWriter, r *http.Request) { 60 did := chi.URLParam(r, "did") 61 name := chi.URLParam(r, "name") 62 - repo, err := securejoin.SecureJoin(d.c.Repo.ScanPath, filepath.Join(did, name)) 63 if err != nil { 64 gitError(w, err.Error(), http.StatusInternalServerError) 65 - d.l.Error("git: failed to secure join repo path", "handler", "UploadPack", "error", err) 66 return 67 } 68 ··· 77 gzipReader, err := gzip.NewReader(r.Body) 78 if err != nil { 79 gitError(w, err.Error(), http.StatusInternalServerError) 80 - d.l.Error("git: failed to create gzip reader", "handler", "UploadPack", "error", err) 81 return 82 } 83 defer gzipReader.Close() ··· 88 w.Header().Set("Connection", "Keep-Alive") 89 w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate") 90 91 - d.l.Info("git: executing git-upload-pack", "handler", "UploadPack", "repo", repo) 92 93 cmd := service.ServiceCommand{ 94 GitProtocol: r.Header.Get("Git-Protocol"), ··· 100 w.WriteHeader(http.StatusOK) 101 102 if err := cmd.UploadPack(); err != nil { 103 - d.l.Error("git: failed to execute git-upload-pack", "handler", "UploadPack", "error", err) 104 return 105 } 106 } 107 108 - func (d *Knot) ReceivePack(w http.ResponseWriter, r *http.Request) { 109 did := chi.URLParam(r, "did") 110 name := chi.URLParam(r, "name") 111 - _, err := securejoin.SecureJoin(d.c.Repo.ScanPath, filepath.Join(did, name)) 112 if err != nil { 113 gitError(w, err.Error(), http.StatusForbidden) 114 - d.l.Error("git: failed to secure join repo path", "handler", "ReceivePack", "error", err) 115 return 116 } 117 118 - d.RejectPush(w, r, name) 119 } 120 121 - func (d *Knot) RejectPush(w http.ResponseWriter, r *http.Request, unqualifiedRepoName string) { 122 // A text/plain response will cause git to print each line of the body 123 // prefixed with "remote: ". 124 w.Header().Set("content-type", "text/plain; charset=UTF-8") ··· 131 ownerHandle := r.Header.Get("x-tangled-repo-owner-handle") 132 ownerHandle = strings.TrimPrefix(ownerHandle, "@") 133 if ownerHandle != "" && !strings.ContainsAny(ownerHandle, ":") { 134 - hostname := d.c.Server.Hostname 135 if strings.Contains(hostname, ":") { 136 hostname = strings.Split(hostname, ":")[0] 137 }
··· 13 "tangled.org/core/knotserver/git/service" 14 ) 15 16 + func (h *Knot) InfoRefs(w http.ResponseWriter, r *http.Request) { 17 did := chi.URLParam(r, "did") 18 name := chi.URLParam(r, "name") 19 repoName, err := securejoin.SecureJoin(did, name) 20 if err != nil { 21 gitError(w, "repository not found", http.StatusNotFound) 22 + h.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err) 23 return 24 } 25 26 + repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, repoName) 27 if err != nil { 28 gitError(w, "repository not found", http.StatusNotFound) 29 + h.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err) 30 return 31 } 32 ··· 46 47 if err := cmd.InfoRefs(); err != nil { 48 gitError(w, err.Error(), http.StatusInternalServerError) 49 + h.l.Error("git: process failed", "handler", "InfoRefs", "service", serviceName, "error", err) 50 return 51 } 52 case "git-receive-pack": 53 + h.RejectPush(w, r, name) 54 default: 55 gitError(w, fmt.Sprintf("service unsupported: '%s'", serviceName), http.StatusForbidden) 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") 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 ··· 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", "UploadPack", "error", err) 81 return 82 } 83 defer gzipReader.Close() ··· 88 w.Header().Set("Connection", "Keep-Alive") 89 w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate") 90 91 + h.l.Info("git: executing git-upload-pack", "handler", "UploadPack", "repo", repo) 92 93 cmd := service.ServiceCommand{ 94 GitProtocol: r.Header.Get("Git-Protocol"), ··· 100 w.WriteHeader(http.StatusOK) 101 102 if err := cmd.UploadPack(); err != nil { 103 + h.l.Error("git: failed to execute git-upload-pack", "handler", "UploadPack", "error", err) 104 return 105 } 106 } 107 108 + func (h *Knot) ReceivePack(w http.ResponseWriter, r *http.Request) { 109 did := chi.URLParam(r, "did") 110 name := chi.URLParam(r, "name") 111 + _, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name)) 112 if err != nil { 113 gitError(w, err.Error(), http.StatusForbidden) 114 + h.l.Error("git: failed to secure join repo path", "handler", "ReceivePack", "error", err) 115 return 116 } 117 118 + h.RejectPush(w, r, name) 119 } 120 121 + func (h *Knot) RejectPush(w http.ResponseWriter, r *http.Request, unqualifiedRepoName string) { 122 // A text/plain response will cause git to print each line of the body 123 // prefixed with "remote: ". 124 w.Header().Set("content-type", "text/plain; charset=UTF-8") ··· 131 ownerHandle := r.Header.Get("x-tangled-repo-owner-handle") 132 ownerHandle = strings.TrimPrefix(ownerHandle, "@") 133 if ownerHandle != "" && !strings.ContainsAny(ownerHandle, ":") { 134 + hostname := h.c.Server.Hostname 135 if strings.Contains(hostname, ":") { 136 hostname = strings.Split(hostname, ":")[0] 137 }
+4 -1
knotserver/internal.go
··· 20 "tangled.org/core/knotserver/config" 21 "tangled.org/core/knotserver/db" 22 "tangled.org/core/knotserver/git" 23 "tangled.org/core/notifier" 24 "tangled.org/core/rbac" 25 "tangled.org/core/workflow" ··· 314 return h.db.InsertEvent(event, h.n) 315 } 316 317 - func Internal(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, l *slog.Logger, n *notifier.Notifier) http.Handler { 318 r := chi.NewRouter() 319 320 h := InternalHandle{ 321 db,
··· 20 "tangled.org/core/knotserver/config" 21 "tangled.org/core/knotserver/db" 22 "tangled.org/core/knotserver/git" 23 + "tangled.org/core/log" 24 "tangled.org/core/notifier" 25 "tangled.org/core/rbac" 26 "tangled.org/core/workflow" ··· 315 return h.db.InsertEvent(event, h.n) 316 } 317 318 + func Internal(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, n *notifier.Notifier) http.Handler { 319 r := chi.NewRouter() 320 + l := log.FromContext(ctx) 321 + l = log.SubLogger(l, "internal") 322 323 h := InternalHandle{ 324 db,
+35
knotserver/middleware.go
···
··· 1 + package knotserver 2 + 3 + import ( 4 + "log/slog" 5 + "net/http" 6 + "time" 7 + ) 8 + 9 + func (h *Knot) RequestLogger(next http.Handler) http.Handler { 10 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 11 + start := time.Now() 12 + 13 + next.ServeHTTP(w, r) 14 + 15 + // Build query params as slog.Attrs for the group 16 + queryParams := r.URL.Query() 17 + queryAttrs := make([]any, 0, len(queryParams)) 18 + for key, values := range queryParams { 19 + if len(values) == 1 { 20 + queryAttrs = append(queryAttrs, slog.String(key, values[0])) 21 + } else { 22 + queryAttrs = append(queryAttrs, slog.Any(key, values)) 23 + } 24 + } 25 + 26 + h.l.LogAttrs(r.Context(), slog.LevelInfo, "", 27 + slog.Group("request", 28 + slog.String("method", r.Method), 29 + slog.String("path", r.URL.Path), 30 + slog.Group("query", queryAttrs...), 31 + slog.Duration("duration", time.Since(start)), 32 + ), 33 + ) 34 + }) 35 + }
+16 -9
knotserver/router.go
··· 12 "tangled.org/core/knotserver/config" 13 "tangled.org/core/knotserver/db" 14 "tangled.org/core/knotserver/xrpc" 15 - tlog "tangled.org/core/log" 16 "tangled.org/core/notifier" 17 "tangled.org/core/rbac" 18 "tangled.org/core/xrpc/serviceauth" ··· 28 resolver *idresolver.Resolver 29 } 30 31 - func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, l *slog.Logger, n *notifier.Notifier) (http.Handler, error) { 32 - r := chi.NewRouter() 33 - 34 h := Knot{ 35 c: c, 36 db: db, 37 e: e, 38 - l: l, 39 jc: jc, 40 n: n, 41 resolver: idresolver.DefaultResolver(), ··· 67 return nil, fmt.Errorf("failed to start jetstream: %w", err) 68 } 69 70 r.Get("/", func(w http.ResponseWriter, r *http.Request) { 71 w.Write([]byte("This is a knot server. More info at https://tangled.sh")) 72 }) ··· 86 // Socket that streams git oplogs 87 r.Get("/events", h.Events) 88 89 - return r, nil 90 } 91 92 func (h *Knot) XrpcRouter() http.Handler { 93 - logger := tlog.New("knots") 94 95 - serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String()) 96 97 xrpc := &xrpc.Xrpc{ 98 Config: h.c, 99 Db: h.db, 100 Ingester: h.jc, 101 Enforcer: h.e, 102 - Logger: logger, 103 Notifier: h.n, 104 Resolver: h.resolver, 105 ServiceAuth: serviceAuth, 106 } 107 return xrpc.Router() 108 } 109
··· 12 "tangled.org/core/knotserver/config" 13 "tangled.org/core/knotserver/db" 14 "tangled.org/core/knotserver/xrpc" 15 + "tangled.org/core/log" 16 "tangled.org/core/notifier" 17 "tangled.org/core/rbac" 18 "tangled.org/core/xrpc/serviceauth" ··· 28 resolver *idresolver.Resolver 29 } 30 31 + func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, n *notifier.Notifier) (http.Handler, error) { 32 h := Knot{ 33 c: c, 34 db: db, 35 e: e, 36 + l: log.FromContext(ctx), 37 jc: jc, 38 n: n, 39 resolver: idresolver.DefaultResolver(), ··· 65 return nil, fmt.Errorf("failed to start jetstream: %w", err) 66 } 67 68 + return h.Router(), nil 69 + } 70 + 71 + func (h *Knot) Router() http.Handler { 72 + r := chi.NewRouter() 73 + 74 + r.Use(h.RequestLogger) 75 + 76 r.Get("/", func(w http.ResponseWriter, r *http.Request) { 77 w.Write([]byte("This is a knot server. More info at https://tangled.sh")) 78 }) ··· 92 // Socket that streams git oplogs 93 r.Get("/events", h.Events) 94 95 + return r 96 } 97 98 func (h *Knot) XrpcRouter() http.Handler { 99 + serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String()) 100 101 + l := log.SubLogger(h.l, "xrpc") 102 103 xrpc := &xrpc.Xrpc{ 104 Config: h.c, 105 Db: h.db, 106 Ingester: h.jc, 107 Enforcer: h.e, 108 + Logger: l, 109 Notifier: h.n, 110 Resolver: h.resolver, 111 ServiceAuth: serviceAuth, 112 } 113 + 114 return xrpc.Router() 115 } 116
+5 -4
knotserver/server.go
··· 43 44 func Run(ctx context.Context, cmd *cli.Command) error { 45 logger := log.FromContext(ctx) 46 - iLogger := log.New("knotserver/internal") 47 48 c, err := config.Load(ctx) 49 if err != nil { ··· 80 tangled.KnotMemberNSID, 81 tangled.RepoPullNSID, 82 tangled.RepoCollaboratorNSID, 83 - }, nil, logger, db, true, c.Server.LogDids) 84 if err != nil { 85 logger.Error("failed to setup jetstream", "error", err) 86 } 87 88 notifier := notifier.New() 89 90 - mux, err := Setup(ctx, c, db, e, jc, logger, &notifier) 91 if err != nil { 92 return fmt.Errorf("failed to setup server: %w", err) 93 } 94 95 - imux := Internal(ctx, c, db, e, iLogger, &notifier) 96 97 logger.Info("starting internal server", "address", c.Server.InternalListenAddr) 98 go http.ListenAndServe(c.Server.InternalListenAddr, imux)
··· 43 44 func Run(ctx context.Context, cmd *cli.Command) error { 45 logger := log.FromContext(ctx) 46 + logger = log.SubLogger(logger, cmd.Name) 47 + ctx = log.IntoContext(ctx, logger) 48 49 c, err := config.Load(ctx) 50 if err != nil { ··· 81 tangled.KnotMemberNSID, 82 tangled.RepoPullNSID, 83 tangled.RepoCollaboratorNSID, 84 + }, nil, log.SubLogger(logger, "jetstream"), db, true, c.Server.LogDids) 85 if err != nil { 86 logger.Error("failed to setup jetstream", "error", err) 87 } 88 89 notifier := notifier.New() 90 91 + mux, err := Setup(ctx, c, db, e, jc, &notifier) 92 if err != nil { 93 return fmt.Errorf("failed to setup server: %w", err) 94 } 95 96 + imux := Internal(ctx, c, db, e, &notifier) 97 98 logger.Info("starting internal server", "address", c.Server.InternalListenAddr) 99 go http.ListenAndServe(c.Server.InternalListenAddr, imux)
+23 -9
log/log.go
··· 4 "context" 5 "log/slog" 6 "os" 7 ) 8 9 - // NewHandler sets up a new slog.Handler with the service name 10 - // as an attribute 11 func NewHandler(name string) slog.Handler { 12 - handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ 13 - Level: slog.LevelDebug, 14 }) 15 - 16 - var attrs []slog.Attr 17 - attrs = append(attrs, slog.Attr{Key: "service", Value: slog.StringValue(name)}) 18 - handler.WithAttrs(attrs) 19 - return handler 20 } 21 22 func New(name string) *slog.Logger { ··· 49 50 return slog.Default() 51 }
··· 4 "context" 5 "log/slog" 6 "os" 7 + 8 + "github.com/charmbracelet/log" 9 ) 10 11 func NewHandler(name string) slog.Handler { 12 + return log.NewWithOptions(os.Stderr, log.Options{ 13 + ReportTimestamp: true, 14 + Prefix: name, 15 + Level: log.DebugLevel, 16 }) 17 } 18 19 func New(name string) *slog.Logger { ··· 46 47 return slog.Default() 48 } 49 + 50 + // sublogger derives a new logger from an existing one by appending a suffix to its prefix. 51 + func SubLogger(base *slog.Logger, suffix string) *slog.Logger { 52 + // try to get the underlying charmbracelet logger 53 + if cl, ok := base.Handler().(*log.Logger); ok { 54 + prefix := cl.GetPrefix() 55 + if prefix != "" { 56 + prefix = prefix + "/" + suffix 57 + } else { 58 + prefix = suffix 59 + } 60 + return slog.New(NewHandler(prefix)) 61 + } 62 + 63 + // Fallback: no known handler type 64 + return slog.New(NewHandler(suffix)) 65 + }
+39
nix/gomod2nix.toml
··· 29 [mod."github.com/avast/retry-go/v4"] 30 version = "v4.6.1" 31 hash = "sha256-PeZc8k4rDV64+k8nZt/oy1YNVbLevltXP3ZD1jf6Z6k=" 32 [mod."github.com/aymerick/douceur"] 33 version = "v0.2.0" 34 hash = "sha256-NiBX8EfOvLXNiK3pJaZX4N73YgfzdrzRXdiBFe3X3sE=" ··· 63 [mod."github.com/cespare/xxhash/v2"] 64 version = "v2.3.0" 65 hash = "sha256-7hRlwSR+fos1kx4VZmJ/7snR7zHh8ZFKX+qqqqGcQpY=" 66 [mod."github.com/cloudflare/circl"] 67 version = "v1.6.2-0.20250618153321-aa837fd1539d" 68 hash = "sha256-0s/i/XmMcuvPQ+qK9OIU5KxwYZyLVXRtdlYvIXRJT3Y=" ··· 145 [mod."github.com/go-jose/go-jose/v3"] 146 version = "v3.0.4" 147 hash = "sha256-RrLHCu9l6k0XVobdZQJ9Sx/VTQcWjrdLR5BEG7yXTEQ=" 148 [mod."github.com/go-logr/logr"] 149 version = "v1.4.3" 150 hash = "sha256-Nnp/dEVNMxLp3RSPDHZzGbI8BkSNuZMX0I0cjWKXXLA=" ··· 298 [mod."github.com/lestrrat-go/option"] 299 version = "v1.0.1" 300 hash = "sha256-jVcIYYVsxElIS/l2akEw32vdEPR8+anR6oeT1FoYULI=" 301 [mod."github.com/mattn/go-isatty"] 302 version = "v0.0.20" 303 hash = "sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ=" 304 [mod."github.com/mattn/go-sqlite3"] 305 version = "v1.14.24" 306 hash = "sha256-taGKFZFQlR5++5b2oZ1dYS3RERKv6yh1gniNWhb4egg=" ··· 328 [mod."github.com/mr-tron/base58"] 329 version = "v1.2.0" 330 hash = "sha256-8FzMu3kHUbBX10pUdtGf59Ag7BNupx8ZHeUaodR1/Vk=" 331 [mod."github.com/multiformats/go-base32"] 332 version = "v0.1.0" 333 hash = "sha256-O2IM7FB+Y9MkDdZztyQL5F8oEnmON2Yew7XkotQziio=" ··· 394 [mod."github.com/resend/resend-go/v2"] 395 version = "v2.15.0" 396 hash = "sha256-1lMoxuMLQXaNWFKadS6rpztAKwvIl3/LWMXqw7f5WYg=" 397 [mod."github.com/ryanuber/go-glob"] 398 version = "v1.0.0" 399 hash = "sha256-YkMl1utwUhi3E0sHK23ISpAsPyj4+KeXyXKoFYGXGVY=" ··· 440 [mod."github.com/wyatt915/treeblood"] 441 version = "v0.1.15" 442 hash = "sha256-hb99exdkoY2Qv8WdDxhwgPXGbEYimUr6wFtPXEvcO9g=" 443 [mod."github.com/yuin/goldmark"] 444 version = "v1.7.13" 445 hash = "sha256-vBCxZrPYPc8x/nvAAv3Au59dCCyfS80Vw3/a9EXK7TE="
··· 29 [mod."github.com/avast/retry-go/v4"] 30 version = "v4.6.1" 31 hash = "sha256-PeZc8k4rDV64+k8nZt/oy1YNVbLevltXP3ZD1jf6Z6k=" 32 + [mod."github.com/aymanbagabas/go-osc52/v2"] 33 + version = "v2.0.1" 34 + hash = "sha256-6Bp0jBZ6npvsYcKZGHHIUSVSTAMEyieweAX2YAKDjjg=" 35 [mod."github.com/aymerick/douceur"] 36 version = "v0.2.0" 37 hash = "sha256-NiBX8EfOvLXNiK3pJaZX4N73YgfzdrzRXdiBFe3X3sE=" ··· 66 [mod."github.com/cespare/xxhash/v2"] 67 version = "v2.3.0" 68 hash = "sha256-7hRlwSR+fos1kx4VZmJ/7snR7zHh8ZFKX+qqqqGcQpY=" 69 + [mod."github.com/charmbracelet/colorprofile"] 70 + version = "v0.2.3-0.20250311203215-f60798e515dc" 71 + hash = "sha256-D9E/bMOyLXAUVOHA1/6o3i+vVmLfwIMOWib6sU7A6+Q=" 72 + [mod."github.com/charmbracelet/lipgloss"] 73 + version = "v1.1.0" 74 + hash = "sha256-RHsRT2EZ1nDOElxAK+6/DC9XAaGVjDTgPvRh3pyCfY4=" 75 + [mod."github.com/charmbracelet/log"] 76 + version = "v0.4.2" 77 + hash = "sha256-3w1PCM/c4JvVEh2d0sMfv4C77Xs1bPa1Ea84zdynC7I=" 78 + [mod."github.com/charmbracelet/x/ansi"] 79 + version = "v0.8.0" 80 + hash = "sha256-/YyDkGrULV2BtnNk3ojeSl0nUWQwIfIdW7WJuGbAZas=" 81 + [mod."github.com/charmbracelet/x/cellbuf"] 82 + version = "v0.0.13-0.20250311204145-2c3ea96c31dd" 83 + hash = "sha256-XAhCOt8qJ2vR77lH1ez0IVU1/2CaLTq9jSmrHVg5HHU=" 84 + [mod."github.com/charmbracelet/x/term"] 85 + version = "v0.2.1" 86 + hash = "sha256-VBkCZLI90PhMasftGw3403IqoV7d3E5WEGAIVrN5xQM=" 87 [mod."github.com/cloudflare/circl"] 88 version = "v1.6.2-0.20250618153321-aa837fd1539d" 89 hash = "sha256-0s/i/XmMcuvPQ+qK9OIU5KxwYZyLVXRtdlYvIXRJT3Y=" ··· 166 [mod."github.com/go-jose/go-jose/v3"] 167 version = "v3.0.4" 168 hash = "sha256-RrLHCu9l6k0XVobdZQJ9Sx/VTQcWjrdLR5BEG7yXTEQ=" 169 + [mod."github.com/go-logfmt/logfmt"] 170 + version = "v0.6.0" 171 + hash = "sha256-RtIG2qARd5sT10WQ7F3LR8YJhS8exs+KiuUiVf75bWg=" 172 [mod."github.com/go-logr/logr"] 173 version = "v1.4.3" 174 hash = "sha256-Nnp/dEVNMxLp3RSPDHZzGbI8BkSNuZMX0I0cjWKXXLA=" ··· 322 [mod."github.com/lestrrat-go/option"] 323 version = "v1.0.1" 324 hash = "sha256-jVcIYYVsxElIS/l2akEw32vdEPR8+anR6oeT1FoYULI=" 325 + [mod."github.com/lucasb-eyer/go-colorful"] 326 + version = "v1.2.0" 327 + hash = "sha256-Gg9dDJFCTaHrKHRR1SrJgZ8fWieJkybljybkI9x0gyE=" 328 [mod."github.com/mattn/go-isatty"] 329 version = "v0.0.20" 330 hash = "sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ=" 331 + [mod."github.com/mattn/go-runewidth"] 332 + version = "v0.0.16" 333 + hash = "sha256-NC+ntvwIpqDNmXb7aixcg09il80ygq6JAnW0Gb5b/DQ=" 334 [mod."github.com/mattn/go-sqlite3"] 335 version = "v1.14.24" 336 hash = "sha256-taGKFZFQlR5++5b2oZ1dYS3RERKv6yh1gniNWhb4egg=" ··· 358 [mod."github.com/mr-tron/base58"] 359 version = "v1.2.0" 360 hash = "sha256-8FzMu3kHUbBX10pUdtGf59Ag7BNupx8ZHeUaodR1/Vk=" 361 + [mod."github.com/muesli/termenv"] 362 + version = "v0.16.0" 363 + hash = "sha256-hGo275DJlyLtcifSLpWnk8jardOksdeX9lH4lBeE3gI=" 364 [mod."github.com/multiformats/go-base32"] 365 version = "v0.1.0" 366 hash = "sha256-O2IM7FB+Y9MkDdZztyQL5F8oEnmON2Yew7XkotQziio=" ··· 427 [mod."github.com/resend/resend-go/v2"] 428 version = "v2.15.0" 429 hash = "sha256-1lMoxuMLQXaNWFKadS6rpztAKwvIl3/LWMXqw7f5WYg=" 430 + [mod."github.com/rivo/uniseg"] 431 + version = "v0.4.7" 432 + hash = "sha256-rDcdNYH6ZD8KouyyiZCUEy8JrjOQoAkxHBhugrfHjFo=" 433 [mod."github.com/ryanuber/go-glob"] 434 version = "v1.0.0" 435 hash = "sha256-YkMl1utwUhi3E0sHK23ISpAsPyj4+KeXyXKoFYGXGVY=" ··· 476 [mod."github.com/wyatt915/treeblood"] 477 version = "v0.1.15" 478 hash = "sha256-hb99exdkoY2Qv8WdDxhwgPXGbEYimUr6wFtPXEvcO9g=" 479 + [mod."github.com/xo/terminfo"] 480 + version = "v0.0.0-20220910002029-abceb7e1c41e" 481 + hash = "sha256-GyCDxxMQhXA3Pi/TsWXpA8cX5akEoZV7CFx4RO3rARU=" 482 [mod."github.com/yuin/goldmark"] 483 version = "v1.7.13" 484 hash = "sha256-vBCxZrPYPc8x/nvAAv3Au59dCCyfS80Vw3/a9EXK7TE="
+35
spindle/middleware.go
···
··· 1 + package spindle 2 + 3 + import ( 4 + "log/slog" 5 + "net/http" 6 + "time" 7 + ) 8 + 9 + func (s *Spindle) RequestLogger(next http.Handler) http.Handler { 10 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 11 + start := time.Now() 12 + 13 + next.ServeHTTP(w, r) 14 + 15 + // Build query params as slog.Attrs for the group 16 + queryParams := r.URL.Query() 17 + queryAttrs := make([]any, 0, len(queryParams)) 18 + for key, values := range queryParams { 19 + if len(values) == 1 { 20 + queryAttrs = append(queryAttrs, slog.String(key, values[0])) 21 + } else { 22 + queryAttrs = append(queryAttrs, slog.Any(key, values)) 23 + } 24 + } 25 + 26 + s.l.LogAttrs(r.Context(), slog.LevelInfo, "", 27 + slog.Group("request", 28 + slog.String("method", r.Method), 29 + slog.String("path", r.URL.Path), 30 + slog.Group("query", queryAttrs...), 31 + slog.Duration("duration", time.Since(start)), 32 + ), 33 + ) 34 + }) 35 + }
+6 -6
spindle/server.go
··· 108 tangled.RepoNSID, 109 tangled.RepoCollaboratorNSID, 110 } 111 - jc, err := jetstream.NewJetstreamClient(cfg.Server.JetstreamEndpoint, "spindle", collections, nil, logger, d, true, true) 112 if err != nil { 113 return fmt.Errorf("failed to setup jetstream client: %w", err) 114 } ··· 171 // spindle.processPipeline, which in turn enqueues the pipeline 172 // job in the above registered queue. 173 ccfg := eventconsumer.NewConsumerConfig() 174 - ccfg.Logger = logger 175 ccfg.Dev = cfg.Server.Dev 176 ccfg.ProcessFunc = spindle.processPipeline 177 ccfg.CursorStore = cursorStore ··· 210 } 211 212 func (s *Spindle) XrpcRouter() http.Handler { 213 - logger := s.l.With("route", "xrpc") 214 - 215 serviceAuth := serviceauth.NewServiceAuth(s.l, s.res, s.cfg.Server.Did().String()) 216 217 x := xrpc.Xrpc{ 218 - Logger: logger, 219 Db: s.db, 220 Enforcer: s.e, 221 Engines: s.engs, ··· 305 306 ok := s.jq.Enqueue(queue.Job{ 307 Run: func() error { 308 - engine.StartWorkflows(s.l, s.vault, s.cfg, s.db, s.n, ctx, &models.Pipeline{ 309 RepoOwner: tpl.TriggerMetadata.Repo.Did, 310 RepoName: tpl.TriggerMetadata.Repo.Repo, 311 Workflows: workflows,
··· 108 tangled.RepoNSID, 109 tangled.RepoCollaboratorNSID, 110 } 111 + jc, err := jetstream.NewJetstreamClient(cfg.Server.JetstreamEndpoint, "spindle", collections, nil, log.SubLogger(logger, "jetstream"), d, true, true) 112 if err != nil { 113 return fmt.Errorf("failed to setup jetstream client: %w", err) 114 } ··· 171 // spindle.processPipeline, which in turn enqueues the pipeline 172 // job in the above registered queue. 173 ccfg := eventconsumer.NewConsumerConfig() 174 + ccfg.Logger = log.SubLogger(logger, "eventconsumer") 175 ccfg.Dev = cfg.Server.Dev 176 ccfg.ProcessFunc = spindle.processPipeline 177 ccfg.CursorStore = cursorStore ··· 210 } 211 212 func (s *Spindle) XrpcRouter() http.Handler { 213 serviceAuth := serviceauth.NewServiceAuth(s.l, s.res, s.cfg.Server.Did().String()) 214 215 + l := log.SubLogger(s.l, "xrpc") 216 + 217 x := xrpc.Xrpc{ 218 + Logger: l, 219 Db: s.db, 220 Enforcer: s.e, 221 Engines: s.engs, ··· 305 306 ok := s.jq.Enqueue(queue.Job{ 307 Run: func() error { 308 + engine.StartWorkflows(log.SubLogger(s.l, "engine"), s.vault, s.cfg, s.db, s.n, ctx, &models.Pipeline{ 309 RepoOwner: tpl.TriggerMetadata.Repo.Did, 310 RepoName: tpl.TriggerMetadata.Repo.Repo, 311 Workflows: workflows,
+3 -3
spindle/stream.go
··· 10 "strconv" 11 "time" 12 13 "tangled.org/core/spindle/models" 14 15 "github.com/go-chi/chi/v5" ··· 23 } 24 25 func (s *Spindle) Events(w http.ResponseWriter, r *http.Request) { 26 - l := s.l.With("handler", "Events") 27 l.Debug("received new connection") 28 29 conn, err := upgrader.Upgrade(w, r, nil) ··· 82 } 83 case <-time.After(30 * time.Second): 84 // send a keep-alive 85 - l.Debug("sent keepalive") 86 if err = conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(time.Second)); err != nil { 87 l.Error("failed to write control", "err", err) 88 } ··· 222 s.l.Debug("err", "err", err) 223 return err 224 } 225 - s.l.Debug("ops", "ops", events) 226 227 for _, event := range events { 228 // first extract the inner json into a map
··· 10 "strconv" 11 "time" 12 13 + "tangled.org/core/log" 14 "tangled.org/core/spindle/models" 15 16 "github.com/go-chi/chi/v5" ··· 24 } 25 26 func (s *Spindle) Events(w http.ResponseWriter, r *http.Request) { 27 + l := log.SubLogger(s.l, "eventstream") 28 + 29 l.Debug("received new connection") 30 31 conn, err := upgrader.Upgrade(w, r, nil) ··· 84 } 85 case <-time.After(30 * time.Second): 86 // send a keep-alive 87 if err = conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(time.Second)); err != nil { 88 l.Error("failed to write control", "err", err) 89 } ··· 223 s.l.Debug("err", "err", err) 224 return err 225 } 226 227 for _, event := range events { 228 // first extract the inner json into a map
+5 -4
xrpc/serviceauth/service_auth.go
··· 9 10 "github.com/bluesky-social/indigo/atproto/auth" 11 "tangled.org/core/idresolver" 12 xrpcerr "tangled.org/core/xrpc/errors" 13 ) 14 ··· 22 23 func NewServiceAuth(logger *slog.Logger, resolver *idresolver.Resolver, audienceDid string) *ServiceAuth { 24 return &ServiceAuth{ 25 - logger: logger, 26 resolver: resolver, 27 audienceDid: audienceDid, 28 } ··· 30 31 func (sa *ServiceAuth) VerifyServiceAuth(next http.Handler) http.Handler { 32 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 - l := sa.logger.With("url", r.URL) 34 - 35 token := r.Header.Get("Authorization") 36 token = strings.TrimPrefix(token, "Bearer ") 37 ··· 42 43 did, err := s.Validate(r.Context(), token, nil) 44 if err != nil { 45 - l.Error("signature verification failed", "err", err) 46 writeError(w, xrpcerr.AuthError(err), http.StatusForbidden) 47 return 48 } 49 50 r = r.WithContext( 51 context.WithValue(r.Context(), ActorDid, did),
··· 9 10 "github.com/bluesky-social/indigo/atproto/auth" 11 "tangled.org/core/idresolver" 12 + "tangled.org/core/log" 13 xrpcerr "tangled.org/core/xrpc/errors" 14 ) 15 ··· 23 24 func NewServiceAuth(logger *slog.Logger, resolver *idresolver.Resolver, audienceDid string) *ServiceAuth { 25 return &ServiceAuth{ 26 + logger: log.SubLogger(logger, "serviceauth"), 27 resolver: resolver, 28 audienceDid: audienceDid, 29 } ··· 31 32 func (sa *ServiceAuth) VerifyServiceAuth(next http.Handler) http.Handler { 33 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 34 token := r.Header.Get("Authorization") 35 token = strings.TrimPrefix(token, "Bearer ") 36 ··· 41 42 did, err := s.Validate(r.Context(), token, nil) 43 if err != nil { 44 + sa.logger.Error("signature verification failed", "err", err) 45 writeError(w, xrpcerr.AuthError(err), http.StatusForbidden) 46 return 47 } 48 + 49 + sa.logger.Debug("valid signature", ActorDid, did) 50 51 r = r.WithContext( 52 context.WithValue(r.Context(), ActorDid, did),