forked from tangled.org/core
Monorepo for Tangled

Compare changes

Choose any two refs to compare.

+1643 -2956
-34
api/tangled/pipelinecancelPipeline.go
··· 1 - // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 - 3 - package tangled 4 - 5 - // schema: sh.tangled.pipeline.cancelPipeline 6 - 7 - import ( 8 - "context" 9 - 10 - "github.com/bluesky-social/indigo/lex/util" 11 - ) 12 - 13 - const ( 14 - PipelineCancelPipelineNSID = "sh.tangled.pipeline.cancelPipeline" 15 - ) 16 - 17 - // PipelineCancelPipeline_Input is the input argument to a sh.tangled.pipeline.cancelPipeline call. 18 - type PipelineCancelPipeline_Input struct { 19 - // pipeline: pipeline at-uri 20 - Pipeline string `json:"pipeline" cborgen:"pipeline"` 21 - // repo: repo at-uri, spindle can't resolve repo from pipeline at-uri yet 22 - Repo string `json:"repo" cborgen:"repo"` 23 - // workflow: workflow name 24 - Workflow string `json:"workflow" cborgen:"workflow"` 25 - } 26 - 27 - // PipelineCancelPipeline calls the XRPC method "sh.tangled.pipeline.cancelPipeline". 28 - func PipelineCancelPipeline(ctx context.Context, c util.LexClient, input *PipelineCancelPipeline_Input) error { 29 - if err := c.LexDo(ctx, util.Procedure, "application/json", "sh.tangled.pipeline.cancelPipeline", nil, input, nil); err != nil { 30 - return err 31 - } 32 - 33 - return nil 34 - }
···
+56
appview/db/issues.go
··· 295 return GetIssuesPaginated(e, pagination.Page{}, filters...) 296 } 297 298 func AddIssueComment(tx *sql.Tx, c models.IssueComment) (int64, error) { 299 result, err := tx.Exec( 300 `insert into issue_comments (
··· 295 return GetIssuesPaginated(e, pagination.Page{}, filters...) 296 } 297 298 + // GetIssueIDs gets list of all existing issue's IDs 299 + func GetIssueIDs(e Execer, opts models.IssueSearchOptions) ([]int64, error) { 300 + var ids []int64 301 + 302 + var filters []orm.Filter 303 + openValue := 0 304 + if opts.IsOpen { 305 + openValue = 1 306 + } 307 + filters = append(filters, orm.FilterEq("open", openValue)) 308 + if opts.RepoAt != "" { 309 + filters = append(filters, orm.FilterEq("repo_at", opts.RepoAt)) 310 + } 311 + 312 + var conditions []string 313 + var args []any 314 + 315 + for _, filter := range filters { 316 + conditions = append(conditions, filter.Condition()) 317 + args = append(args, filter.Arg()...) 318 + } 319 + 320 + whereClause := "" 321 + if conditions != nil { 322 + whereClause = " where " + strings.Join(conditions, " and ") 323 + } 324 + query := fmt.Sprintf( 325 + ` 326 + select 327 + id 328 + from 329 + issues 330 + %s 331 + limit ? offset ?`, 332 + whereClause, 333 + ) 334 + args = append(args, opts.Page.Limit, opts.Page.Offset) 335 + rows, err := e.Query(query, args...) 336 + if err != nil { 337 + return nil, err 338 + } 339 + defer rows.Close() 340 + 341 + for rows.Next() { 342 + var id int64 343 + err := rows.Scan(&id) 344 + if err != nil { 345 + return nil, err 346 + } 347 + 348 + ids = append(ids, id) 349 + } 350 + 351 + return ids, nil 352 + } 353 + 354 func AddIssueComment(tx *sql.Tx, c models.IssueComment) (int64, error) { 355 result, err := tx.Exec( 356 `insert into issue_comments (
+6 -6
appview/db/pipeline.go
··· 6 "strings" 7 "time" 8 9 - "github.com/bluesky-social/indigo/atproto/syntax" 10 "tangled.org/core/appview/models" 11 "tangled.org/core/orm" 12 ) ··· 217 } 218 defer rows.Close() 219 220 - pipelines := make(map[syntax.ATURI]models.Pipeline) 221 for rows.Next() { 222 var p models.Pipeline 223 var t models.Trigger ··· 254 p.Trigger = &t 255 p.Statuses = make(map[string]models.WorkflowStatus) 256 257 - pipelines[p.AtUri()] = p 258 } 259 260 // get all statuses ··· 314 return nil, fmt.Errorf("invalid status created timestamp %q: %w", created, err) 315 } 316 317 - pipelineAt := ps.PipelineAt() 318 319 // extract 320 - pipeline, ok := pipelines[pipelineAt] 321 if !ok { 322 continue 323 } ··· 331 332 // reassign 333 pipeline.Statuses[ps.Workflow] = statuses 334 - pipelines[pipelineAt] = pipeline 335 } 336 337 var all []models.Pipeline
··· 6 "strings" 7 "time" 8 9 "tangled.org/core/appview/models" 10 "tangled.org/core/orm" 11 ) ··· 216 } 217 defer rows.Close() 218 219 + pipelines := make(map[string]models.Pipeline) 220 for rows.Next() { 221 var p models.Pipeline 222 var t models.Trigger ··· 253 p.Trigger = &t 254 p.Statuses = make(map[string]models.WorkflowStatus) 255 256 + k := fmt.Sprintf("%s/%s", p.Knot, p.Rkey) 257 + pipelines[k] = p 258 } 259 260 // get all statuses ··· 314 return nil, fmt.Errorf("invalid status created timestamp %q: %w", created, err) 315 } 316 317 + key := fmt.Sprintf("%s/%s", ps.PipelineKnot, ps.PipelineRkey) 318 319 // extract 320 + pipeline, ok := pipelines[key] 321 if !ok { 322 continue 323 } ··· 331 332 // reassign 333 pipeline.Statuses[ps.Workflow] = statuses 334 + pipelines[key] = pipeline 335 } 336 337 var all []models.Pipeline
+68 -12
appview/db/pulls.go
··· 13 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 "tangled.org/core/appview/models" 16 - "tangled.org/core/appview/pagination" 17 "tangled.org/core/orm" 18 ) 19 ··· 120 return pullId - 1, err 121 } 122 123 - func GetPullsPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]*models.Pull, error) { 124 pulls := make(map[syntax.ATURI]*models.Pull) 125 126 var conditions []string ··· 134 if conditions != nil { 135 whereClause = " where " + strings.Join(conditions, " and ") 136 } 137 - pageClause := "" 138 - if page.Limit != 0 { 139 - pageClause = fmt.Sprintf( 140 - " limit %d offset %d ", 141 - page.Limit, 142 - page.Offset, 143 - ) 144 } 145 146 query := fmt.Sprintf(` ··· 166 order by 167 created desc 168 %s 169 - `, whereClause, pageClause) 170 171 rows, err := e.Query(query, args...) 172 if err != nil { ··· 302 } 303 304 func GetPulls(e Execer, filters ...orm.Filter) ([]*models.Pull, error) { 305 - return GetPullsPaginated(e, pagination.Page{}, filters...) 306 } 307 308 func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*models.Pull, error) { 309 - pulls, err := GetPullsPaginated(e, pagination.Page{Limit: 1}, orm.FilterEq("repo_at", repoAt), orm.FilterEq("pull_id", pullId)) 310 if err != nil { 311 return nil, err 312 }
··· 13 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 "tangled.org/core/appview/models" 16 "tangled.org/core/orm" 17 ) 18 ··· 119 return pullId - 1, err 120 } 121 122 + func GetPullsWithLimit(e Execer, limit int, filters ...orm.Filter) ([]*models.Pull, error) { 123 pulls := make(map[syntax.ATURI]*models.Pull) 124 125 var conditions []string ··· 133 if conditions != nil { 134 whereClause = " where " + strings.Join(conditions, " and ") 135 } 136 + limitClause := "" 137 + if limit != 0 { 138 + limitClause = fmt.Sprintf(" limit %d ", limit) 139 } 140 141 query := fmt.Sprintf(` ··· 161 order by 162 created desc 163 %s 164 + `, whereClause, limitClause) 165 166 rows, err := e.Query(query, args...) 167 if err != nil { ··· 297 } 298 299 func GetPulls(e Execer, filters ...orm.Filter) ([]*models.Pull, error) { 300 + return GetPullsWithLimit(e, 0, filters...) 301 + } 302 + 303 + func GetPullIDs(e Execer, opts models.PullSearchOptions) ([]int64, error) { 304 + var ids []int64 305 + 306 + var filters []orm.Filter 307 + filters = append(filters, orm.FilterEq("state", opts.State)) 308 + if opts.RepoAt != "" { 309 + filters = append(filters, orm.FilterEq("repo_at", opts.RepoAt)) 310 + } 311 + 312 + var conditions []string 313 + var args []any 314 + 315 + for _, filter := range filters { 316 + conditions = append(conditions, filter.Condition()) 317 + args = append(args, filter.Arg()...) 318 + } 319 + 320 + whereClause := "" 321 + if conditions != nil { 322 + whereClause = " where " + strings.Join(conditions, " and ") 323 + } 324 + pageClause := "" 325 + if opts.Page.Limit != 0 { 326 + pageClause = fmt.Sprintf( 327 + " limit %d offset %d ", 328 + opts.Page.Limit, 329 + opts.Page.Offset, 330 + ) 331 + } 332 + 333 + query := fmt.Sprintf( 334 + ` 335 + select 336 + id 337 + from 338 + pulls 339 + %s 340 + %s`, 341 + whereClause, 342 + pageClause, 343 + ) 344 + args = append(args, opts.Page.Limit, opts.Page.Offset) 345 + rows, err := e.Query(query, args...) 346 + if err != nil { 347 + return nil, err 348 + } 349 + defer rows.Close() 350 + 351 + for rows.Next() { 352 + var id int64 353 + err := rows.Scan(&id) 354 + if err != nil { 355 + return nil, err 356 + } 357 + 358 + ids = append(ids, id) 359 + } 360 + 361 + return ids, nil 362 } 363 364 func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*models.Pull, error) { 365 + pulls, err := GetPullsWithLimit(e, 1, orm.FilterEq("repo_at", repoAt), orm.FilterEq("pull_id", pullId)) 366 if err != nil { 367 return nil, err 368 }
+32 -32
appview/issues/issues.go
··· 81 82 func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { 83 l := rp.logger.With("handler", "RepoSingleIssue") 84 - user := rp.oauth.GetMultiAccountUser(r) 85 f, err := rp.repoResolver.Resolve(r) 86 if err != nil { 87 l.Error("failed to get repo and knot", "err", err) ··· 102 103 userReactions := map[models.ReactionKind]bool{} 104 if user != nil { 105 - userReactions = db.GetReactionStatusMap(rp.db, user.Active.Did, issue.AtUri()) 106 } 107 108 backlinks, err := db.GetBacklinks(rp.db, issue.AtUri()) ··· 143 144 func (rp *Issues) EditIssue(w http.ResponseWriter, r *http.Request) { 145 l := rp.logger.With("handler", "EditIssue") 146 - user := rp.oauth.GetMultiAccountUser(r) 147 148 issue, ok := r.Context().Value("issue").(*models.Issue) 149 if !ok { ··· 182 return 183 } 184 185 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueNSID, user.Active.Did, newIssue.Rkey) 186 if err != nil { 187 l.Error("failed to get record", "err", err) 188 rp.pages.Notice(w, noticeId, "Failed to edit issue, no record found on PDS.") ··· 191 192 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 193 Collection: tangled.RepoIssueNSID, 194 - Repo: user.Active.Did, 195 Rkey: newIssue.Rkey, 196 SwapRecord: ex.Cid, 197 Record: &lexutil.LexiconTypeDecoder{ ··· 292 293 func (rp *Issues) CloseIssue(w http.ResponseWriter, r *http.Request) { 294 l := rp.logger.With("handler", "CloseIssue") 295 - user := rp.oauth.GetMultiAccountUser(r) 296 f, err := rp.repoResolver.Resolve(r) 297 if err != nil { 298 l.Error("failed to get repo and knot", "err", err) ··· 306 return 307 } 308 309 - roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 310 isRepoOwner := roles.IsOwner() 311 isCollaborator := roles.IsCollaborator() 312 - isIssueOwner := user.Active.Did == issue.Did 313 314 // TODO: make this more granular 315 if isIssueOwner || isRepoOwner || isCollaborator { ··· 326 issue.Open = false 327 328 // notify about the issue closure 329 - rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Active.Did), issue) 330 331 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 332 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId)) ··· 340 341 func (rp *Issues) ReopenIssue(w http.ResponseWriter, r *http.Request) { 342 l := rp.logger.With("handler", "ReopenIssue") 343 - user := rp.oauth.GetMultiAccountUser(r) 344 f, err := rp.repoResolver.Resolve(r) 345 if err != nil { 346 l.Error("failed to get repo and knot", "err", err) ··· 354 return 355 } 356 357 - roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 358 isRepoOwner := roles.IsOwner() 359 isCollaborator := roles.IsCollaborator() 360 - isIssueOwner := user.Active.Did == issue.Did 361 362 if isCollaborator || isRepoOwner || isIssueOwner { 363 err := db.ReopenIssues( ··· 373 issue.Open = true 374 375 // notify about the issue reopen 376 - rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Active.Did), issue) 377 378 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 379 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId)) ··· 387 388 func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) { 389 l := rp.logger.With("handler", "NewIssueComment") 390 - user := rp.oauth.GetMultiAccountUser(r) 391 f, err := rp.repoResolver.Resolve(r) 392 if err != nil { 393 l.Error("failed to get repo and knot", "err", err) ··· 416 mentions, references := rp.mentionsResolver.Resolve(r.Context(), body) 417 418 comment := models.IssueComment{ 419 - Did: user.Active.Did, 420 Rkey: tid.TID(), 421 IssueAt: issue.AtUri().String(), 422 ReplyTo: replyTo, ··· 495 496 func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) { 497 l := rp.logger.With("handler", "IssueComment") 498 - user := rp.oauth.GetMultiAccountUser(r) 499 500 issue, ok := r.Context().Value("issue").(*models.Issue) 501 if !ok { ··· 531 532 func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) { 533 l := rp.logger.With("handler", "EditIssueComment") 534 - user := rp.oauth.GetMultiAccountUser(r) 535 536 issue, ok := r.Context().Value("issue").(*models.Issue) 537 if !ok { ··· 557 } 558 comment := comments[0] 559 560 - if comment.Did != user.Active.Did { 561 - l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Active.Did) 562 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 563 return 564 } ··· 608 // rkey is optional, it was introduced later 609 if newComment.Rkey != "" { 610 // update the record on pds 611 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Active.Did, comment.Rkey) 612 if err != nil { 613 l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey) 614 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") ··· 617 618 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 619 Collection: tangled.RepoIssueCommentNSID, 620 - Repo: user.Active.Did, 621 Rkey: newComment.Rkey, 622 SwapRecord: ex.Cid, 623 Record: &lexutil.LexiconTypeDecoder{ ··· 641 642 func (rp *Issues) ReplyIssueCommentPlaceholder(w http.ResponseWriter, r *http.Request) { 643 l := rp.logger.With("handler", "ReplyIssueCommentPlaceholder") 644 - user := rp.oauth.GetMultiAccountUser(r) 645 646 issue, ok := r.Context().Value("issue").(*models.Issue) 647 if !ok { ··· 677 678 func (rp *Issues) ReplyIssueComment(w http.ResponseWriter, r *http.Request) { 679 l := rp.logger.With("handler", "ReplyIssueComment") 680 - user := rp.oauth.GetMultiAccountUser(r) 681 682 issue, ok := r.Context().Value("issue").(*models.Issue) 683 if !ok { ··· 713 714 func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { 715 l := rp.logger.With("handler", "DeleteIssueComment") 716 - user := rp.oauth.GetMultiAccountUser(r) 717 718 issue, ok := r.Context().Value("issue").(*models.Issue) 719 if !ok { ··· 739 } 740 comment := comments[0] 741 742 - if comment.Did != user.Active.Did { 743 - l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Active.Did) 744 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 745 return 746 } ··· 769 } 770 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 771 Collection: tangled.RepoIssueCommentNSID, 772 - Repo: user.Active.Did, 773 Rkey: comment.Rkey, 774 }) 775 if err != nil { ··· 807 808 page := pagination.FromContext(r.Context()) 809 810 - user := rp.oauth.GetMultiAccountUser(r) 811 f, err := rp.repoResolver.Resolve(r) 812 if err != nil { 813 l.Error("failed to get repo and knot", "err", err) ··· 884 } 885 886 rp.pages.RepoIssues(w, pages.RepoIssuesParams{ 887 - LoggedInUser: rp.oauth.GetMultiAccountUser(r), 888 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 889 Issues: issues, 890 IssueCount: totalIssues, ··· 897 898 func (rp *Issues) NewIssue(w http.ResponseWriter, r *http.Request) { 899 l := rp.logger.With("handler", "NewIssue") 900 - user := rp.oauth.GetMultiAccountUser(r) 901 902 f, err := rp.repoResolver.Resolve(r) 903 if err != nil { ··· 921 Title: r.FormValue("title"), 922 Body: body, 923 Open: true, 924 - Did: user.Active.Did, 925 Created: time.Now(), 926 Mentions: mentions, 927 References: references, ··· 945 } 946 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 947 Collection: tangled.RepoIssueNSID, 948 - Repo: user.Active.Did, 949 Rkey: issue.Rkey, 950 Record: &lexutil.LexiconTypeDecoder{ 951 Val: &record,
··· 81 82 func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { 83 l := rp.logger.With("handler", "RepoSingleIssue") 84 + user := rp.oauth.GetUser(r) 85 f, err := rp.repoResolver.Resolve(r) 86 if err != nil { 87 l.Error("failed to get repo and knot", "err", err) ··· 102 103 userReactions := map[models.ReactionKind]bool{} 104 if user != nil { 105 + userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri()) 106 } 107 108 backlinks, err := db.GetBacklinks(rp.db, issue.AtUri()) ··· 143 144 func (rp *Issues) EditIssue(w http.ResponseWriter, r *http.Request) { 145 l := rp.logger.With("handler", "EditIssue") 146 + user := rp.oauth.GetUser(r) 147 148 issue, ok := r.Context().Value("issue").(*models.Issue) 149 if !ok { ··· 182 return 183 } 184 185 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueNSID, user.Did, newIssue.Rkey) 186 if err != nil { 187 l.Error("failed to get record", "err", err) 188 rp.pages.Notice(w, noticeId, "Failed to edit issue, no record found on PDS.") ··· 191 192 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 193 Collection: tangled.RepoIssueNSID, 194 + Repo: user.Did, 195 Rkey: newIssue.Rkey, 196 SwapRecord: ex.Cid, 197 Record: &lexutil.LexiconTypeDecoder{ ··· 292 293 func (rp *Issues) CloseIssue(w http.ResponseWriter, r *http.Request) { 294 l := rp.logger.With("handler", "CloseIssue") 295 + user := rp.oauth.GetUser(r) 296 f, err := rp.repoResolver.Resolve(r) 297 if err != nil { 298 l.Error("failed to get repo and knot", "err", err) ··· 306 return 307 } 308 309 + roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())} 310 isRepoOwner := roles.IsOwner() 311 isCollaborator := roles.IsCollaborator() 312 + isIssueOwner := user.Did == issue.Did 313 314 // TODO: make this more granular 315 if isIssueOwner || isRepoOwner || isCollaborator { ··· 326 issue.Open = false 327 328 // notify about the issue closure 329 + rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Did), issue) 330 331 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 332 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId)) ··· 340 341 func (rp *Issues) ReopenIssue(w http.ResponseWriter, r *http.Request) { 342 l := rp.logger.With("handler", "ReopenIssue") 343 + user := rp.oauth.GetUser(r) 344 f, err := rp.repoResolver.Resolve(r) 345 if err != nil { 346 l.Error("failed to get repo and knot", "err", err) ··· 354 return 355 } 356 357 + roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())} 358 isRepoOwner := roles.IsOwner() 359 isCollaborator := roles.IsCollaborator() 360 + isIssueOwner := user.Did == issue.Did 361 362 if isCollaborator || isRepoOwner || isIssueOwner { 363 err := db.ReopenIssues( ··· 373 issue.Open = true 374 375 // notify about the issue reopen 376 + rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Did), issue) 377 378 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 379 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId)) ··· 387 388 func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) { 389 l := rp.logger.With("handler", "NewIssueComment") 390 + user := rp.oauth.GetUser(r) 391 f, err := rp.repoResolver.Resolve(r) 392 if err != nil { 393 l.Error("failed to get repo and knot", "err", err) ··· 416 mentions, references := rp.mentionsResolver.Resolve(r.Context(), body) 417 418 comment := models.IssueComment{ 419 + Did: user.Did, 420 Rkey: tid.TID(), 421 IssueAt: issue.AtUri().String(), 422 ReplyTo: replyTo, ··· 495 496 func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) { 497 l := rp.logger.With("handler", "IssueComment") 498 + user := rp.oauth.GetUser(r) 499 500 issue, ok := r.Context().Value("issue").(*models.Issue) 501 if !ok { ··· 531 532 func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) { 533 l := rp.logger.With("handler", "EditIssueComment") 534 + user := rp.oauth.GetUser(r) 535 536 issue, ok := r.Context().Value("issue").(*models.Issue) 537 if !ok { ··· 557 } 558 comment := comments[0] 559 560 + if comment.Did != user.Did { 561 + l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did) 562 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 563 return 564 } ··· 608 // rkey is optional, it was introduced later 609 if newComment.Rkey != "" { 610 // update the record on pds 611 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, comment.Rkey) 612 if err != nil { 613 l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey) 614 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") ··· 617 618 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 619 Collection: tangled.RepoIssueCommentNSID, 620 + Repo: user.Did, 621 Rkey: newComment.Rkey, 622 SwapRecord: ex.Cid, 623 Record: &lexutil.LexiconTypeDecoder{ ··· 641 642 func (rp *Issues) ReplyIssueCommentPlaceholder(w http.ResponseWriter, r *http.Request) { 643 l := rp.logger.With("handler", "ReplyIssueCommentPlaceholder") 644 + user := rp.oauth.GetUser(r) 645 646 issue, ok := r.Context().Value("issue").(*models.Issue) 647 if !ok { ··· 677 678 func (rp *Issues) ReplyIssueComment(w http.ResponseWriter, r *http.Request) { 679 l := rp.logger.With("handler", "ReplyIssueComment") 680 + user := rp.oauth.GetUser(r) 681 682 issue, ok := r.Context().Value("issue").(*models.Issue) 683 if !ok { ··· 713 714 func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { 715 l := rp.logger.With("handler", "DeleteIssueComment") 716 + user := rp.oauth.GetUser(r) 717 718 issue, ok := r.Context().Value("issue").(*models.Issue) 719 if !ok { ··· 739 } 740 comment := comments[0] 741 742 + if comment.Did != user.Did { 743 + l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Did) 744 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 745 return 746 } ··· 769 } 770 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 771 Collection: tangled.RepoIssueCommentNSID, 772 + Repo: user.Did, 773 Rkey: comment.Rkey, 774 }) 775 if err != nil { ··· 807 808 page := pagination.FromContext(r.Context()) 809 810 + user := rp.oauth.GetUser(r) 811 f, err := rp.repoResolver.Resolve(r) 812 if err != nil { 813 l.Error("failed to get repo and knot", "err", err) ··· 884 } 885 886 rp.pages.RepoIssues(w, pages.RepoIssuesParams{ 887 + LoggedInUser: rp.oauth.GetUser(r), 888 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 889 Issues: issues, 890 IssueCount: totalIssues, ··· 897 898 func (rp *Issues) NewIssue(w http.ResponseWriter, r *http.Request) { 899 l := rp.logger.With("handler", "NewIssue") 900 + user := rp.oauth.GetUser(r) 901 902 f, err := rp.repoResolver.Resolve(r) 903 if err != nil { ··· 921 Title: r.FormValue("title"), 922 Body: body, 923 Open: true, 924 + Did: user.Did, 925 Created: time.Now(), 926 Mentions: mentions, 927 References: references, ··· 945 } 946 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 947 Collection: tangled.RepoIssueNSID, 948 + Repo: user.Did, 949 Rkey: issue.Rkey, 950 Record: &lexutil.LexiconTypeDecoder{ 951 Val: &record,
+31 -31
appview/knots/knots.go
··· 70 } 71 72 func (k *Knots) knots(w http.ResponseWriter, r *http.Request) { 73 - user := k.OAuth.GetMultiAccountUser(r) 74 registrations, err := db.GetRegistrations( 75 k.Db, 76 - orm.FilterEq("did", user.Active.Did), 77 ) 78 if err != nil { 79 k.Logger.Error("failed to fetch knot registrations", "err", err) ··· 92 func (k *Knots) dashboard(w http.ResponseWriter, r *http.Request) { 93 l := k.Logger.With("handler", "dashboard") 94 95 - user := k.OAuth.GetMultiAccountUser(r) 96 - l = l.With("user", user.Active.Did) 97 98 domain := chi.URLParam(r, "domain") 99 if domain == "" { ··· 103 104 registrations, err := db.GetRegistrations( 105 k.Db, 106 - orm.FilterEq("did", user.Active.Did), 107 orm.FilterEq("domain", domain), 108 ) 109 if err != nil { ··· 154 } 155 156 func (k *Knots) register(w http.ResponseWriter, r *http.Request) { 157 - user := k.OAuth.GetMultiAccountUser(r) 158 l := k.Logger.With("handler", "register") 159 160 noticeId := "register-error" ··· 175 return 176 } 177 l = l.With("domain", domain) 178 - l = l.With("user", user.Active.Did) 179 180 tx, err := k.Db.Begin() 181 if err != nil { ··· 188 k.Enforcer.E.LoadPolicy() 189 }() 190 191 - err = db.AddKnot(tx, domain, user.Active.Did) 192 if err != nil { 193 l.Error("failed to insert", "err", err) 194 fail() ··· 210 return 211 } 212 213 - ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Active.Did, domain) 214 var exCid *string 215 if ex != nil { 216 exCid = ex.Cid ··· 219 // re-announce by registering under same rkey 220 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 221 Collection: tangled.KnotNSID, 222 - Repo: user.Active.Did, 223 Rkey: domain, 224 Record: &lexutil.LexiconTypeDecoder{ 225 Val: &tangled.Knot{ ··· 250 } 251 252 // begin verification 253 - err = serververify.RunVerification(r.Context(), domain, user.Active.Did, k.Config.Core.Dev) 254 if err != nil { 255 l.Error("verification failed", "err", err) 256 k.Pages.HxRefresh(w) 257 return 258 } 259 260 - err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Active.Did) 261 if err != nil { 262 l.Error("failed to mark verified", "err", err) 263 k.Pages.HxRefresh(w) ··· 275 } 276 277 func (k *Knots) delete(w http.ResponseWriter, r *http.Request) { 278 - user := k.OAuth.GetMultiAccountUser(r) 279 l := k.Logger.With("handler", "delete") 280 281 noticeId := "operation-error" ··· 294 // get record from db first 295 registrations, err := db.GetRegistrations( 296 k.Db, 297 - orm.FilterEq("did", user.Active.Did), 298 orm.FilterEq("domain", domain), 299 ) 300 if err != nil { ··· 322 323 err = db.DeleteKnot( 324 tx, 325 - orm.FilterEq("did", user.Active.Did), 326 orm.FilterEq("domain", domain), 327 ) 328 if err != nil { ··· 350 351 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 352 Collection: tangled.KnotNSID, 353 - Repo: user.Active.Did, 354 Rkey: domain, 355 }) 356 if err != nil { ··· 382 } 383 384 func (k *Knots) retry(w http.ResponseWriter, r *http.Request) { 385 - user := k.OAuth.GetMultiAccountUser(r) 386 l := k.Logger.With("handler", "retry") 387 388 noticeId := "operation-error" ··· 398 return 399 } 400 l = l.With("domain", domain) 401 - l = l.With("user", user.Active.Did) 402 403 // get record from db first 404 registrations, err := db.GetRegistrations( 405 k.Db, 406 - orm.FilterEq("did", user.Active.Did), 407 orm.FilterEq("domain", domain), 408 ) 409 if err != nil { ··· 419 registration := registrations[0] 420 421 // begin verification 422 - err = serververify.RunVerification(r.Context(), domain, user.Active.Did, k.Config.Core.Dev) 423 if err != nil { 424 l.Error("verification failed", "err", err) 425 ··· 437 return 438 } 439 440 - err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Active.Did) 441 if err != nil { 442 l.Error("failed to mark verified", "err", err) 443 k.Pages.Notice(w, noticeId, err.Error()) ··· 456 return 457 } 458 459 - ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Active.Did, domain) 460 var exCid *string 461 if ex != nil { 462 exCid = ex.Cid ··· 465 // ignore the error here 466 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 467 Collection: tangled.KnotNSID, 468 - Repo: user.Active.Did, 469 Rkey: domain, 470 Record: &lexutil.LexiconTypeDecoder{ 471 Val: &tangled.Knot{ ··· 494 // Get updated registration to show 495 registrations, err = db.GetRegistrations( 496 k.Db, 497 - orm.FilterEq("did", user.Active.Did), 498 orm.FilterEq("domain", domain), 499 ) 500 if err != nil { ··· 516 } 517 518 func (k *Knots) addMember(w http.ResponseWriter, r *http.Request) { 519 - user := k.OAuth.GetMultiAccountUser(r) 520 l := k.Logger.With("handler", "addMember") 521 522 domain := chi.URLParam(r, "domain") ··· 526 return 527 } 528 l = l.With("domain", domain) 529 - l = l.With("user", user.Active.Did) 530 531 registrations, err := db.GetRegistrations( 532 k.Db, 533 - orm.FilterEq("did", user.Active.Did), 534 orm.FilterEq("domain", domain), 535 orm.FilterIsNot("registered", "null"), 536 ) ··· 583 584 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 585 Collection: tangled.KnotMemberNSID, 586 - Repo: user.Active.Did, 587 Rkey: rkey, 588 Record: &lexutil.LexiconTypeDecoder{ 589 Val: &tangled.KnotMember{ ··· 618 } 619 620 func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) { 621 - user := k.OAuth.GetMultiAccountUser(r) 622 l := k.Logger.With("handler", "removeMember") 623 624 noticeId := "operation-error" ··· 634 return 635 } 636 l = l.With("domain", domain) 637 - l = l.With("user", user.Active.Did) 638 639 registrations, err := db.GetRegistrations( 640 k.Db, 641 - orm.FilterEq("did", user.Active.Did), 642 orm.FilterEq("domain", domain), 643 orm.FilterIsNot("registered", "null"), 644 )
··· 70 } 71 72 func (k *Knots) knots(w http.ResponseWriter, r *http.Request) { 73 + user := k.OAuth.GetUser(r) 74 registrations, err := db.GetRegistrations( 75 k.Db, 76 + orm.FilterEq("did", user.Did), 77 ) 78 if err != nil { 79 k.Logger.Error("failed to fetch knot registrations", "err", err) ··· 92 func (k *Knots) dashboard(w http.ResponseWriter, r *http.Request) { 93 l := k.Logger.With("handler", "dashboard") 94 95 + user := k.OAuth.GetUser(r) 96 + l = l.With("user", user.Did) 97 98 domain := chi.URLParam(r, "domain") 99 if domain == "" { ··· 103 104 registrations, err := db.GetRegistrations( 105 k.Db, 106 + orm.FilterEq("did", user.Did), 107 orm.FilterEq("domain", domain), 108 ) 109 if err != nil { ··· 154 } 155 156 func (k *Knots) register(w http.ResponseWriter, r *http.Request) { 157 + user := k.OAuth.GetUser(r) 158 l := k.Logger.With("handler", "register") 159 160 noticeId := "register-error" ··· 175 return 176 } 177 l = l.With("domain", domain) 178 + l = l.With("user", user.Did) 179 180 tx, err := k.Db.Begin() 181 if err != nil { ··· 188 k.Enforcer.E.LoadPolicy() 189 }() 190 191 + err = db.AddKnot(tx, domain, user.Did) 192 if err != nil { 193 l.Error("failed to insert", "err", err) 194 fail() ··· 210 return 211 } 212 213 + ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Did, domain) 214 var exCid *string 215 if ex != nil { 216 exCid = ex.Cid ··· 219 // re-announce by registering under same rkey 220 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 221 Collection: tangled.KnotNSID, 222 + Repo: user.Did, 223 Rkey: domain, 224 Record: &lexutil.LexiconTypeDecoder{ 225 Val: &tangled.Knot{ ··· 250 } 251 252 // begin verification 253 + err = serververify.RunVerification(r.Context(), domain, user.Did, k.Config.Core.Dev) 254 if err != nil { 255 l.Error("verification failed", "err", err) 256 k.Pages.HxRefresh(w) 257 return 258 } 259 260 + err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Did) 261 if err != nil { 262 l.Error("failed to mark verified", "err", err) 263 k.Pages.HxRefresh(w) ··· 275 } 276 277 func (k *Knots) delete(w http.ResponseWriter, r *http.Request) { 278 + user := k.OAuth.GetUser(r) 279 l := k.Logger.With("handler", "delete") 280 281 noticeId := "operation-error" ··· 294 // get record from db first 295 registrations, err := db.GetRegistrations( 296 k.Db, 297 + orm.FilterEq("did", user.Did), 298 orm.FilterEq("domain", domain), 299 ) 300 if err != nil { ··· 322 323 err = db.DeleteKnot( 324 tx, 325 + orm.FilterEq("did", user.Did), 326 orm.FilterEq("domain", domain), 327 ) 328 if err != nil { ··· 350 351 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 352 Collection: tangled.KnotNSID, 353 + Repo: user.Did, 354 Rkey: domain, 355 }) 356 if err != nil { ··· 382 } 383 384 func (k *Knots) retry(w http.ResponseWriter, r *http.Request) { 385 + user := k.OAuth.GetUser(r) 386 l := k.Logger.With("handler", "retry") 387 388 noticeId := "operation-error" ··· 398 return 399 } 400 l = l.With("domain", domain) 401 + l = l.With("user", user.Did) 402 403 // get record from db first 404 registrations, err := db.GetRegistrations( 405 k.Db, 406 + orm.FilterEq("did", user.Did), 407 orm.FilterEq("domain", domain), 408 ) 409 if err != nil { ··· 419 registration := registrations[0] 420 421 // begin verification 422 + err = serververify.RunVerification(r.Context(), domain, user.Did, k.Config.Core.Dev) 423 if err != nil { 424 l.Error("verification failed", "err", err) 425 ··· 437 return 438 } 439 440 + err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Did) 441 if err != nil { 442 l.Error("failed to mark verified", "err", err) 443 k.Pages.Notice(w, noticeId, err.Error()) ··· 456 return 457 } 458 459 + ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Did, domain) 460 var exCid *string 461 if ex != nil { 462 exCid = ex.Cid ··· 465 // ignore the error here 466 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 467 Collection: tangled.KnotNSID, 468 + Repo: user.Did, 469 Rkey: domain, 470 Record: &lexutil.LexiconTypeDecoder{ 471 Val: &tangled.Knot{ ··· 494 // Get updated registration to show 495 registrations, err = db.GetRegistrations( 496 k.Db, 497 + orm.FilterEq("did", user.Did), 498 orm.FilterEq("domain", domain), 499 ) 500 if err != nil { ··· 516 } 517 518 func (k *Knots) addMember(w http.ResponseWriter, r *http.Request) { 519 + user := k.OAuth.GetUser(r) 520 l := k.Logger.With("handler", "addMember") 521 522 domain := chi.URLParam(r, "domain") ··· 526 return 527 } 528 l = l.With("domain", domain) 529 + l = l.With("user", user.Did) 530 531 registrations, err := db.GetRegistrations( 532 k.Db, 533 + orm.FilterEq("did", user.Did), 534 orm.FilterEq("domain", domain), 535 orm.FilterIsNot("registered", "null"), 536 ) ··· 583 584 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 585 Collection: tangled.KnotMemberNSID, 586 + Repo: user.Did, 587 Rkey: rkey, 588 Record: &lexutil.LexiconTypeDecoder{ 589 Val: &tangled.KnotMember{ ··· 618 } 619 620 func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) { 621 + user := k.OAuth.GetUser(r) 622 l := k.Logger.With("handler", "removeMember") 623 624 noticeId := "operation-error" ··· 634 return 635 } 636 l = l.With("domain", domain) 637 + l = l.With("user", user.Did) 638 639 registrations, err := db.GetRegistrations( 640 k.Db, 641 + orm.FilterEq("did", user.Did), 642 orm.FilterEq("domain", domain), 643 orm.FilterIsNot("registered", "null"), 644 )
+2 -2
appview/labels/labels.go
··· 68 // - this handler should calculate the diff in order to create the labelop record 69 // - we need the diff in order to maintain a "history" of operations performed by users 70 func (l *Labels) PerformLabelOp(w http.ResponseWriter, r *http.Request) { 71 - user := l.oauth.GetMultiAccountUser(r) 72 73 noticeId := "add-label-error" 74 ··· 82 return 83 } 84 85 - did := user.Active.Did 86 rkey := tid.TID() 87 performedAt := time.Now() 88 indexedAt := time.Now()
··· 68 // - this handler should calculate the diff in order to create the labelop record 69 // - we need the diff in order to maintain a "history" of operations performed by users 70 func (l *Labels) PerformLabelOp(w http.ResponseWriter, r *http.Request) { 71 + user := l.oauth.GetUser(r) 72 73 noticeId := "add-label-error" 74 ··· 82 return 83 } 84 85 + did := user.Did 86 rkey := tid.TID() 87 performedAt := time.Now() 88 indexedAt := time.Now()
+8 -6
appview/middleware/middleware.go
··· 115 return func(next http.Handler) http.Handler { 116 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 117 // requires auth also 118 - actor := mw.oauth.GetMultiAccountUser(r) 119 if actor == nil { 120 // we need a logged in user 121 log.Printf("not logged in, redirecting") ··· 128 return 129 } 130 131 - ok, err := mw.enforcer.E.HasGroupingPolicy(actor.Active.Did, group, domain) 132 if err != nil || !ok { 133 - log.Printf("%s does not have perms of a %s in domain %s", actor.Active.Did, group, domain) 134 http.Error(w, "Forbiden", http.StatusUnauthorized) 135 return 136 } ··· 148 return func(next http.Handler) http.Handler { 149 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 150 // requires auth also 151 - actor := mw.oauth.GetMultiAccountUser(r) 152 if actor == nil { 153 // we need a logged in user 154 log.Printf("not logged in, redirecting") ··· 161 return 162 } 163 164 - ok, err := mw.enforcer.E.Enforce(actor.Active.Did, f.Knot, f.DidSlashRepo(), requiredPerm) 165 if err != nil || !ok { 166 - log.Printf("%s does not have perms of a %s in repo %s", actor.Active.Did, requiredPerm, f.DidSlashRepo()) 167 http.Error(w, "Forbiden", http.StatusUnauthorized) 168 return 169 }
··· 115 return func(next http.Handler) http.Handler { 116 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 117 // requires auth also 118 + actor := mw.oauth.GetUser(r) 119 if actor == nil { 120 // we need a logged in user 121 log.Printf("not logged in, redirecting") ··· 128 return 129 } 130 131 + ok, err := mw.enforcer.E.HasGroupingPolicy(actor.Did, group, domain) 132 if err != nil || !ok { 133 + // we need a logged in user 134 + log.Printf("%s does not have perms of a %s in domain %s", actor.Did, group, domain) 135 http.Error(w, "Forbiden", http.StatusUnauthorized) 136 return 137 } ··· 149 return func(next http.Handler) http.Handler { 150 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 151 // requires auth also 152 + actor := mw.oauth.GetUser(r) 153 if actor == nil { 154 // we need a logged in user 155 log.Printf("not logged in, redirecting") ··· 162 return 163 } 164 165 + ok, err := mw.enforcer.E.Enforce(actor.Did, f.Knot, f.DidSlashRepo(), requiredPerm) 166 if err != nil || !ok { 167 + // we need a logged in user 168 + log.Printf("%s does not have perms of a %s in repo %s", actor.Did, requiredPerm, f.DidSlashRepo()) 169 http.Error(w, "Forbiden", http.StatusUnauthorized) 170 return 171 }
-48
appview/models/pipeline.go
··· 1 package models 2 3 import ( 4 - "fmt" 5 "slices" 6 - "strings" 7 "time" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "github.com/go-git/go-git/v5/plumbing" 11 - "tangled.org/core/api/tangled" 12 spindle "tangled.org/core/spindle/models" 13 "tangled.org/core/workflow" 14 ) ··· 28 Statuses map[string]WorkflowStatus 29 } 30 31 - func (p *Pipeline) AtUri() syntax.ATURI { 32 - return syntax.ATURI(fmt.Sprintf("at://did:web:%s/%s/%s", p.Knot, tangled.PipelineNSID, p.Rkey)) 33 - } 34 - 35 type WorkflowStatus struct { 36 Data []PipelineStatus 37 } ··· 59 return 0 60 } 61 62 - // produces short summary of successes: 63 - // - "0/4" when zero successes of 4 workflows 64 - // - "4/4" when all successes of 4 workflows 65 - // - "0/0" when no workflows run in this pipeline 66 - func (p Pipeline) ShortStatusSummary() string { 67 - counts := make(map[spindle.StatusKind]int) 68 - for _, w := range p.Statuses { 69 - counts[w.Latest().Status] += 1 70 - } 71 - 72 - total := len(p.Statuses) 73 - successes := counts[spindle.StatusKindSuccess] 74 - 75 - return fmt.Sprintf("%d/%d", successes, total) 76 - } 77 - 78 - // produces a string of the form "3/4 success, 2/4 failed, 1/4 pending" 79 - func (p Pipeline) LongStatusSummary() string { 80 - counts := make(map[spindle.StatusKind]int) 81 - for _, w := range p.Statuses { 82 - counts[w.Latest().Status] += 1 83 - } 84 - 85 - total := len(p.Statuses) 86 - 87 - var result []string 88 - // finish states first, followed by start states 89 - states := append(spindle.FinishStates[:], spindle.StartStates[:]...) 90 - for _, state := range states { 91 - if count, ok := counts[state]; ok { 92 - result = append(result, fmt.Sprintf("%d/%d %s", count, total, state.String())) 93 - } 94 - } 95 - 96 - return strings.Join(result, ", ") 97 - } 98 - 99 func (p Pipeline) Counts() map[string]int { 100 m := make(map[string]int) 101 for _, w := range p.Statuses { ··· 172 Error *string 173 ExitCode int 174 } 175 - 176 - func (ps *PipelineStatus) PipelineAt() syntax.ATURI { 177 - return syntax.ATURI(fmt.Sprintf("at://did:web:%s/%s/%s", ps.PipelineKnot, tangled.PipelineNSID, ps.PipelineRkey)) 178 - }
··· 1 package models 2 3 import ( 4 "slices" 5 "time" 6 7 "github.com/bluesky-social/indigo/atproto/syntax" 8 "github.com/go-git/go-git/v5/plumbing" 9 spindle "tangled.org/core/spindle/models" 10 "tangled.org/core/workflow" 11 ) ··· 25 Statuses map[string]WorkflowStatus 26 } 27 28 type WorkflowStatus struct { 29 Data []PipelineStatus 30 } ··· 52 return 0 53 } 54 55 func (p Pipeline) Counts() map[string]int { 56 m := make(map[string]int) 57 for _, w := range p.Statuses { ··· 128 Error *string 129 ExitCode int 130 }
+17 -7
appview/models/pull.go
··· 171 return syntax.ATURI(p.CommentAt) 172 } 173 174 - func (p *Pull) TotalComments() int { 175 - total := 0 176 - for _, s := range p.Submissions { 177 - total += len(s.Comments) 178 - } 179 - return total 180 - } 181 182 func (p *Pull) LastRoundNumber() int { 183 return len(p.Submissions) - 1
··· 171 return syntax.ATURI(p.CommentAt) 172 } 173 174 + // func (p *PullComment) AsRecord() tangled.RepoPullComment { 175 + // mentions := make([]string, len(p.Mentions)) 176 + // for i, did := range p.Mentions { 177 + // mentions[i] = string(did) 178 + // } 179 + // references := make([]string, len(p.References)) 180 + // for i, uri := range p.References { 181 + // references[i] = string(uri) 182 + // } 183 + // return tangled.RepoPullComment{ 184 + // Pull: p.PullAt, 185 + // Body: p.Body, 186 + // Mentions: mentions, 187 + // References: references, 188 + // CreatedAt: p.Created.Format(time.RFC3339), 189 + // } 190 + // } 191 192 func (p *Pull) LastRoundNumber() int { 193 return len(p.Submissions) - 1
+6 -7
appview/notifications/notifications.go
··· 48 49 func (n *Notifications) notificationsPage(w http.ResponseWriter, r *http.Request) { 50 l := n.logger.With("handler", "notificationsPage") 51 - user := n.oauth.GetMultiAccountUser(r) 52 53 page := pagination.FromContext(r.Context()) 54 55 total, err := db.CountNotifications( 56 n.db, 57 - orm.FilterEq("recipient_did", user.Active.Did), 58 ) 59 if err != nil { 60 l.Error("failed to get total notifications", "err", err) ··· 65 notifications, err := db.GetNotificationsWithEntities( 66 n.db, 67 page, 68 - orm.FilterEq("recipient_did", user.Active.Did), 69 ) 70 if err != nil { 71 l.Error("failed to get notifications", "err", err) ··· 73 return 74 } 75 76 - err = db.MarkAllNotificationsRead(n.db, user.Active.Did) 77 if err != nil { 78 l.Error("failed to mark notifications as read", "err", err) 79 } ··· 90 } 91 92 func (n *Notifications) getUnreadCount(w http.ResponseWriter, r *http.Request) { 93 - user := n.oauth.GetMultiAccountUser(r) 94 if user == nil { 95 - http.Error(w, "Forbidden", http.StatusUnauthorized) 96 return 97 } 98 99 count, err := db.CountNotifications( 100 n.db, 101 - orm.FilterEq("recipient_did", user.Active.Did), 102 orm.FilterEq("read", 0), 103 ) 104 if err != nil {
··· 48 49 func (n *Notifications) notificationsPage(w http.ResponseWriter, r *http.Request) { 50 l := n.logger.With("handler", "notificationsPage") 51 + user := n.oauth.GetUser(r) 52 53 page := pagination.FromContext(r.Context()) 54 55 total, err := db.CountNotifications( 56 n.db, 57 + orm.FilterEq("recipient_did", user.Did), 58 ) 59 if err != nil { 60 l.Error("failed to get total notifications", "err", err) ··· 65 notifications, err := db.GetNotificationsWithEntities( 66 n.db, 67 page, 68 + orm.FilterEq("recipient_did", user.Did), 69 ) 70 if err != nil { 71 l.Error("failed to get notifications", "err", err) ··· 73 return 74 } 75 76 + err = db.MarkAllNotificationsRead(n.db, user.Did) 77 if err != nil { 78 l.Error("failed to mark notifications as read", "err", err) 79 } ··· 90 } 91 92 func (n *Notifications) getUnreadCount(w http.ResponseWriter, r *http.Request) { 93 + user := n.oauth.GetUser(r) 94 if user == nil { 95 return 96 } 97 98 count, err := db.CountNotifications( 99 n.db, 100 + orm.FilterEq("recipient_did", user.Did), 101 orm.FilterEq("read", 0), 102 ) 103 if err != nil {
-191
appview/oauth/accounts.go
··· 1 - package oauth 2 - 3 - import ( 4 - "encoding/json" 5 - "errors" 6 - "net/http" 7 - "time" 8 - ) 9 - 10 - const MaxAccounts = 20 11 - 12 - var ErrMaxAccountsReached = errors.New("maximum number of linked accounts reached") 13 - 14 - type AccountInfo struct { 15 - Did string `json:"did"` 16 - Handle string `json:"handle"` 17 - SessionId string `json:"session_id"` 18 - AddedAt int64 `json:"added_at"` 19 - } 20 - 21 - type AccountRegistry struct { 22 - Accounts []AccountInfo `json:"accounts"` 23 - } 24 - 25 - type MultiAccountUser struct { 26 - Active *User 27 - Accounts []AccountInfo 28 - } 29 - 30 - func (m *MultiAccountUser) Did() string { 31 - if m.Active == nil { 32 - return "" 33 - } 34 - return m.Active.Did 35 - } 36 - 37 - func (m *MultiAccountUser) Pds() string { 38 - if m.Active == nil { 39 - return "" 40 - } 41 - return m.Active.Pds 42 - } 43 - 44 - func (o *OAuth) GetAccounts(r *http.Request) *AccountRegistry { 45 - session, err := o.SessStore.Get(r, AccountsName) 46 - if err != nil || session.IsNew { 47 - return &AccountRegistry{Accounts: []AccountInfo{}} 48 - } 49 - 50 - data, ok := session.Values["accounts"].(string) 51 - if !ok { 52 - return &AccountRegistry{Accounts: []AccountInfo{}} 53 - } 54 - 55 - var registry AccountRegistry 56 - if err := json.Unmarshal([]byte(data), &registry); err != nil { 57 - return &AccountRegistry{Accounts: []AccountInfo{}} 58 - } 59 - 60 - return &registry 61 - } 62 - 63 - func (o *OAuth) SaveAccounts(w http.ResponseWriter, r *http.Request, registry *AccountRegistry) error { 64 - session, err := o.SessStore.Get(r, AccountsName) 65 - if err != nil { 66 - return err 67 - } 68 - 69 - data, err := json.Marshal(registry) 70 - if err != nil { 71 - return err 72 - } 73 - 74 - session.Values["accounts"] = string(data) 75 - session.Options.MaxAge = 60 * 60 * 24 * 365 76 - session.Options.HttpOnly = true 77 - session.Options.Secure = !o.Config.Core.Dev 78 - session.Options.SameSite = http.SameSiteLaxMode 79 - 80 - return session.Save(r, w) 81 - } 82 - 83 - func (r *AccountRegistry) AddAccount(did, handle, sessionId string) error { 84 - for i, acc := range r.Accounts { 85 - if acc.Did == did { 86 - r.Accounts[i].SessionId = sessionId 87 - r.Accounts[i].Handle = handle 88 - return nil 89 - } 90 - } 91 - 92 - if len(r.Accounts) >= MaxAccounts { 93 - return ErrMaxAccountsReached 94 - } 95 - 96 - r.Accounts = append(r.Accounts, AccountInfo{ 97 - Did: did, 98 - Handle: handle, 99 - SessionId: sessionId, 100 - AddedAt: time.Now().Unix(), 101 - }) 102 - return nil 103 - } 104 - 105 - func (r *AccountRegistry) RemoveAccount(did string) { 106 - filtered := make([]AccountInfo, 0, len(r.Accounts)) 107 - for _, acc := range r.Accounts { 108 - if acc.Did != did { 109 - filtered = append(filtered, acc) 110 - } 111 - } 112 - r.Accounts = filtered 113 - } 114 - 115 - func (r *AccountRegistry) FindAccount(did string) *AccountInfo { 116 - for i := range r.Accounts { 117 - if r.Accounts[i].Did == did { 118 - return &r.Accounts[i] 119 - } 120 - } 121 - return nil 122 - } 123 - 124 - func (r *AccountRegistry) OtherAccounts(activeDid string) []AccountInfo { 125 - result := make([]AccountInfo, 0, len(r.Accounts)) 126 - for _, acc := range r.Accounts { 127 - if acc.Did != activeDid { 128 - result = append(result, acc) 129 - } 130 - } 131 - return result 132 - } 133 - 134 - func (o *OAuth) GetMultiAccountUser(r *http.Request) *MultiAccountUser { 135 - user := o.GetUser(r) 136 - if user == nil { 137 - return nil 138 - } 139 - 140 - registry := o.GetAccounts(r) 141 - return &MultiAccountUser{ 142 - Active: user, 143 - Accounts: registry.Accounts, 144 - } 145 - } 146 - 147 - type AuthReturnInfo struct { 148 - ReturnURL string 149 - AddAccount bool 150 - } 151 - 152 - func (o *OAuth) SetAuthReturn(w http.ResponseWriter, r *http.Request, returnURL string, addAccount bool) error { 153 - session, err := o.SessStore.Get(r, AuthReturnName) 154 - if err != nil { 155 - return err 156 - } 157 - 158 - session.Values[AuthReturnURL] = returnURL 159 - session.Values[AuthAddAccount] = addAccount 160 - session.Options.MaxAge = 60 * 30 161 - session.Options.HttpOnly = true 162 - session.Options.Secure = !o.Config.Core.Dev 163 - session.Options.SameSite = http.SameSiteLaxMode 164 - 165 - return session.Save(r, w) 166 - } 167 - 168 - func (o *OAuth) GetAuthReturn(r *http.Request) *AuthReturnInfo { 169 - session, err := o.SessStore.Get(r, AuthReturnName) 170 - if err != nil || session.IsNew { 171 - return &AuthReturnInfo{} 172 - } 173 - 174 - returnURL, _ := session.Values[AuthReturnURL].(string) 175 - addAccount, _ := session.Values[AuthAddAccount].(bool) 176 - 177 - return &AuthReturnInfo{ 178 - ReturnURL: returnURL, 179 - AddAccount: addAccount, 180 - } 181 - } 182 - 183 - func (o *OAuth) ClearAuthReturn(w http.ResponseWriter, r *http.Request) error { 184 - session, err := o.SessStore.Get(r, AuthReturnName) 185 - if err != nil { 186 - return err 187 - } 188 - 189 - session.Options.MaxAge = -1 190 - return session.Save(r, w) 191 - }
···
-265
appview/oauth/accounts_test.go
··· 1 - package oauth 2 - 3 - import ( 4 - "testing" 5 - ) 6 - 7 - func TestAccountRegistry_AddAccount(t *testing.T) { 8 - tests := []struct { 9 - name string 10 - initial []AccountInfo 11 - addDid string 12 - addHandle string 13 - addSessionId string 14 - wantErr error 15 - wantLen int 16 - wantSessionId string 17 - }{ 18 - { 19 - name: "add first account", 20 - initial: []AccountInfo{}, 21 - addDid: "did:plc:abc123", 22 - addHandle: "alice.bsky.social", 23 - addSessionId: "session-1", 24 - wantErr: nil, 25 - wantLen: 1, 26 - wantSessionId: "session-1", 27 - }, 28 - { 29 - name: "add second account", 30 - initial: []AccountInfo{ 31 - {Did: "did:plc:abc123", Handle: "alice.bsky.social", SessionId: "session-1", AddedAt: 1000}, 32 - }, 33 - addDid: "did:plc:def456", 34 - addHandle: "bob.bsky.social", 35 - addSessionId: "session-2", 36 - wantErr: nil, 37 - wantLen: 2, 38 - wantSessionId: "session-2", 39 - }, 40 - { 41 - name: "update existing account session", 42 - initial: []AccountInfo{ 43 - {Did: "did:plc:abc123", Handle: "alice.bsky.social", SessionId: "old-session", AddedAt: 1000}, 44 - }, 45 - addDid: "did:plc:abc123", 46 - addHandle: "alice.bsky.social", 47 - addSessionId: "new-session", 48 - wantErr: nil, 49 - wantLen: 1, 50 - wantSessionId: "new-session", 51 - }, 52 - } 53 - 54 - for _, tt := range tests { 55 - t.Run(tt.name, func(t *testing.T) { 56 - registry := &AccountRegistry{Accounts: tt.initial} 57 - err := registry.AddAccount(tt.addDid, tt.addHandle, tt.addSessionId) 58 - 59 - if err != tt.wantErr { 60 - t.Errorf("AddAccount() error = %v, want %v", err, tt.wantErr) 61 - } 62 - 63 - if len(registry.Accounts) != tt.wantLen { 64 - t.Errorf("AddAccount() len = %d, want %d", len(registry.Accounts), tt.wantLen) 65 - } 66 - 67 - found := registry.FindAccount(tt.addDid) 68 - if found == nil { 69 - t.Errorf("AddAccount() account not found after add") 70 - return 71 - } 72 - 73 - if found.SessionId != tt.wantSessionId { 74 - t.Errorf("AddAccount() sessionId = %s, want %s", found.SessionId, tt.wantSessionId) 75 - } 76 - }) 77 - } 78 - } 79 - 80 - func TestAccountRegistry_AddAccount_MaxLimit(t *testing.T) { 81 - registry := &AccountRegistry{Accounts: make([]AccountInfo, 0, MaxAccounts)} 82 - 83 - for i := range MaxAccounts { 84 - err := registry.AddAccount("did:plc:user"+string(rune('a'+i)), "handle", "session") 85 - if err != nil { 86 - t.Fatalf("AddAccount() unexpected error on account %d: %v", i, err) 87 - } 88 - } 89 - 90 - if len(registry.Accounts) != MaxAccounts { 91 - t.Errorf("expected %d accounts, got %d", MaxAccounts, len(registry.Accounts)) 92 - } 93 - 94 - err := registry.AddAccount("did:plc:overflow", "overflow", "session-overflow") 95 - if err != ErrMaxAccountsReached { 96 - t.Errorf("AddAccount() error = %v, want %v", err, ErrMaxAccountsReached) 97 - } 98 - 99 - if len(registry.Accounts) != MaxAccounts { 100 - t.Errorf("account added despite max limit, got %d", len(registry.Accounts)) 101 - } 102 - } 103 - 104 - func TestAccountRegistry_RemoveAccount(t *testing.T) { 105 - tests := []struct { 106 - name string 107 - initial []AccountInfo 108 - removeDid string 109 - wantLen int 110 - wantDids []string 111 - }{ 112 - { 113 - name: "remove existing account", 114 - initial: []AccountInfo{ 115 - {Did: "did:plc:abc123", Handle: "alice", SessionId: "s1"}, 116 - {Did: "did:plc:def456", Handle: "bob", SessionId: "s2"}, 117 - }, 118 - removeDid: "did:plc:abc123", 119 - wantLen: 1, 120 - wantDids: []string{"did:plc:def456"}, 121 - }, 122 - { 123 - name: "remove non-existing account", 124 - initial: []AccountInfo{ 125 - {Did: "did:plc:abc123", Handle: "alice", SessionId: "s1"}, 126 - }, 127 - removeDid: "did:plc:notfound", 128 - wantLen: 1, 129 - wantDids: []string{"did:plc:abc123"}, 130 - }, 131 - { 132 - name: "remove last account", 133 - initial: []AccountInfo{ 134 - {Did: "did:plc:abc123", Handle: "alice", SessionId: "s1"}, 135 - }, 136 - removeDid: "did:plc:abc123", 137 - wantLen: 0, 138 - wantDids: []string{}, 139 - }, 140 - { 141 - name: "remove from empty registry", 142 - initial: []AccountInfo{}, 143 - removeDid: "did:plc:abc123", 144 - wantLen: 0, 145 - wantDids: []string{}, 146 - }, 147 - } 148 - 149 - for _, tt := range tests { 150 - t.Run(tt.name, func(t *testing.T) { 151 - registry := &AccountRegistry{Accounts: tt.initial} 152 - registry.RemoveAccount(tt.removeDid) 153 - 154 - if len(registry.Accounts) != tt.wantLen { 155 - t.Errorf("RemoveAccount() len = %d, want %d", len(registry.Accounts), tt.wantLen) 156 - } 157 - 158 - for _, wantDid := range tt.wantDids { 159 - if registry.FindAccount(wantDid) == nil { 160 - t.Errorf("RemoveAccount() expected %s to remain", wantDid) 161 - } 162 - } 163 - 164 - if registry.FindAccount(tt.removeDid) != nil && tt.wantLen < len(tt.initial) { 165 - t.Errorf("RemoveAccount() %s should have been removed", tt.removeDid) 166 - } 167 - }) 168 - } 169 - } 170 - 171 - func TestAccountRegistry_FindAccount(t *testing.T) { 172 - registry := &AccountRegistry{ 173 - Accounts: []AccountInfo{ 174 - {Did: "did:plc:first", Handle: "first", SessionId: "s1", AddedAt: 1000}, 175 - {Did: "did:plc:second", Handle: "second", SessionId: "s2", AddedAt: 2000}, 176 - {Did: "did:plc:third", Handle: "third", SessionId: "s3", AddedAt: 3000}, 177 - }, 178 - } 179 - 180 - t.Run("find existing account", func(t *testing.T) { 181 - found := registry.FindAccount("did:plc:second") 182 - if found == nil { 183 - t.Fatal("FindAccount() returned nil for existing account") 184 - } 185 - if found.Handle != "second" { 186 - t.Errorf("FindAccount() handle = %s, want second", found.Handle) 187 - } 188 - if found.SessionId != "s2" { 189 - t.Errorf("FindAccount() sessionId = %s, want s2", found.SessionId) 190 - } 191 - }) 192 - 193 - t.Run("find non-existing account", func(t *testing.T) { 194 - found := registry.FindAccount("did:plc:notfound") 195 - if found != nil { 196 - t.Errorf("FindAccount() = %v, want nil", found) 197 - } 198 - }) 199 - 200 - t.Run("returned pointer is mutable", func(t *testing.T) { 201 - found := registry.FindAccount("did:plc:first") 202 - if found == nil { 203 - t.Fatal("FindAccount() returned nil") 204 - } 205 - found.SessionId = "modified" 206 - 207 - refetch := registry.FindAccount("did:plc:first") 208 - if refetch.SessionId != "modified" { 209 - t.Errorf("FindAccount() pointer not referencing original, got %s", refetch.SessionId) 210 - } 211 - }) 212 - } 213 - 214 - func TestAccountRegistry_OtherAccounts(t *testing.T) { 215 - registry := &AccountRegistry{ 216 - Accounts: []AccountInfo{ 217 - {Did: "did:plc:active", Handle: "active", SessionId: "s1"}, 218 - {Did: "did:plc:other1", Handle: "other1", SessionId: "s2"}, 219 - {Did: "did:plc:other2", Handle: "other2", SessionId: "s3"}, 220 - }, 221 - } 222 - 223 - others := registry.OtherAccounts("did:plc:active") 224 - 225 - if len(others) != 2 { 226 - t.Errorf("OtherAccounts() len = %d, want 2", len(others)) 227 - } 228 - 229 - for _, acc := range others { 230 - if acc.Did == "did:plc:active" { 231 - t.Errorf("OtherAccounts() should not include active account") 232 - } 233 - } 234 - 235 - hasDid := func(did string) bool { 236 - for _, acc := range others { 237 - if acc.Did == did { 238 - return true 239 - } 240 - } 241 - return false 242 - } 243 - 244 - if !hasDid("did:plc:other1") || !hasDid("did:plc:other2") { 245 - t.Errorf("OtherAccounts() missing expected accounts") 246 - } 247 - } 248 - 249 - func TestMultiAccountUser_Did(t *testing.T) { 250 - t.Run("with active user", func(t *testing.T) { 251 - user := &MultiAccountUser{ 252 - Active: &User{Did: "did:plc:test", Pds: "https://bsky.social"}, 253 - } 254 - if user.Did() != "did:plc:test" { 255 - t.Errorf("Did() = %s, want did:plc:test", user.Did()) 256 - } 257 - }) 258 - 259 - t.Run("with nil active", func(t *testing.T) { 260 - user := &MultiAccountUser{Active: nil} 261 - if user.Did() != "" { 262 - t.Errorf("Did() = %s, want empty string", user.Did()) 263 - } 264 - }) 265 - }
···
-4
appview/oauth/consts.go
··· 2 3 const ( 4 SessionName = "appview-session-v2" 5 - AccountsName = "appview-accounts-v2" 6 - AuthReturnName = "appview-auth-return" 7 - AuthReturnURL = "return_url" 8 - AuthAddAccount = "add_account" 9 SessionHandle = "handle" 10 SessionDid = "did" 11 SessionId = "id"
··· 2 3 const ( 4 SessionName = "appview-session-v2" 5 SessionHandle = "handle" 6 SessionDid = "did" 7 SessionId = "id"
+2 -14
appview/oauth/handler.go
··· 55 ctx := r.Context() 56 l := o.Logger.With("query", r.URL.Query()) 57 58 - authReturn := o.GetAuthReturn(r) 59 - _ = o.ClearAuthReturn(w, r) 60 - 61 sessData, err := o.ClientApp.ProcessCallback(ctx, r.URL.Query()) 62 if err != nil { 63 var callbackErr *oauth.AuthRequestCallbackError ··· 73 74 if err := o.SaveSession(w, r, sessData); err != nil { 75 l.Error("failed to save session", "data", sessData, "err", err) 76 - errorCode := "session" 77 - if errors.Is(err, ErrMaxAccountsReached) { 78 - errorCode = "max_accounts" 79 - } 80 - http.Redirect(w, r, fmt.Sprintf("/login?error=%s", errorCode), http.StatusFound) 81 return 82 } 83 ··· 95 } 96 } 97 98 - redirectURL := "/" 99 - if authReturn.ReturnURL != "" { 100 - redirectURL = authReturn.ReturnURL 101 - } 102 - 103 - http.Redirect(w, r, redirectURL, http.StatusFound) 104 } 105 106 func (o *OAuth) addToDefaultSpindle(did string) {
··· 55 ctx := r.Context() 56 l := o.Logger.With("query", r.URL.Query()) 57 58 sessData, err := o.ClientApp.ProcessCallback(ctx, r.URL.Query()) 59 if err != nil { 60 var callbackErr *oauth.AuthRequestCallbackError ··· 70 71 if err := o.SaveSession(w, r, sessData); err != nil { 72 l.Error("failed to save session", "data", sessData, "err", err) 73 + http.Redirect(w, r, "/login?error=session", http.StatusFound) 74 return 75 } 76 ··· 88 } 89 } 90 91 + http.Redirect(w, r, "/", http.StatusFound) 92 } 93 94 func (o *OAuth) addToDefaultSpindle(did string) {
+4 -66
appview/oauth/oauth.go
··· 98 } 99 100 func (o *OAuth) SaveSession(w http.ResponseWriter, r *http.Request, sessData *oauth.ClientSessionData) error { 101 userSession, err := o.SessStore.Get(r, SessionName) 102 if err != nil { 103 return err ··· 107 userSession.Values[SessionPds] = sessData.HostURL 108 userSession.Values[SessionId] = sessData.SessionID 109 userSession.Values[SessionAuthenticated] = true 110 - 111 - if err := userSession.Save(r, w); err != nil { 112 - return err 113 - } 114 - 115 - handle := "" 116 - resolved, err := o.IdResolver.ResolveIdent(r.Context(), sessData.AccountDID.String()) 117 - if err == nil && resolved.Handle.String() != "" { 118 - handle = resolved.Handle.String() 119 - } 120 - 121 - registry := o.GetAccounts(r) 122 - if err := registry.AddAccount(sessData.AccountDID.String(), handle, sessData.SessionID); err != nil { 123 - return err 124 - } 125 - return o.SaveAccounts(w, r, registry) 126 } 127 128 func (o *OAuth) ResumeSession(r *http.Request) (*oauth.ClientSession, error) { ··· 177 return errors.Join(err1, err2) 178 } 179 180 - func (o *OAuth) SwitchAccount(w http.ResponseWriter, r *http.Request, targetDid string) error { 181 - registry := o.GetAccounts(r) 182 - account := registry.FindAccount(targetDid) 183 - if account == nil { 184 - return fmt.Errorf("account not found in registry: %s", targetDid) 185 - } 186 - 187 - did, err := syntax.ParseDID(targetDid) 188 - if err != nil { 189 - return fmt.Errorf("invalid DID: %w", err) 190 - } 191 - 192 - sess, err := o.ClientApp.ResumeSession(r.Context(), did, account.SessionId) 193 - if err != nil { 194 - registry.RemoveAccount(targetDid) 195 - _ = o.SaveAccounts(w, r, registry) 196 - return fmt.Errorf("session expired for account: %w", err) 197 - } 198 - 199 - userSession, err := o.SessStore.Get(r, SessionName) 200 - if err != nil { 201 - return err 202 - } 203 - 204 - userSession.Values[SessionDid] = sess.Data.AccountDID.String() 205 - userSession.Values[SessionPds] = sess.Data.HostURL 206 - userSession.Values[SessionId] = sess.Data.SessionID 207 - userSession.Values[SessionAuthenticated] = true 208 - 209 - return userSession.Save(r, w) 210 - } 211 - 212 - func (o *OAuth) RemoveAccount(w http.ResponseWriter, r *http.Request, targetDid string) error { 213 - registry := o.GetAccounts(r) 214 - account := registry.FindAccount(targetDid) 215 - if account == nil { 216 - return nil 217 - } 218 - 219 - did, err := syntax.ParseDID(targetDid) 220 - if err == nil { 221 - _ = o.ClientApp.Logout(r.Context(), did, account.SessionId) 222 - } 223 - 224 - registry.RemoveAccount(targetDid) 225 - return o.SaveAccounts(w, r, registry) 226 - } 227 - 228 type User struct { 229 Did string 230 Pds string ··· 243 } 244 245 func (o *OAuth) GetDid(r *http.Request) string { 246 - if u := o.GetMultiAccountUser(r); u != nil { 247 - return u.Did() 248 } 249 250 return ""
··· 98 } 99 100 func (o *OAuth) SaveSession(w http.ResponseWriter, r *http.Request, sessData *oauth.ClientSessionData) error { 101 + // first we save the did in the user session 102 userSession, err := o.SessStore.Get(r, SessionName) 103 if err != nil { 104 return err ··· 108 userSession.Values[SessionPds] = sessData.HostURL 109 userSession.Values[SessionId] = sessData.SessionID 110 userSession.Values[SessionAuthenticated] = true 111 + return userSession.Save(r, w) 112 } 113 114 func (o *OAuth) ResumeSession(r *http.Request) (*oauth.ClientSession, error) { ··· 163 return errors.Join(err1, err2) 164 } 165 166 type User struct { 167 Did string 168 Pds string ··· 181 } 182 183 func (o *OAuth) GetDid(r *http.Request) string { 184 + if u := o.GetUser(r); u != nil { 185 + return u.Did 186 } 187 188 return ""
+9 -30
appview/pages/funcmap.go
··· 26 "github.com/go-enry/go-enry/v2" 27 "github.com/yuin/goldmark" 28 emoji "github.com/yuin/goldmark-emoji" 29 "tangled.org/core/appview/models" 30 - "tangled.org/core/appview/oauth" 31 "tangled.org/core/appview/pages/markup" 32 "tangled.org/core/crypto" 33 ) ··· 334 }, 335 "deref": func(v any) any { 336 val := reflect.ValueOf(v) 337 - if val.Kind() == reflect.Pointer && !val.IsNil() { 338 return val.Elem().Interface() 339 } 340 return nil ··· 348 return template.HTML(data) 349 }, 350 "cssContentHash": p.CssContentHash, 351 "pathEscape": func(s string) string { 352 return url.PathEscape(s) 353 }, ··· 365 return p.AvatarUrl(handle, "") 366 }, 367 "langColor": enry.GetColor, 368 - "reverse": func(s any) any { 369 - if s == nil { 370 - return nil 371 - } 372 - 373 - v := reflect.ValueOf(s) 374 - 375 - if v.Kind() != reflect.Slice { 376 - return s 377 - } 378 379 - length := v.Len() 380 - reversed := reflect.MakeSlice(v.Type(), length, length) 381 - 382 - for i := range length { 383 - reversed.Index(i).Set(v.Index(length - 1 - i)) 384 - } 385 - 386 - return reversed.Interface() 387 - }, 388 "normalizeForHtmlId": func(s string) string { 389 normalized := strings.ReplaceAll(s, ":", "_") 390 normalized = strings.ReplaceAll(normalized, ".", "_") ··· 396 return "error" 397 } 398 return fp 399 - }, 400 - "otherAccounts": func(activeDid string, accounts []oauth.AccountInfo) []oauth.AccountInfo { 401 - result := make([]oauth.AccountInfo, 0, len(accounts)) 402 - for _, acc := range accounts { 403 - if acc.Did != activeDid { 404 - result = append(result, acc) 405 - } 406 - } 407 - return result 408 }, 409 } 410 }
··· 26 "github.com/go-enry/go-enry/v2" 27 "github.com/yuin/goldmark" 28 emoji "github.com/yuin/goldmark-emoji" 29 + "tangled.org/core/appview/filetree" 30 "tangled.org/core/appview/models" 31 "tangled.org/core/appview/pages/markup" 32 "tangled.org/core/crypto" 33 ) ··· 334 }, 335 "deref": func(v any) any { 336 val := reflect.ValueOf(v) 337 + if val.Kind() == reflect.Ptr && !val.IsNil() { 338 return val.Elem().Interface() 339 } 340 return nil ··· 348 return template.HTML(data) 349 }, 350 "cssContentHash": p.CssContentHash, 351 + "fileTree": filetree.FileTree, 352 "pathEscape": func(s string) string { 353 return url.PathEscape(s) 354 }, ··· 366 return p.AvatarUrl(handle, "") 367 }, 368 "langColor": enry.GetColor, 369 + "layoutSide": func() string { 370 + return "col-span-1 md:col-span-2 lg:col-span-3" 371 + }, 372 + "layoutCenter": func() string { 373 + return "col-span-1 md:col-span-8 lg:col-span-6" 374 + }, 375 376 "normalizeForHtmlId": func(s string) string { 377 normalized := strings.ReplaceAll(s, ":", "_") 378 normalized = strings.ReplaceAll(normalized, ".", "_") ··· 384 return "error" 385 } 386 return fp 387 }, 388 } 389 }
+66 -72
appview/pages/pages.go
··· 226 } 227 228 type LoginParams struct { 229 - ReturnUrl string 230 - ErrorCode string 231 - AddAccount bool 232 - LoggedInUser *oauth.MultiAccountUser 233 } 234 235 func (p *Pages) Login(w io.Writer, params LoginParams) error { ··· 249 } 250 251 type TermsOfServiceParams struct { 252 - LoggedInUser *oauth.MultiAccountUser 253 Content template.HTML 254 } 255 ··· 277 } 278 279 type PrivacyPolicyParams struct { 280 - LoggedInUser *oauth.MultiAccountUser 281 Content template.HTML 282 } 283 ··· 305 } 306 307 type BrandParams struct { 308 - LoggedInUser *oauth.MultiAccountUser 309 } 310 311 func (p *Pages) Brand(w io.Writer, params BrandParams) error { ··· 313 } 314 315 type TimelineParams struct { 316 - LoggedInUser *oauth.MultiAccountUser 317 Timeline []models.TimelineEvent 318 Repos []models.Repo 319 GfiLabel *models.LabelDefinition ··· 324 } 325 326 type GoodFirstIssuesParams struct { 327 - LoggedInUser *oauth.MultiAccountUser 328 Issues []models.Issue 329 RepoGroups []*models.RepoGroup 330 LabelDefs map[string]*models.LabelDefinition ··· 337 } 338 339 type UserProfileSettingsParams struct { 340 - LoggedInUser *oauth.MultiAccountUser 341 Tabs []map[string]any 342 Tab string 343 } ··· 347 } 348 349 type NotificationsParams struct { 350 - LoggedInUser *oauth.MultiAccountUser 351 Notifications []*models.NotificationWithEntity 352 UnreadCount int 353 Page pagination.Page ··· 375 } 376 377 type UserKeysSettingsParams struct { 378 - LoggedInUser *oauth.MultiAccountUser 379 PubKeys []models.PublicKey 380 Tabs []map[string]any 381 Tab string ··· 386 } 387 388 type UserEmailsSettingsParams struct { 389 - LoggedInUser *oauth.MultiAccountUser 390 Emails []models.Email 391 Tabs []map[string]any 392 Tab string ··· 397 } 398 399 type UserNotificationSettingsParams struct { 400 - LoggedInUser *oauth.MultiAccountUser 401 Preferences *models.NotificationPreferences 402 Tabs []map[string]any 403 Tab string ··· 417 } 418 419 type KnotsParams struct { 420 - LoggedInUser *oauth.MultiAccountUser 421 Registrations []models.Registration 422 Tabs []map[string]any 423 Tab string ··· 428 } 429 430 type KnotParams struct { 431 - LoggedInUser *oauth.MultiAccountUser 432 Registration *models.Registration 433 Members []string 434 Repos map[string][]models.Repo ··· 450 } 451 452 type SpindlesParams struct { 453 - LoggedInUser *oauth.MultiAccountUser 454 Spindles []models.Spindle 455 Tabs []map[string]any 456 Tab string ··· 471 } 472 473 type SpindleDashboardParams struct { 474 - LoggedInUser *oauth.MultiAccountUser 475 Spindle models.Spindle 476 Members []string 477 Repos map[string][]models.Repo ··· 484 } 485 486 type NewRepoParams struct { 487 - LoggedInUser *oauth.MultiAccountUser 488 Knots []string 489 } 490 ··· 493 } 494 495 type ForkRepoParams struct { 496 - LoggedInUser *oauth.MultiAccountUser 497 Knots []string 498 RepoInfo repoinfo.RepoInfo 499 } ··· 531 } 532 533 type ProfileOverviewParams struct { 534 - LoggedInUser *oauth.MultiAccountUser 535 Repos []models.Repo 536 CollaboratingRepos []models.Repo 537 ProfileTimeline *models.ProfileTimeline ··· 545 } 546 547 type ProfileReposParams struct { 548 - LoggedInUser *oauth.MultiAccountUser 549 Repos []models.Repo 550 Card *ProfileCard 551 Active string ··· 557 } 558 559 type ProfileStarredParams struct { 560 - LoggedInUser *oauth.MultiAccountUser 561 Repos []models.Repo 562 Card *ProfileCard 563 Active string ··· 569 } 570 571 type ProfileStringsParams struct { 572 - LoggedInUser *oauth.MultiAccountUser 573 Strings []models.String 574 Card *ProfileCard 575 Active string ··· 582 583 type FollowCard struct { 584 UserDid string 585 - LoggedInUser *oauth.MultiAccountUser 586 FollowStatus models.FollowStatus 587 FollowersCount int64 588 FollowingCount int64 ··· 590 } 591 592 type ProfileFollowersParams struct { 593 - LoggedInUser *oauth.MultiAccountUser 594 Followers []FollowCard 595 Card *ProfileCard 596 Active string ··· 602 } 603 604 type ProfileFollowingParams struct { 605 - LoggedInUser *oauth.MultiAccountUser 606 Following []FollowCard 607 Card *ProfileCard 608 Active string ··· 624 } 625 626 type EditBioParams struct { 627 - LoggedInUser *oauth.MultiAccountUser 628 Profile *models.Profile 629 } 630 ··· 633 } 634 635 type EditPinsParams struct { 636 - LoggedInUser *oauth.MultiAccountUser 637 Profile *models.Profile 638 AllRepos []PinnedRepo 639 } ··· 660 } 661 662 type RepoIndexParams struct { 663 - LoggedInUser *oauth.MultiAccountUser 664 RepoInfo repoinfo.RepoInfo 665 Active string 666 TagMap map[string][]string ··· 709 } 710 711 type RepoLogParams struct { 712 - LoggedInUser *oauth.MultiAccountUser 713 RepoInfo repoinfo.RepoInfo 714 TagMap map[string][]string 715 Active string ··· 726 } 727 728 type RepoCommitParams struct { 729 - LoggedInUser *oauth.MultiAccountUser 730 RepoInfo repoinfo.RepoInfo 731 Active string 732 EmailToDid map[string]string ··· 745 } 746 747 type RepoTreeParams struct { 748 - LoggedInUser *oauth.MultiAccountUser 749 RepoInfo repoinfo.RepoInfo 750 Active string 751 BreadCrumbs [][]string ··· 800 } 801 802 type RepoBranchesParams struct { 803 - LoggedInUser *oauth.MultiAccountUser 804 RepoInfo repoinfo.RepoInfo 805 Active string 806 types.RepoBranchesResponse ··· 812 } 813 814 type RepoTagsParams struct { 815 - LoggedInUser *oauth.MultiAccountUser 816 RepoInfo repoinfo.RepoInfo 817 Active string 818 types.RepoTagsResponse ··· 826 } 827 828 type RepoArtifactParams struct { 829 - LoggedInUser *oauth.MultiAccountUser 830 RepoInfo repoinfo.RepoInfo 831 Artifact models.Artifact 832 } ··· 836 } 837 838 type RepoBlobParams struct { 839 - LoggedInUser *oauth.MultiAccountUser 840 RepoInfo repoinfo.RepoInfo 841 Active string 842 BreadCrumbs [][]string ··· 860 } 861 862 type RepoSettingsParams struct { 863 - LoggedInUser *oauth.MultiAccountUser 864 RepoInfo repoinfo.RepoInfo 865 Collaborators []Collaborator 866 Active string ··· 879 } 880 881 type RepoGeneralSettingsParams struct { 882 - LoggedInUser *oauth.MultiAccountUser 883 RepoInfo repoinfo.RepoInfo 884 Labels []models.LabelDefinition 885 DefaultLabels []models.LabelDefinition ··· 897 } 898 899 type RepoAccessSettingsParams struct { 900 - LoggedInUser *oauth.MultiAccountUser 901 RepoInfo repoinfo.RepoInfo 902 Active string 903 Tabs []map[string]any ··· 911 } 912 913 type RepoPipelineSettingsParams struct { 914 - LoggedInUser *oauth.MultiAccountUser 915 RepoInfo repoinfo.RepoInfo 916 Active string 917 Tabs []map[string]any ··· 927 } 928 929 type RepoIssuesParams struct { 930 - LoggedInUser *oauth.MultiAccountUser 931 RepoInfo repoinfo.RepoInfo 932 Active string 933 Issues []models.Issue ··· 944 } 945 946 type RepoSingleIssueParams struct { 947 - LoggedInUser *oauth.MultiAccountUser 948 RepoInfo repoinfo.RepoInfo 949 Active string 950 Issue *models.Issue ··· 963 } 964 965 type EditIssueParams struct { 966 - LoggedInUser *oauth.MultiAccountUser 967 RepoInfo repoinfo.RepoInfo 968 Issue *models.Issue 969 Action string ··· 987 } 988 989 type RepoNewIssueParams struct { 990 - LoggedInUser *oauth.MultiAccountUser 991 RepoInfo repoinfo.RepoInfo 992 Issue *models.Issue // existing issue if any -- passed when editing 993 Active string ··· 1001 } 1002 1003 type EditIssueCommentParams struct { 1004 - LoggedInUser *oauth.MultiAccountUser 1005 RepoInfo repoinfo.RepoInfo 1006 Issue *models.Issue 1007 Comment *models.IssueComment ··· 1012 } 1013 1014 type ReplyIssueCommentPlaceholderParams struct { 1015 - LoggedInUser *oauth.MultiAccountUser 1016 RepoInfo repoinfo.RepoInfo 1017 Issue *models.Issue 1018 Comment *models.IssueComment ··· 1023 } 1024 1025 type ReplyIssueCommentParams struct { 1026 - LoggedInUser *oauth.MultiAccountUser 1027 RepoInfo repoinfo.RepoInfo 1028 Issue *models.Issue 1029 Comment *models.IssueComment ··· 1034 } 1035 1036 type IssueCommentBodyParams struct { 1037 - LoggedInUser *oauth.MultiAccountUser 1038 RepoInfo repoinfo.RepoInfo 1039 Issue *models.Issue 1040 Comment *models.IssueComment ··· 1045 } 1046 1047 type RepoNewPullParams struct { 1048 - LoggedInUser *oauth.MultiAccountUser 1049 RepoInfo repoinfo.RepoInfo 1050 Branches []types.Branch 1051 Strategy string ··· 1062 } 1063 1064 type RepoPullsParams struct { 1065 - LoggedInUser *oauth.MultiAccountUser 1066 RepoInfo repoinfo.RepoInfo 1067 Pulls []*models.Pull 1068 Active string ··· 1099 } 1100 1101 type RepoSinglePullParams struct { 1102 - LoggedInUser *oauth.MultiAccountUser 1103 RepoInfo repoinfo.RepoInfo 1104 Active string 1105 Pull *models.Pull ··· 1110 MergeCheck types.MergeCheckResponse 1111 ResubmitCheck ResubmitResult 1112 Pipelines map[string]models.Pipeline 1113 - Diff types.DiffRenderer 1114 - DiffOpts types.DiffOpts 1115 - ActiveRound int 1116 - IsInterdiff bool 1117 1118 OrderedReactionKinds []models.ReactionKind 1119 Reactions map[models.ReactionKind]models.ReactionDisplayData ··· 1128 } 1129 1130 type RepoPullPatchParams struct { 1131 - LoggedInUser *oauth.MultiAccountUser 1132 RepoInfo repoinfo.RepoInfo 1133 Pull *models.Pull 1134 Stack models.Stack ··· 1145 } 1146 1147 type RepoPullInterdiffParams struct { 1148 - LoggedInUser *oauth.MultiAccountUser 1149 RepoInfo repoinfo.RepoInfo 1150 Pull *models.Pull 1151 Round int ··· 1198 } 1199 1200 type PullResubmitParams struct { 1201 - LoggedInUser *oauth.MultiAccountUser 1202 RepoInfo repoinfo.RepoInfo 1203 Pull *models.Pull 1204 SubmissionId int ··· 1209 } 1210 1211 type PullActionsParams struct { 1212 - LoggedInUser *oauth.MultiAccountUser 1213 RepoInfo repoinfo.RepoInfo 1214 Pull *models.Pull 1215 RoundNumber int ··· 1224 } 1225 1226 type PullNewCommentParams struct { 1227 - LoggedInUser *oauth.MultiAccountUser 1228 RepoInfo repoinfo.RepoInfo 1229 Pull *models.Pull 1230 RoundNumber int ··· 1235 } 1236 1237 type RepoCompareParams struct { 1238 - LoggedInUser *oauth.MultiAccountUser 1239 RepoInfo repoinfo.RepoInfo 1240 Forks []models.Repo 1241 Branches []types.Branch ··· 1254 } 1255 1256 type RepoCompareNewParams struct { 1257 - LoggedInUser *oauth.MultiAccountUser 1258 RepoInfo repoinfo.RepoInfo 1259 Forks []models.Repo 1260 Branches []types.Branch ··· 1271 } 1272 1273 type RepoCompareAllowPullParams struct { 1274 - LoggedInUser *oauth.MultiAccountUser 1275 RepoInfo repoinfo.RepoInfo 1276 Base string 1277 Head string ··· 1291 } 1292 1293 type LabelPanelParams struct { 1294 - LoggedInUser *oauth.MultiAccountUser 1295 RepoInfo repoinfo.RepoInfo 1296 Defs map[string]*models.LabelDefinition 1297 Subject string ··· 1303 } 1304 1305 type EditLabelPanelParams struct { 1306 - LoggedInUser *oauth.MultiAccountUser 1307 RepoInfo repoinfo.RepoInfo 1308 Defs map[string]*models.LabelDefinition 1309 Subject string ··· 1315 } 1316 1317 type PipelinesParams struct { 1318 - LoggedInUser *oauth.MultiAccountUser 1319 RepoInfo repoinfo.RepoInfo 1320 Pipelines []models.Pipeline 1321 Active string ··· 1358 } 1359 1360 type WorkflowParams struct { 1361 - LoggedInUser *oauth.MultiAccountUser 1362 RepoInfo repoinfo.RepoInfo 1363 Pipeline models.Pipeline 1364 Workflow string ··· 1372 } 1373 1374 type PutStringParams struct { 1375 - LoggedInUser *oauth.MultiAccountUser 1376 Action string 1377 1378 // this is supplied in the case of editing an existing string ··· 1384 } 1385 1386 type StringsDashboardParams struct { 1387 - LoggedInUser *oauth.MultiAccountUser 1388 Card ProfileCard 1389 Strings []models.String 1390 } ··· 1394 } 1395 1396 type StringTimelineParams struct { 1397 - LoggedInUser *oauth.MultiAccountUser 1398 Strings []models.String 1399 } 1400 ··· 1403 } 1404 1405 type SingleStringParams struct { 1406 - LoggedInUser *oauth.MultiAccountUser 1407 ShowRendered bool 1408 RenderToggle bool 1409 RenderedContents template.HTML
··· 226 } 227 228 type LoginParams struct { 229 + ReturnUrl string 230 + ErrorCode string 231 } 232 233 func (p *Pages) Login(w io.Writer, params LoginParams) error { ··· 247 } 248 249 type TermsOfServiceParams struct { 250 + LoggedInUser *oauth.User 251 Content template.HTML 252 } 253 ··· 275 } 276 277 type PrivacyPolicyParams struct { 278 + LoggedInUser *oauth.User 279 Content template.HTML 280 } 281 ··· 303 } 304 305 type BrandParams struct { 306 + LoggedInUser *oauth.User 307 } 308 309 func (p *Pages) Brand(w io.Writer, params BrandParams) error { ··· 311 } 312 313 type TimelineParams struct { 314 + LoggedInUser *oauth.User 315 Timeline []models.TimelineEvent 316 Repos []models.Repo 317 GfiLabel *models.LabelDefinition ··· 322 } 323 324 type GoodFirstIssuesParams struct { 325 + LoggedInUser *oauth.User 326 Issues []models.Issue 327 RepoGroups []*models.RepoGroup 328 LabelDefs map[string]*models.LabelDefinition ··· 335 } 336 337 type UserProfileSettingsParams struct { 338 + LoggedInUser *oauth.User 339 Tabs []map[string]any 340 Tab string 341 } ··· 345 } 346 347 type NotificationsParams struct { 348 + LoggedInUser *oauth.User 349 Notifications []*models.NotificationWithEntity 350 UnreadCount int 351 Page pagination.Page ··· 373 } 374 375 type UserKeysSettingsParams struct { 376 + LoggedInUser *oauth.User 377 PubKeys []models.PublicKey 378 Tabs []map[string]any 379 Tab string ··· 384 } 385 386 type UserEmailsSettingsParams struct { 387 + LoggedInUser *oauth.User 388 Emails []models.Email 389 Tabs []map[string]any 390 Tab string ··· 395 } 396 397 type UserNotificationSettingsParams struct { 398 + LoggedInUser *oauth.User 399 Preferences *models.NotificationPreferences 400 Tabs []map[string]any 401 Tab string ··· 415 } 416 417 type KnotsParams struct { 418 + LoggedInUser *oauth.User 419 Registrations []models.Registration 420 Tabs []map[string]any 421 Tab string ··· 426 } 427 428 type KnotParams struct { 429 + LoggedInUser *oauth.User 430 Registration *models.Registration 431 Members []string 432 Repos map[string][]models.Repo ··· 448 } 449 450 type SpindlesParams struct { 451 + LoggedInUser *oauth.User 452 Spindles []models.Spindle 453 Tabs []map[string]any 454 Tab string ··· 469 } 470 471 type SpindleDashboardParams struct { 472 + LoggedInUser *oauth.User 473 Spindle models.Spindle 474 Members []string 475 Repos map[string][]models.Repo ··· 482 } 483 484 type NewRepoParams struct { 485 + LoggedInUser *oauth.User 486 Knots []string 487 } 488 ··· 491 } 492 493 type ForkRepoParams struct { 494 + LoggedInUser *oauth.User 495 Knots []string 496 RepoInfo repoinfo.RepoInfo 497 } ··· 529 } 530 531 type ProfileOverviewParams struct { 532 + LoggedInUser *oauth.User 533 Repos []models.Repo 534 CollaboratingRepos []models.Repo 535 ProfileTimeline *models.ProfileTimeline ··· 543 } 544 545 type ProfileReposParams struct { 546 + LoggedInUser *oauth.User 547 Repos []models.Repo 548 Card *ProfileCard 549 Active string ··· 555 } 556 557 type ProfileStarredParams struct { 558 + LoggedInUser *oauth.User 559 Repos []models.Repo 560 Card *ProfileCard 561 Active string ··· 567 } 568 569 type ProfileStringsParams struct { 570 + LoggedInUser *oauth.User 571 Strings []models.String 572 Card *ProfileCard 573 Active string ··· 580 581 type FollowCard struct { 582 UserDid string 583 + LoggedInUser *oauth.User 584 FollowStatus models.FollowStatus 585 FollowersCount int64 586 FollowingCount int64 ··· 588 } 589 590 type ProfileFollowersParams struct { 591 + LoggedInUser *oauth.User 592 Followers []FollowCard 593 Card *ProfileCard 594 Active string ··· 600 } 601 602 type ProfileFollowingParams struct { 603 + LoggedInUser *oauth.User 604 Following []FollowCard 605 Card *ProfileCard 606 Active string ··· 622 } 623 624 type EditBioParams struct { 625 + LoggedInUser *oauth.User 626 Profile *models.Profile 627 } 628 ··· 631 } 632 633 type EditPinsParams struct { 634 + LoggedInUser *oauth.User 635 Profile *models.Profile 636 AllRepos []PinnedRepo 637 } ··· 658 } 659 660 type RepoIndexParams struct { 661 + LoggedInUser *oauth.User 662 RepoInfo repoinfo.RepoInfo 663 Active string 664 TagMap map[string][]string ··· 707 } 708 709 type RepoLogParams struct { 710 + LoggedInUser *oauth.User 711 RepoInfo repoinfo.RepoInfo 712 TagMap map[string][]string 713 Active string ··· 724 } 725 726 type RepoCommitParams struct { 727 + LoggedInUser *oauth.User 728 RepoInfo repoinfo.RepoInfo 729 Active string 730 EmailToDid map[string]string ··· 743 } 744 745 type RepoTreeParams struct { 746 + LoggedInUser *oauth.User 747 RepoInfo repoinfo.RepoInfo 748 Active string 749 BreadCrumbs [][]string ··· 798 } 799 800 type RepoBranchesParams struct { 801 + LoggedInUser *oauth.User 802 RepoInfo repoinfo.RepoInfo 803 Active string 804 types.RepoBranchesResponse ··· 810 } 811 812 type RepoTagsParams struct { 813 + LoggedInUser *oauth.User 814 RepoInfo repoinfo.RepoInfo 815 Active string 816 types.RepoTagsResponse ··· 824 } 825 826 type RepoArtifactParams struct { 827 + LoggedInUser *oauth.User 828 RepoInfo repoinfo.RepoInfo 829 Artifact models.Artifact 830 } ··· 834 } 835 836 type RepoBlobParams struct { 837 + LoggedInUser *oauth.User 838 RepoInfo repoinfo.RepoInfo 839 Active string 840 BreadCrumbs [][]string ··· 858 } 859 860 type RepoSettingsParams struct { 861 + LoggedInUser *oauth.User 862 RepoInfo repoinfo.RepoInfo 863 Collaborators []Collaborator 864 Active string ··· 877 } 878 879 type RepoGeneralSettingsParams struct { 880 + LoggedInUser *oauth.User 881 RepoInfo repoinfo.RepoInfo 882 Labels []models.LabelDefinition 883 DefaultLabels []models.LabelDefinition ··· 895 } 896 897 type RepoAccessSettingsParams struct { 898 + LoggedInUser *oauth.User 899 RepoInfo repoinfo.RepoInfo 900 Active string 901 Tabs []map[string]any ··· 909 } 910 911 type RepoPipelineSettingsParams struct { 912 + LoggedInUser *oauth.User 913 RepoInfo repoinfo.RepoInfo 914 Active string 915 Tabs []map[string]any ··· 925 } 926 927 type RepoIssuesParams struct { 928 + LoggedInUser *oauth.User 929 RepoInfo repoinfo.RepoInfo 930 Active string 931 Issues []models.Issue ··· 942 } 943 944 type RepoSingleIssueParams struct { 945 + LoggedInUser *oauth.User 946 RepoInfo repoinfo.RepoInfo 947 Active string 948 Issue *models.Issue ··· 961 } 962 963 type EditIssueParams struct { 964 + LoggedInUser *oauth.User 965 RepoInfo repoinfo.RepoInfo 966 Issue *models.Issue 967 Action string ··· 985 } 986 987 type RepoNewIssueParams struct { 988 + LoggedInUser *oauth.User 989 RepoInfo repoinfo.RepoInfo 990 Issue *models.Issue // existing issue if any -- passed when editing 991 Active string ··· 999 } 1000 1001 type EditIssueCommentParams struct { 1002 + LoggedInUser *oauth.User 1003 RepoInfo repoinfo.RepoInfo 1004 Issue *models.Issue 1005 Comment *models.IssueComment ··· 1010 } 1011 1012 type ReplyIssueCommentPlaceholderParams struct { 1013 + LoggedInUser *oauth.User 1014 RepoInfo repoinfo.RepoInfo 1015 Issue *models.Issue 1016 Comment *models.IssueComment ··· 1021 } 1022 1023 type ReplyIssueCommentParams struct { 1024 + LoggedInUser *oauth.User 1025 RepoInfo repoinfo.RepoInfo 1026 Issue *models.Issue 1027 Comment *models.IssueComment ··· 1032 } 1033 1034 type IssueCommentBodyParams struct { 1035 + LoggedInUser *oauth.User 1036 RepoInfo repoinfo.RepoInfo 1037 Issue *models.Issue 1038 Comment *models.IssueComment ··· 1043 } 1044 1045 type RepoNewPullParams struct { 1046 + LoggedInUser *oauth.User 1047 RepoInfo repoinfo.RepoInfo 1048 Branches []types.Branch 1049 Strategy string ··· 1060 } 1061 1062 type RepoPullsParams struct { 1063 + LoggedInUser *oauth.User 1064 RepoInfo repoinfo.RepoInfo 1065 Pulls []*models.Pull 1066 Active string ··· 1097 } 1098 1099 type RepoSinglePullParams struct { 1100 + LoggedInUser *oauth.User 1101 RepoInfo repoinfo.RepoInfo 1102 Active string 1103 Pull *models.Pull ··· 1108 MergeCheck types.MergeCheckResponse 1109 ResubmitCheck ResubmitResult 1110 Pipelines map[string]models.Pipeline 1111 1112 OrderedReactionKinds []models.ReactionKind 1113 Reactions map[models.ReactionKind]models.ReactionDisplayData ··· 1122 } 1123 1124 type RepoPullPatchParams struct { 1125 + LoggedInUser *oauth.User 1126 RepoInfo repoinfo.RepoInfo 1127 Pull *models.Pull 1128 Stack models.Stack ··· 1139 } 1140 1141 type RepoPullInterdiffParams struct { 1142 + LoggedInUser *oauth.User 1143 RepoInfo repoinfo.RepoInfo 1144 Pull *models.Pull 1145 Round int ··· 1192 } 1193 1194 type PullResubmitParams struct { 1195 + LoggedInUser *oauth.User 1196 RepoInfo repoinfo.RepoInfo 1197 Pull *models.Pull 1198 SubmissionId int ··· 1203 } 1204 1205 type PullActionsParams struct { 1206 + LoggedInUser *oauth.User 1207 RepoInfo repoinfo.RepoInfo 1208 Pull *models.Pull 1209 RoundNumber int ··· 1218 } 1219 1220 type PullNewCommentParams struct { 1221 + LoggedInUser *oauth.User 1222 RepoInfo repoinfo.RepoInfo 1223 Pull *models.Pull 1224 RoundNumber int ··· 1229 } 1230 1231 type RepoCompareParams struct { 1232 + LoggedInUser *oauth.User 1233 RepoInfo repoinfo.RepoInfo 1234 Forks []models.Repo 1235 Branches []types.Branch ··· 1248 } 1249 1250 type RepoCompareNewParams struct { 1251 + LoggedInUser *oauth.User 1252 RepoInfo repoinfo.RepoInfo 1253 Forks []models.Repo 1254 Branches []types.Branch ··· 1265 } 1266 1267 type RepoCompareAllowPullParams struct { 1268 + LoggedInUser *oauth.User 1269 RepoInfo repoinfo.RepoInfo 1270 Base string 1271 Head string ··· 1285 } 1286 1287 type LabelPanelParams struct { 1288 + LoggedInUser *oauth.User 1289 RepoInfo repoinfo.RepoInfo 1290 Defs map[string]*models.LabelDefinition 1291 Subject string ··· 1297 } 1298 1299 type EditLabelPanelParams struct { 1300 + LoggedInUser *oauth.User 1301 RepoInfo repoinfo.RepoInfo 1302 Defs map[string]*models.LabelDefinition 1303 Subject string ··· 1309 } 1310 1311 type PipelinesParams struct { 1312 + LoggedInUser *oauth.User 1313 RepoInfo repoinfo.RepoInfo 1314 Pipelines []models.Pipeline 1315 Active string ··· 1352 } 1353 1354 type WorkflowParams struct { 1355 + LoggedInUser *oauth.User 1356 RepoInfo repoinfo.RepoInfo 1357 Pipeline models.Pipeline 1358 Workflow string ··· 1366 } 1367 1368 type PutStringParams struct { 1369 + LoggedInUser *oauth.User 1370 Action string 1371 1372 // this is supplied in the case of editing an existing string ··· 1378 } 1379 1380 type StringsDashboardParams struct { 1381 + LoggedInUser *oauth.User 1382 Card ProfileCard 1383 Strings []models.String 1384 } ··· 1388 } 1389 1390 type StringTimelineParams struct { 1391 + LoggedInUser *oauth.User 1392 Strings []models.String 1393 } 1394 ··· 1397 } 1398 1399 type SingleStringParams struct { 1400 + LoggedInUser *oauth.User 1401 ShowRendered bool 1402 RenderToggle bool 1403 RenderedContents template.HTML
+1 -1
appview/pages/templates/banner.html
··· 30 <div class="mx-6"> 31 These services may not be fully accessible until upgraded. 32 <a class="underline text-red-800 dark:text-red-200" 33 - href="https://docs.tangled.org/migrating-knots-and-spindles.html"> 34 Click to read the upgrade guide</a>. 35 </div> 36 </details>
··· 30 <div class="mx-6"> 31 These services may not be fully accessible until upgraded. 32 <a class="underline text-red-800 dark:text-red-200" 33 + href="https://docs.tangled.org/migrating-knots-spindles.html#migrating-knots-spindles"> 34 Click to read the upgrade guide</a>. 35 </div> 36 </details>
+5 -7
appview/pages/templates/fragments/tinyAvatarList.html
··· 5 <div class="inline-flex items-center -space-x-3"> 6 {{ $c := "z-50 z-40 z-30 z-20 z-10" }} 7 {{ range $i, $p := $ps }} 8 - <a href="/{{ resolve . }}" title="{{ resolve . }}"> 9 - <img 10 - src="{{ tinyAvatar . }}" 11 - alt="" 12 - class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900 z-{{sub 5 $i}}0 {{ $classes }}" 13 - /> 14 - </a> 15 {{ end }} 16 17 {{ if gt (len $all) 5 }}
··· 5 <div class="inline-flex items-center -space-x-3"> 6 {{ $c := "z-50 z-40 z-30 z-20 z-10" }} 7 {{ range $i, $p := $ps }} 8 + <img 9 + src="{{ tinyAvatar . }}" 10 + alt="" 11 + class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900 z-{{sub 5 $i}}0 {{ $classes }}" 12 + /> 13 {{ end }} 14 15 {{ if gt (len $all) 5 }}
+17 -30
appview/pages/templates/labels/fragments/label.html
··· 2 {{ $d := .def }} 3 {{ $v := .val }} 4 {{ $withPrefix := .withPrefix }} 5 6 - {{ $lhs := printf "%s" $d.Name }} 7 - {{ $rhs := "" }} 8 - {{ $isDid := false }} 9 - {{ $resolvedVal := "" }} 10 11 - {{ if not $d.ValueType.IsNull }} 12 - {{ $isDid = $d.ValueType.IsDidFormat }} 13 - {{ if $isDid }} 14 - {{ $resolvedVal = resolve $v }} 15 - {{ $v = $resolvedVal }} 16 - {{ end }} 17 18 - {{ if not $withPrefix }} 19 - {{ $lhs = "" }} 20 - {{ else }} 21 - {{ $lhs = printf "%s/" $d.Name }} 22 {{ end }} 23 24 - {{ $rhs = printf "%s" $v }} 25 - {{ end }} 26 - 27 - {{ $chipClasses := "w-fit flex items-center gap-2 font-normal normal-case rounded py-1 px-2 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm text-inherit" }} 28 - 29 - {{ if $isDid }} 30 - <a href="/{{ $resolvedVal }}" class="{{ $chipClasses }} no-underline hover:underline"> 31 - {{ template "repo/fragments/colorBall" (dict "color" $d.GetColor) }} 32 - {{ printf "%s%s" $lhs $rhs }} 33 - </a> 34 - {{ else }} 35 - <span class="{{ $chipClasses }}"> 36 - {{ template "repo/fragments/colorBall" (dict "color" $d.GetColor) }} 37 - {{ printf "%s%s" $lhs $rhs }} 38 - </span> 39 - {{ end }} 40 {{ end }} 41 42
··· 2 {{ $d := .def }} 3 {{ $v := .val }} 4 {{ $withPrefix := .withPrefix }} 5 + <span class="w-fit flex items-center gap-2 font-normal normal-case rounded py-1 px-2 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm"> 6 + {{ template "repo/fragments/colorBall" (dict "color" $d.GetColor) }} 7 8 + {{ $lhs := printf "%s" $d.Name }} 9 + {{ $rhs := "" }} 10 11 + {{ if not $d.ValueType.IsNull }} 12 + {{ if $d.ValueType.IsDidFormat }} 13 + {{ $v = resolve $v }} 14 + {{ end }} 15 + 16 + {{ if not $withPrefix }} 17 + {{ $lhs = "" }} 18 + {{ else }} 19 + {{ $lhs = printf "%s/" $d.Name }} 20 + {{ end }} 21 22 + {{ $rhs = printf "%s" $v }} 23 {{ end }} 24 25 + {{ printf "%s%s" $lhs $rhs }} 26 + </span> 27 {{ end }} 28 29
+11 -49
appview/pages/templates/layouts/fragments/topbar.html
··· 45 {{ define "profileDropdown" }} 46 <details class="relative inline-block text-left nav-dropdown"> 47 <summary class="cursor-pointer list-none flex items-center gap-1"> 48 - {{ $user := .Active.Did }} 49 <img 50 src="{{ tinyAvatar $user }}" 51 alt="" ··· 53 /> 54 <span class="hidden md:inline">{{ $user | resolve | truncateAt30 }}</span> 55 </summary> 56 - <div class="absolute right-0 mt-4 p-4 rounded bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700 shadow-lg z-50" style="width: 14rem;"> 57 - {{ $active := .Active.Did }} 58 - 59 - <div class="pb-2 mb-2 border-b border-gray-200 dark:border-gray-700"> 60 - <div class="flex items-center gap-2"> 61 - <img src="{{ tinyAvatar $active }}" alt="" class="rounded-full h-8 w-8 flex-shrink-0 border border-gray-300 dark:border-gray-700" /> 62 - <div class="flex-1 overflow-hidden"> 63 - <p class="font-medium text-sm truncate">{{ $active | resolve }}</p> 64 - <p class="text-xs text-green-600 dark:text-green-400">active</p> 65 - </div> 66 - </div> 67 - </div> 68 - 69 - {{ $others := .Accounts | otherAccounts $active }} 70 - {{ if $others }} 71 - <div class="pb-2 mb-2 border-b border-gray-200 dark:border-gray-700"> 72 - <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">Switch Account</p> 73 - {{ range $others }} 74 - <button 75 - type="button" 76 - hx-post="/account/switch" 77 - hx-vals='{"did": "{{ .Did }}"}' 78 - hx-swap="none" 79 - class="flex items-center gap-2 w-full py-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-left" 80 - > 81 - <img src="{{ tinyAvatar .Did }}" alt="" class="rounded-full h-6 w-6 flex-shrink-0 border border-gray-300 dark:border-gray-700" /> 82 - <span class="text-sm truncate flex-1">{{ .Did | resolve }}</span> 83 - </button> 84 - {{ end }} 85 - </div> 86 - {{ end }} 87 - 88 - <a href="/login?mode=add_account" class="flex items-center gap-2 py-1 text-sm"> 89 - {{ i "plus" "w-4 h-4 flex-shrink-0" }} 90 - <span>Add another account</span> 91 </a> 92 - 93 - <div class="pt-2 mt-2 border-t border-gray-200 dark:border-gray-700 space-y-1"> 94 - <a href="/{{ $active }}" class="block py-1 text-sm">profile</a> 95 - <a href="/{{ $active }}?tab=repos" class="block py-1 text-sm">repositories</a> 96 - <a href="/{{ $active }}?tab=strings" class="block py-1 text-sm">strings</a> 97 - <a href="/settings" class="block py-1 text-sm">settings</a> 98 - <a href="#" 99 - hx-post="/logout" 100 - hx-swap="none" 101 - class="block py-1 text-sm text-red-400 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"> 102 - logout 103 - </a> 104 - </div> 105 </div> 106 </details> 107
··· 45 {{ define "profileDropdown" }} 46 <details class="relative inline-block text-left nav-dropdown"> 47 <summary class="cursor-pointer list-none flex items-center gap-1"> 48 + {{ $user := .Did }} 49 <img 50 src="{{ tinyAvatar $user }}" 51 alt="" ··· 53 /> 54 <span class="hidden md:inline">{{ $user | resolve | truncateAt30 }}</span> 55 </summary> 56 + <div class="absolute flex flex-col right-0 mt-4 p-4 rounded w-48 bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700"> 57 + <a href="/{{ $user }}">profile</a> 58 + <a href="/{{ $user }}?tab=repos">repositories</a> 59 + <a href="/{{ $user }}?tab=strings">strings</a> 60 + <a href="/settings">settings</a> 61 + <a href="#" 62 + hx-post="/logout" 63 + hx-swap="none" 64 + class="text-red-400 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"> 65 + logout 66 </a> 67 </div> 68 </details> 69
+1 -1
appview/pages/templates/layouts/repobase.html
··· 1 {{ define "title" }}{{ .RepoInfo.FullName }}{{ end }} 2 3 {{ define "content" }} 4 - <section id="repo-header" class="mb-2 py-2 px-4 dark:text-white"> 5 <div class="text-lg flex flex-col sm:flex-row items-start gap-4 justify-between"> 6 <!-- left items --> 7 <div class="flex flex-col gap-2">
··· 1 {{ define "title" }}{{ .RepoInfo.FullName }}{{ end }} 2 3 {{ define "content" }} 4 + <section id="repo-header" class="mb-4 p-2 dark:text-white"> 5 <div class="text-lg flex flex-col sm:flex-row items-start gap-4 justify-between"> 6 <!-- left items --> 7 <div class="flex flex-col gap-2">
+18 -1
appview/pages/templates/repo/commit.html
··· 116 {{ block "content" . }}{{ end }} 117 {{ end }} 118 119 - {{ block "contentAfter" . }}{{ end }} 120 </div> 121 {{ end }} 122 ··· 130 {{ template "repo/fragments/diff" (list .Diff .DiffOpts) }} 131 {{end}} 132
··· 116 {{ block "content" . }}{{ end }} 117 {{ end }} 118 119 + {{ block "contentAfterLayout" . }} 120 + <div class="flex-grow grid grid-cols-1 md:grid-cols-12 gap-4"> 121 + <div class="flex flex-col gap-4 col-span-1 md:col-span-2"> 122 + {{ block "contentAfterLeft" . }} {{ end }} 123 + </div> 124 + <main class="col-span-1 md:col-span-10"> 125 + {{ block "contentAfter" . }}{{ end }} 126 + </main> 127 + </div> 128 + {{ end }} 129 </div> 130 {{ end }} 131 ··· 139 {{ template "repo/fragments/diff" (list .Diff .DiffOpts) }} 140 {{end}} 141 142 + {{ define "contentAfterLeft" }} 143 + <div class="flex flex-col gap-4 col-span-1 md:col-span-2"> 144 + {{ template "repo/fragments/diffOpts" .DiffOpts }} 145 + </div> 146 + <div class="sticky top-0 flex-grow max-h-screen overflow-y-auto"> 147 + {{ template "repo/fragments/diffChangedFiles" .Diff }} 148 + </div> 149 + {{end}}
+19 -1
appview/pages/templates/repo/compare/compare.html
··· 22 {{ block "content" . }}{{ end }} 23 {{ end }} 24 25 - {{ block "contentAfter" . }}{{ end }} 26 </div> 27 {{ end }} 28 ··· 35 {{ define "contentAfter" }} 36 {{ template "repo/fragments/diff" (list .Diff .DiffOpts) }} 37 {{end}}
··· 22 {{ block "content" . }}{{ end }} 23 {{ end }} 24 25 + {{ block "contentAfterLayout" . }} 26 + <div class="flex-grow grid grid-cols-1 md:grid-cols-12 gap-4"> 27 + <div class="flex flex-col gap-4 col-span-1 md:col-span-2"> 28 + {{ block "contentAfterLeft" . }} {{ end }} 29 + </div> 30 + <main class="col-span-1 md:col-span-10"> 31 + {{ block "contentAfter" . }}{{ end }} 32 + </main> 33 + </div> 34 + {{ end }} 35 </div> 36 {{ end }} 37 ··· 44 {{ define "contentAfter" }} 45 {{ template "repo/fragments/diff" (list .Diff .DiffOpts) }} 46 {{end}} 47 + 48 + {{ define "contentAfterLeft" }} 49 + <div class="flex flex-col gap-4 col-span-1 md:col-span-2"> 50 + {{ template "repo/fragments/diffOpts" .DiffOpts }} 51 + </div> 52 + <div class="sticky top-0 flex-grow max-h-screen overflow-y-auto"> 53 + {{ template "repo/fragments/diffChangedFiles" .Diff }} 54 + </div> 55 + {{end}}
+96 -81
appview/pages/templates/repo/fragments/cloneDropdown.html
··· 1 {{ define "repo/fragments/cloneDropdown" }} 2 - {{ $knot := .RepoInfo.Knot }} 3 - {{ if eq $knot "knot1.tangled.sh" }} 4 - {{ $knot = "tangled.org" }} 5 - {{ end }} 6 7 - <button 8 - popovertarget="clone-dropdown" 9 - popovertargetaction="toggle" 10 - class="btn-create cursor-pointer list-none flex items-center gap-2 px-4"> 11 - {{ i "download" "w-4 h-4" }} 12 - <span class="hidden md:inline">code</span> 13 - </button> 14 - <div 15 - popover 16 - id="clone-dropdown" 17 - class=" 18 - bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 19 - dark:text-white backdrop:bg-gray-400/50 dark:backdrop:bg-gray-800/50 20 - w-96 p-4 rounded drop-shadow overflow-visible"> 21 - <h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-5">Clone this repository</h3> 22 23 - <!-- HTTPS Clone --> 24 - <div class="mb-3"> 25 - <label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">HTTPS</label> 26 - <div class="flex items-center border border-gray-300 dark:border-gray-600 rounded"> 27 - <code 28 - class="flex-1 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-l select-all cursor-pointer whitespace-nowrap overflow-x-auto" 29 - onclick="window.getSelection().selectAllChildren(this)" 30 - data-url="https://tangled.org/{{ resolve .RepoInfo.OwnerDid }}/{{ .RepoInfo.Name }}" 31 - >https://tangled.org/{{ resolve .RepoInfo.OwnerDid }}/{{ .RepoInfo.Name }}</code> 32 - <button 33 - onclick="copyToClipboard(this, this.previousElementSibling.getAttribute('data-url'))" 34 - class="px-3 py-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 border-l border-gray-300 dark:border-gray-600" 35 - title="Copy to clipboard" 36 - > 37 - {{ i "copy" "w-4 h-4" }} 38 - </button> 39 - </div> 40 - </div> 41 42 - <!-- SSH Clone --> 43 - <div class="mb-3"> 44 - {{ $repoOwnerHandle := resolve .RepoInfo.OwnerDid }} 45 - <label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">SSH</label> 46 - <div class="flex items-center border border-gray-300 dark:border-gray-600 rounded"> 47 - <code 48 - class="flex-1 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-l select-all cursor-pointer whitespace-nowrap overflow-x-auto" 49 - onclick="window.getSelection().selectAllChildren(this)" 50 - data-url="git@{{ $knot | stripPort }}:{{ $repoOwnerHandle }}/{{ .RepoInfo.Name }}" 51 - >git@{{ $knot | stripPort }}:{{ $repoOwnerHandle }}/{{ .RepoInfo.Name }}</code> 52 - <button 53 - onclick="copyToClipboard(this, this.previousElementSibling.getAttribute('data-url'))" 54 - class="px-3 py-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 border-l border-gray-300 dark:border-gray-600" 55 - title="Copy to clipboard" 56 - > 57 - {{ i "copy" "w-4 h-4" }} 58 - </button> 59 - </div> 60 - </div> 61 62 - <!-- Note for self-hosted --> 63 - <p class="text-xs text-gray-500 dark:text-gray-400"> 64 - For self-hosted knots, clone URLs may differ based on your setup. 65 - </p> 66 67 - <!-- Download Archive --> 68 - <div class="pt-2 mt-2 border-t border-gray-200 dark:border-gray-700"> 69 - <a 70 - href="/{{ .RepoInfo.FullName }}/archive/{{ .Ref | urlquery }}" 71 - class="flex items-center gap-2 px-3 py-2 text-sm" 72 - > 73 - {{ i "download" "w-4 h-4" }} 74 - Download tar.gz 75 - </a> 76 - </div> 77 - </div> 78 79 - <script> 80 - function copyToClipboard(button, text) { 81 - navigator.clipboard.writeText(text).then(() => { 82 - const originalContent = button.innerHTML; 83 - button.innerHTML = `{{ i "check" "w-4 h-4" }}`; 84 - setTimeout(() => { 85 - button.innerHTML = originalContent; 86 - }, 2000); 87 }); 88 - } 89 - </script> 90 {{ end }}
··· 1 {{ define "repo/fragments/cloneDropdown" }} 2 + {{ $knot := .RepoInfo.Knot }} 3 + {{ if eq $knot "knot1.tangled.sh" }} 4 + {{ $knot = "tangled.org" }} 5 + {{ end }} 6 + 7 + <details id="clone-dropdown" class="relative inline-block text-left group"> 8 + <summary class="btn-create cursor-pointer list-none flex items-center gap-2"> 9 + {{ i "download" "w-4 h-4" }} 10 + <span class="hidden md:inline">code</span> 11 + <span class="group-open:hidden"> 12 + {{ i "chevron-down" "w-4 h-4" }} 13 + </span> 14 + <span class="hidden group-open:flex"> 15 + {{ i "chevron-up" "w-4 h-4" }} 16 + </span> 17 + </summary> 18 + 19 + <div class="absolute right-0 mt-2 w-96 bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 drop-shadow-sm dark:text-white z-[9999]"> 20 + <div class="p-4"> 21 + <div class="mb-3"> 22 + <h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">Clone this repository</h3> 23 + </div> 24 + 25 + <!-- HTTPS Clone --> 26 + <div class="mb-3"> 27 + <label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">HTTPS</label> 28 + <div class="flex items-center border border-gray-300 dark:border-gray-600 rounded"> 29 + <code 30 + class="flex-1 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-l select-all cursor-pointer whitespace-nowrap overflow-x-auto" 31 + onclick="window.getSelection().selectAllChildren(this)" 32 + data-url="https://tangled.org/{{ resolve .RepoInfo.OwnerDid }}/{{ .RepoInfo.Name }}" 33 + >https://tangled.org/{{ resolve .RepoInfo.OwnerDid }}/{{ .RepoInfo.Name }}</code> 34 + <button 35 + onclick="copyToClipboard(this, this.previousElementSibling.getAttribute('data-url'))" 36 + class="px-3 py-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 border-l border-gray-300 dark:border-gray-600" 37 + title="Copy to clipboard" 38 + > 39 + {{ i "copy" "w-4 h-4" }} 40 + </button> 41 + </div> 42 + </div> 43 44 + <!-- SSH Clone --> 45 + <div class="mb-3"> 46 + {{ $repoOwnerHandle := resolve .RepoInfo.OwnerDid }} 47 + <label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">SSH</label> 48 + <div class="flex items-center border border-gray-300 dark:border-gray-600 rounded"> 49 + <code 50 + class="flex-1 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-l select-all cursor-pointer whitespace-nowrap overflow-x-auto" 51 + onclick="window.getSelection().selectAllChildren(this)" 52 + data-url="git@{{ $knot | stripPort }}:{{ $repoOwnerHandle }}/{{ .RepoInfo.Name }}" 53 + >git@{{ $knot | stripPort }}:{{ $repoOwnerHandle }}/{{ .RepoInfo.Name }}</code> 54 + <button 55 + onclick="copyToClipboard(this, this.previousElementSibling.getAttribute('data-url'))" 56 + class="px-3 py-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 border-l border-gray-300 dark:border-gray-600" 57 + title="Copy to clipboard" 58 + > 59 + {{ i "copy" "w-4 h-4" }} 60 + </button> 61 + </div> 62 + </div> 63 64 + <!-- Note for self-hosted --> 65 + <p class="text-xs text-gray-500 dark:text-gray-400"> 66 + For self-hosted knots, clone URLs may differ based on your setup. 67 + </p> 68 69 + <!-- Download Archive --> 70 + <div class="pt-2 mt-2 border-t border-gray-200 dark:border-gray-700"> 71 + <a 72 + href="/{{ .RepoInfo.FullName }}/archive/{{ .Ref | urlquery }}" 73 + class="flex items-center gap-2 px-3 py-2 text-sm" 74 + > 75 + {{ i "download" "w-4 h-4" }} 76 + Download tar.gz 77 + </a> 78 + </div> 79 80 + </div> 81 + </div> 82 + </details> 83 84 + <script> 85 + function copyToClipboard(button, text) { 86 + navigator.clipboard.writeText(text).then(() => { 87 + const originalContent = button.innerHTML; 88 + button.innerHTML = `{{ i "check" "w-4 h-4" }}`; 89 + setTimeout(() => { 90 + button.innerHTML = originalContent; 91 + }, 2000); 92 + }); 93 + } 94 95 + // Close clone dropdown when clicking outside 96 + document.addEventListener('click', function(event) { 97 + const cloneDropdown = document.getElementById('clone-dropdown'); 98 + if (cloneDropdown && cloneDropdown.hasAttribute('open')) { 99 + if (!cloneDropdown.contains(event.target)) { 100 + cloneDropdown.removeAttribute('open'); 101 + } 102 + } 103 }); 104 + </script> 105 {{ end }}
+43 -200
appview/pages/templates/repo/fragments/diff.html
··· 1 {{ define "repo/fragments/diff" }} 2 - <style> 3 - #filesToggle:checked ~ div label[for="filesToggle"] .show-text { display: none; } 4 - #filesToggle:checked ~ div label[for="filesToggle"] .hide-text { display: inline; } 5 - #filesToggle:not(:checked) ~ div label[for="filesToggle"] .hide-text { display: none; } 6 - #filesToggle:checked ~ div div#files { width: fit-content; max-width: 15vw; margin-right: 1rem; } 7 - #filesToggle:not(:checked) ~ div div#files { width: 0; display: none; margin-right: 0; } 8 - </style> 9 - 10 - {{ template "diffTopbar" . }} 11 - {{ block "diffLayout" . }} {{ end }} 12 - {{ end }} 13 - 14 - {{ define "diffTopbar" }} 15 {{ $diff := index . 0 }} 16 {{ $opts := index . 1 }} 17 - {{ $root := "" }} 18 - {{ if gt (len .) 2 }} 19 - {{ $root = index . 2 }} 20 - {{ end }} 21 - 22 - {{ block "filesCheckbox" $ }} {{ end }} 23 - {{ block "subsCheckbox" $ }} {{ end }} 24 - 25 - <!-- top bar --> 26 - <div class="sticky top-0 z-30 flex items-center gap-2 col-span-full h-12 p-2 {{ if $root }}mt-4{{ end }}"> 27 - <!-- left panel toggle --> 28 - {{ template "filesToggle" . }} 29 - 30 - <!-- stats --> 31 - {{ $stat := $diff.Stats }} 32 - {{ $count := len $diff.ChangedFiles }} 33 - {{ template "repo/fragments/diffStatPill" $stat }} 34 - <span class="text-xs text-gray-600 dark:text-gray-400">{{ $count }} changed file{{ if ne $count 1 }}s{{ end }}</span> 35 - 36 - {{ if $root }} 37 - {{ if $root.IsInterdiff }} 38 - <!-- interdiff indicator --> 39 - <div class="flex items-center gap-2 before:content-['|'] before:text-gray-300 dark:before:text-gray-600 before:mr-2"> 40 - <span class="text-xs text-gray-600 dark:text-gray-400 uppercase tracking-wide">Interdiff</span> 41 - <a 42 - href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $root.Pull.PullId }}/round/{{ sub $root.ActiveRound 1 }}" 43 - class="px-2 py-0.5 bg-white dark:bg-gray-700 rounded font-mono text-xs hover:bg-gray-50 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600" 44 - > 45 - #{{ sub $root.ActiveRound 1 }} 46 - </a> 47 - <span class="text-gray-400 text-xs">โ†’</span> 48 - <a 49 - href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $root.Pull.PullId }}/round/{{ $root.ActiveRound }}" 50 - class="px-2 py-0.5 bg-white dark:bg-gray-700 rounded font-mono text-xs hover:bg-gray-50 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600" 51 - > 52 - #{{ $root.ActiveRound }} 53 - </a> 54 - </div> 55 - {{ else if ne $root.ActiveRound nil }} 56 - <!-- diff round indicator --> 57 - <div class="flex items-center gap-2 before:content-['|'] before:text-gray-300 dark:before:text-gray-600 before:mr-2"> 58 - <span class="text-xs text-gray-600 dark:text-gray-400 uppercase tracking-wide">Diff</span> 59 - <span class="px-2 py-0.5 bg-white dark:bg-gray-700 rounded font-mono text-xs border border-gray-300 dark:border-gray-600"> 60 - <span class="hidden md:inline">round </span>#{{ $root.ActiveRound }} 61 - </span> 62 - </div> 63 - {{ end }} 64 - {{ end }} 65 66 - <!-- spacer --> 67 - <div class="flex-grow"></div> 68 - 69 - <!-- collapse diffs --> 70 - {{ template "collapseToggle" }} 71 - 72 - <!-- diff options --> 73 - <div class="hidden md:block"> 74 - {{ template "repo/fragments/diffOpts" $opts }} 75 - </div> 76 77 - <!-- right panel toggle --> 78 - {{ block "subsToggle" $ }} {{ end }} 79 - </div> 80 - 81 - {{ end }} 82 - 83 - {{ define "diffLayout" }} 84 - {{ $diff := index . 0 }} 85 - {{ $opts := index . 1 }} 86 - 87 - <div class="flex col-span-full flex-grow"> 88 - <!-- left panel --> 89 - <div id="files" class="w-0 hidden md:block overflow-hidden sticky top-12 max-h-screen overflow-y-auto pb-12"> 90 - <section class="overflow-x-auto text-sm px-6 py-2 border border-gray-200 dark:border-gray-700 w-full mx-auto min-h-full rounded bg-white dark:bg-gray-800 drop-shadow-sm"> 91 - {{ template "repo/fragments/fileTree" $diff.FileTree }} 92 - </section> 93 - </div> 94 - 95 - <!-- main content --> 96 - <div class="flex-1 min-w-0 sticky top-12 pb-12"> 97 - {{ template "diffFiles" (list $diff $opts) }} 98 - </div> 99 - 100 - </div> 101 - {{ end }} 102 - 103 - {{ define "diffFiles" }} 104 - {{ $diff := index . 0 }} 105 - {{ $opts := index . 1 }} 106 - {{ $files := $diff.ChangedFiles }} 107 - {{ $isSplit := $opts.Split }} 108 <div class="flex flex-col gap-4"> 109 - {{ if eq (len $files) 0 }} 110 <div class="text-center text-gray-500 dark:text-gray-400 py-8"> 111 <p>No differences found between the selected revisions.</p> 112 </div> 113 {{ else }} 114 - {{ range $idx, $file := $files }} 115 - {{ template "diffFile" (list $idx $file $isSplit) }} 116 - {{ end }} 117 - {{ end }} 118 - </div> 119 - {{ end }} 120 121 - {{ define "diffFile" }} 122 - {{ $idx := index . 0 }} 123 - {{ $file := index . 1 }} 124 - {{ $isSplit := index . 2 }} 125 - {{ with $file }} 126 - <details open id="file-{{ .Id }}" class="group border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm" tabindex="{{ add $idx 1 }}"> 127 - <summary class="list-none cursor-pointer sticky top-12 group-open:border-b border-gray-200 dark:border-gray-700"> 128 - <div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between"> 129 - <div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto"> 130 - <span class="group-open:hidden inline">{{ i "chevron-right" "w-4 h-4" }}</span> 131 - <span class="hidden group-open:inline">{{ i "chevron-down" "w-4 h-4" }}</span> 132 - {{ template "repo/fragments/diffStatPill" .Stats }} 133 134 - <div class="flex gap-2 items-center overflow-x-auto"> 135 - {{ $n := .Names }} 136 - {{ if and $n.New $n.Old (ne $n.New $n.Old)}} 137 - {{ $n.Old }} {{ i "arrow-right" "w-4 h-4" }} {{ $n.New }} 138 - {{ else if $n.New }} 139 - {{ $n.New }} 140 {{ else }} 141 - {{ $n.Old }} 142 {{ end }} 143 - </div> 144 </div> 145 - </div> 146 - </summary> 147 - 148 - <div class="transition-all duration-700 ease-in-out"> 149 - {{ $reason := .CanRender }} 150 - {{ if $reason }} 151 - <p class="text-center text-gray-400 dark:text-gray-500 p-4">{{ $reason }}</p> 152 - {{ else }} 153 - {{ if $isSplit }} 154 - {{- template "repo/fragments/splitDiff" .Split -}} 155 - {{ else }} 156 - {{- template "repo/fragments/unifiedDiff" . -}} 157 - {{ end }} 158 - {{- end -}} 159 - </div> 160 - </details> 161 - {{ end }} 162 - {{ end }} 163 - 164 - {{ define "filesCheckbox" }} 165 - <input type="checkbox" id="filesToggle" class="peer/files hidden" checked/> 166 - {{ end }} 167 - 168 - {{ define "filesToggle" }} 169 - <label title="Toggle filetree panel" for="filesToggle" class="hidden md:inline-flex items-center justify-center rounded cursor-pointer text-normal font-normal normalcase"> 170 - <span class="show-text">{{ i "panel-left-open" "size-4" }}</span> 171 - <span class="hide-text">{{ i "panel-left-close" "size-4" }}</span> 172 - </label> 173 - {{ end }} 174 - 175 - {{ define "collapseToggle" }} 176 - <label 177 - title="Expand/Collapse diffs" 178 - for="collapseToggle" 179 - class="btn font-normal normal-case p-2" 180 - > 181 - <input type="checkbox" id="collapseToggle" class="peer/collapse hidden" checked/> 182 - <span class="peer-checked/collapse:hidden inline-flex items-center gap-2"> 183 - {{ i "fold-vertical" "w-4 h-4" }} 184 - <span class="hidden md:inline">expand all</span> 185 - </span> 186 - <span class="peer-checked/collapse:inline-flex hidden flex items-center gap-2"> 187 - {{ i "unfold-vertical" "w-4 h-4" }} 188 - <span class="hidden md:inline">collapse all</span> 189 - </span> 190 - </label> 191 - <script> 192 - document.addEventListener('DOMContentLoaded', function() { 193 - const checkbox = document.getElementById('collapseToggle'); 194 - const details = document.querySelectorAll('details[id^="file-"]'); 195 - 196 - checkbox.addEventListener('change', function() { 197 - details.forEach(detail => { 198 - detail.open = checkbox.checked; 199 - }); 200 - }); 201 - 202 - details.forEach(detail => { 203 - detail.addEventListener('toggle', function() { 204 - const allOpen = Array.from(details).every(d => d.open); 205 - const allClosed = Array.from(details).every(d => !d.open); 206 - 207 - if (allOpen) { 208 - checkbox.checked = true; 209 - } else if (allClosed) { 210 - checkbox.checked = false; 211 - } 212 - }); 213 - }); 214 - }); 215 - </script> 216 {{ end }}
··· 1 {{ define "repo/fragments/diff" }} 2 {{ $diff := index . 0 }} 3 {{ $opts := index . 1 }} 4 5 + {{ $commit := $diff.Commit }} 6 + {{ $diff := $diff.Diff }} 7 + {{ $isSplit := $opts.Split }} 8 + {{ $this := $commit.This }} 9 + {{ $parent := $commit.Parent }} 10 + {{ $last := sub (len $diff) 1 }} 11 12 <div class="flex flex-col gap-4"> 13 + {{ if eq (len $diff) 0 }} 14 <div class="text-center text-gray-500 dark:text-gray-400 py-8"> 15 <p>No differences found between the selected revisions.</p> 16 </div> 17 {{ else }} 18 + {{ range $idx, $hunk := $diff }} 19 + {{ with $hunk }} 20 + <details open id="file-{{ .Id }}" class="group border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm" tabindex="{{ add $idx 1 }}"> 21 + <summary class="list-none cursor-pointer sticky top-0"> 22 + <div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between"> 23 + <div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto"> 24 + <span class="group-open:hidden inline">{{ i "chevron-right" "w-4 h-4" }}</span> 25 + <span class="hidden group-open:inline">{{ i "chevron-down" "w-4 h-4" }}</span> 26 + {{ template "repo/fragments/diffStatPill" .Stats }} 27 28 + <div class="flex gap-2 items-center overflow-x-auto"> 29 + {{ if .IsDelete }} 30 + {{ .Name.Old }} 31 + {{ else if (or .IsCopy .IsRename) }} 32 + {{ .Name.Old }} {{ i "arrow-right" "w-4 h-4" }} {{ .Name.New }} 33 + {{ else }} 34 + {{ .Name.New }} 35 + {{ end }} 36 + </div> 37 + </div> 38 + </div> 39 + </summary> 40 41 + <div class="transition-all duration-700 ease-in-out"> 42 + {{ if .IsBinary }} 43 + <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 44 + This is a binary file and will not be displayed. 45 + </p> 46 + {{ else }} 47 + {{ if $isSplit }} 48 + {{- template "repo/fragments/splitDiff" .Split -}} 49 {{ else }} 50 + {{- template "repo/fragments/unifiedDiff" . -}} 51 {{ end }} 52 + {{- end -}} 53 </div> 54 + </details> 55 + {{ end }} 56 + {{ end }} 57 + {{ end }} 58 + </div> 59 {{ end }}
+13
appview/pages/templates/repo/fragments/diffChangedFiles.html
···
··· 1 + {{ define "repo/fragments/diffChangedFiles" }} 2 + {{ $stat := .Stat }} 3 + {{ $fileTree := fileTree .ChangedFiles }} 4 + <section class="overflow-x-auto text-sm px-6 py-2 border border-gray-200 dark:border-gray-700 w-full mx-auto min-h-full rounded bg-white dark:bg-gray-800 drop-shadow-sm"> 5 + <div class="diff-stat"> 6 + <div class="flex gap-2 items-center"> 7 + <strong class="text-sm uppercase dark:text-gray-200">Changed files</strong> 8 + {{ template "repo/fragments/diffStatPill" $stat }} 9 + </div> 10 + {{ template "repo/fragments/fileTree" $fileTree }} 11 + </div> 12 + </section> 13 + {{ end }}
+25 -22
appview/pages/templates/repo/fragments/diffOpts.html
··· 1 {{ define "repo/fragments/diffOpts" }} 2 - {{ $active := "unified" }} 3 - {{ if .Split }} 4 - {{ $active = "split" }} 5 - {{ end }} 6 7 - {{ $unified := 8 - (dict 9 - "Key" "unified" 10 - "Value" "unified" 11 - "Icon" "square-split-vertical" 12 - "Meta" "") }} 13 - {{ $split := 14 - (dict 15 - "Key" "split" 16 - "Value" "split" 17 - "Icon" "square-split-horizontal" 18 - "Meta" "") }} 19 - {{ $values := list $unified $split }} 20 21 - {{ template "fragments/tabSelector" 22 - (dict 23 - "Name" "diff" 24 - "Values" $values 25 - "Active" $active) }} 26 {{ end }} 27
··· 1 {{ define "repo/fragments/diffOpts" }} 2 + <section class="flex flex-col gap-2 overflow-x-auto text-sm px-6 py-2 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm"> 3 + <strong class="text-sm uppercase dark:text-gray-200">options</strong> 4 + {{ $active := "unified" }} 5 + {{ if .Split }} 6 + {{ $active = "split" }} 7 + {{ end }} 8 9 + {{ $unified := 10 + (dict 11 + "Key" "unified" 12 + "Value" "unified" 13 + "Icon" "square-split-vertical" 14 + "Meta" "") }} 15 + {{ $split := 16 + (dict 17 + "Key" "split" 18 + "Value" "split" 19 + "Icon" "square-split-horizontal" 20 + "Meta" "") }} 21 + {{ $values := list $unified $split }} 22 23 + {{ template "fragments/tabSelector" 24 + (dict 25 + "Name" "diff" 26 + "Values" $values 27 + "Active" $active) }} 28 + </section> 29 {{ end }} 30
+67
appview/pages/templates/repo/fragments/interdiff.html
···
··· 1 + {{ define "repo/fragments/interdiff" }} 2 + {{ $repo := index . 0 }} 3 + {{ $x := index . 1 }} 4 + {{ $opts := index . 2 }} 5 + {{ $fileTree := fileTree $x.AffectedFiles }} 6 + {{ $diff := $x.Files }} 7 + {{ $last := sub (len $diff) 1 }} 8 + {{ $isSplit := $opts.Split }} 9 + 10 + <div class="flex flex-col gap-4"> 11 + {{ range $idx, $hunk := $diff }} 12 + {{ with $hunk }} 13 + <details {{ if not (.Status.IsOnlyInOne) }}open{{end}} id="file-{{ .Name }}" class="border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm"> 14 + <summary class="list-none cursor-pointer sticky top-0"> 15 + <div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between"> 16 + <div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto"> 17 + <div class="flex gap-1 items-center" style="direction: ltr;"> 18 + {{ $markerstyle := "diff-type p-1 mr-1 font-mono text-sm rounded select-none" }} 19 + {{ if .Status.IsOk }} 20 + <span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">CHANGED</span> 21 + {{ else if .Status.IsUnchanged }} 22 + <span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">UNCHANGED</span> 23 + {{ else if .Status.IsOnlyInOne }} 24 + <span class="bg-red-100 text-red-700 dark:bg-red-800/50 dark:text-red-400 {{ $markerstyle }}">REVERTED</span> 25 + {{ else if .Status.IsOnlyInTwo }} 26 + <span class="bg-green-100 text-green-700 dark:bg-green-800/50 dark:text-green-400 {{ $markerstyle }}">NEW</span> 27 + {{ else if .Status.IsRebased }} 28 + <span class="bg-amber-100 text-amber-700 dark:bg-amber-800/50 dark:text-amber-400 {{ $markerstyle }}">REBASED</span> 29 + {{ else }} 30 + <span class="bg-red-100 text-red-700 dark:bg-red-800/50 dark:text-red-400 {{ $markerstyle }}">ERROR</span> 31 + {{ end }} 32 + </div> 33 + 34 + <div class="flex gap-2 items-center overflow-x-auto" style="direction: rtl;">{{ .Name }}</div> 35 + </div> 36 + 37 + </div> 38 + </summary> 39 + 40 + <div class="transition-all duration-700 ease-in-out"> 41 + {{ if .Status.IsUnchanged }} 42 + <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 43 + This file has not been changed. 44 + </p> 45 + {{ else if .Status.IsRebased }} 46 + <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 47 + This patch was likely rebased, as context lines do not match. 48 + </p> 49 + {{ else if .Status.IsError }} 50 + <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 51 + Failed to calculate interdiff for this file. 52 + </p> 53 + {{ else }} 54 + {{ if $isSplit }} 55 + {{- template "repo/fragments/splitDiff" .Split -}} 56 + {{ else }} 57 + {{- template "repo/fragments/unifiedDiff" . -}} 58 + {{ end }} 59 + {{- end -}} 60 + </div> 61 + 62 + </details> 63 + {{ end }} 64 + {{ end }} 65 + </div> 66 + {{ end }} 67 +
+11
appview/pages/templates/repo/fragments/interdiffFiles.html
···
··· 1 + {{ define "repo/fragments/interdiffFiles" }} 2 + {{ $fileTree := fileTree .AffectedFiles }} 3 + <section class="px-6 py-2 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm min-h-full text-sm"> 4 + <div class="diff-stat"> 5 + <div class="flex gap-2 items-center"> 6 + <strong class="text-sm uppercase dark:text-gray-200">files</strong> 7 + </div> 8 + {{ template "repo/fragments/fileTree" $fileTree }} 9 + </div> 10 + </section> 11 + {{ end }}
+1 -1
appview/pages/templates/repo/fragments/splitDiff.html
··· 3 {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800" -}} 4 {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 5 {{- $lineNrSepStyle := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 6 - {{- $containerStyle := "inline-flex w-full items-center target:bg-yellow-200 target:dark:bg-yellow-700 scroll-mt-48" -}} 7 {{- $emptyStyle := "bg-gray-200/30 dark:bg-gray-700/30" -}} 8 {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400" -}} 9 {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}}
··· 3 {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800" -}} 4 {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 5 {{- $lineNrSepStyle := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 6 + {{- $containerStyle := "inline-flex w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}} 7 {{- $emptyStyle := "bg-gray-200/30 dark:bg-gray-700/30" -}} 8 {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400" -}} 9 {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}}
+1 -1
appview/pages/templates/repo/fragments/unifiedDiff.html
··· 7 {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 8 {{- $lineNrSepStyle1 := "" -}} 9 {{- $lineNrSepStyle2 := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 10 - {{- $containerStyle := "inline-flex w-full items-center target:bg-yellow-200 target:dark:bg-yellow-700 scroll-mt-48" -}} 11 {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 " -}} 12 {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}} 13 {{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}}
··· 7 {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 8 {{- $lineNrSepStyle1 := "" -}} 9 {{- $lineNrSepStyle2 := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 10 + {{- $containerStyle := "inline-flex w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}} 11 {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 " -}} 12 {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}} 13 {{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}}
+9 -4
appview/pages/templates/repo/index.html
··· 14 {{ end }} 15 <div class="flex items-center justify-between pb-5"> 16 {{ block "branchSelector" . }}{{ end }} 17 - <div class="flex items-center gap-3"> 18 - <a href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}" class="inline-flex md:hidden items-center text-sm gap-1 font-bold"> 19 {{ i "git-commit-horizontal" "w-4" "h-4" }} {{ .TotalCommits }} 20 </a> 21 - <a href="/{{ .RepoInfo.FullName }}/branches" class="inline-flex md:hidden items-center text-sm gap-1 font-bold"> 22 {{ i "git-branch" "w-4" "h-4" }} {{ len .Branches }} 23 </a> 24 - <a href="/{{ .RepoInfo.FullName }}/tags" class="inline-flex md:hidden items-center text-sm gap-1 font-bold"> 25 {{ i "tags" "w-4" "h-4" }} {{ len .Tags }} 26 </a> 27 {{ template "repo/fragments/cloneDropdown" . }} ··· 109 {{ i "git-compare" "w-4 h-4" }} 110 </a> 111 </div> 112 </div> 113 </div> 114 {{ end }}
··· 14 {{ end }} 15 <div class="flex items-center justify-between pb-5"> 16 {{ block "branchSelector" . }}{{ end }} 17 + <div class="flex md:hidden items-center gap-3"> 18 + <a href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}" class="inline-flex items-center text-sm gap-1 font-bold"> 19 {{ i "git-commit-horizontal" "w-4" "h-4" }} {{ .TotalCommits }} 20 </a> 21 + <a href="/{{ .RepoInfo.FullName }}/branches" class="inline-flex items-center text-sm gap-1 font-bold"> 22 {{ i "git-branch" "w-4" "h-4" }} {{ len .Branches }} 23 </a> 24 + <a href="/{{ .RepoInfo.FullName }}/tags" class="inline-flex items-center text-sm gap-1 font-bold"> 25 {{ i "tags" "w-4" "h-4" }} {{ len .Tags }} 26 </a> 27 {{ template "repo/fragments/cloneDropdown" . }} ··· 109 {{ i "git-compare" "w-4 h-4" }} 110 </a> 111 </div> 112 + </div> 113 + 114 + <!-- Clone dropdown in top right --> 115 + <div class="hidden md:flex items-center "> 116 + {{ template "repo/fragments/cloneDropdown" . }} 117 </div> 118 </div> 119 {{ end }}
+22 -35
appview/pages/templates/repo/issues/fragments/commentList.html
··· 1 {{ define "repo/issues/fragments/commentList" }} 2 - <div class="flex flex-col gap-4"> 3 {{ range $item := .CommentList }} 4 {{ template "commentListing" (list $ .) }} 5 {{ end }} ··· 19 <div class="rounded border border-gray-200 dark:border-gray-700 w-full overflow-hidden shadow-sm bg-gray-50 dark:bg-gray-800/50"> 20 {{ template "topLevelComment" $params }} 21 22 - <div class="relative ml-10 border-l-2 border-gray-200 dark:border-gray-700"> 23 {{ range $index, $reply := $comment.Replies }} 24 - <div class="-ml-4"> 25 - {{ 26 - template "replyComment" 27 - (dict 28 - "RepoInfo" $root.RepoInfo 29 - "LoggedInUser" $root.LoggedInUser 30 - "Issue" $root.Issue 31 - "Comment" $reply) 32 - }} 33 </div> 34 {{ end }} 35 </div> ··· 39 {{ end }} 40 41 {{ define "topLevelComment" }} 42 - <div class="rounded px-6 py-4 bg-white dark:bg-gray-800 flex gap-2 "> 43 - <div class="flex-shrink-0"> 44 - <img 45 - src="{{ tinyAvatar .Comment.Did }}" 46 - alt="" 47 - class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900" 48 - /> 49 - </div> 50 - <div class="flex-1 min-w-0"> 51 - {{ template "repo/issues/fragments/issueCommentHeader" . }} 52 - {{ template "repo/issues/fragments/issueCommentBody" . }} 53 - </div> 54 </div> 55 {{ end }} 56 57 {{ define "replyComment" }} 58 - <div class="py-4 pr-4 w-full mx-auto overflow-hidden flex gap-2 "> 59 - <div class="flex-shrink-0"> 60 - <img 61 - src="{{ tinyAvatar .Comment.Did }}" 62 - alt="" 63 - class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900" 64 - /> 65 - </div> 66 - <div class="flex-1 min-w-0"> 67 - {{ template "repo/issues/fragments/issueCommentHeader" . }} 68 - {{ template "repo/issues/fragments/issueCommentBody" . }} 69 - </div> 70 </div> 71 {{ end }}
··· 1 {{ define "repo/issues/fragments/commentList" }} 2 + <div class="flex flex-col gap-8"> 3 {{ range $item := .CommentList }} 4 {{ template "commentListing" (list $ .) }} 5 {{ end }} ··· 19 <div class="rounded border border-gray-200 dark:border-gray-700 w-full overflow-hidden shadow-sm bg-gray-50 dark:bg-gray-800/50"> 20 {{ template "topLevelComment" $params }} 21 22 + <div class="relative ml-4 border-l-2 border-gray-200 dark:border-gray-700"> 23 {{ range $index, $reply := $comment.Replies }} 24 + <div class="relative "> 25 + <!-- Horizontal connector --> 26 + <div class="absolute left-0 top-6 w-4 h-1 bg-gray-200 dark:bg-gray-700"></div> 27 + 28 + <div class="pl-2"> 29 + {{ 30 + template "replyComment" 31 + (dict 32 + "RepoInfo" $root.RepoInfo 33 + "LoggedInUser" $root.LoggedInUser 34 + "Issue" $root.Issue 35 + "Comment" $reply) 36 + }} 37 + </div> 38 </div> 39 {{ end }} 40 </div> ··· 44 {{ end }} 45 46 {{ define "topLevelComment" }} 47 + <div class="rounded px-6 py-4 bg-white dark:bg-gray-800"> 48 + {{ template "repo/issues/fragments/issueCommentHeader" . }} 49 + {{ template "repo/issues/fragments/issueCommentBody" . }} 50 </div> 51 {{ end }} 52 53 {{ define "replyComment" }} 54 + <div class="p-4 w-full mx-auto overflow-hidden"> 55 + {{ template "repo/issues/fragments/issueCommentHeader" . }} 56 + {{ template "repo/issues/fragments/issueCommentBody" . }} 57 </div> 58 {{ end }}
+63
appview/pages/templates/repo/issues/fragments/globalIssueListing.html
···
··· 1 + {{ define "repo/issues/fragments/globalIssueListing" }} 2 + <div class="flex flex-col gap-2"> 3 + {{ range .Issues }} 4 + <div class="rounded drop-shadow-sm bg-white px-6 py-4 dark:bg-gray-800 dark:border-gray-700"> 5 + <div class="pb-2 mb-3"> 6 + <div class="flex items-center gap-3 mb-2"> 7 + <a 8 + href="/{{ resolve .Repo.Did }}/{{ .Repo.Name }}" 9 + class="text-blue-600 dark:text-blue-400 font-medium hover:underline text-sm" 10 + > 11 + {{ resolve .Repo.Did }}/{{ .Repo.Name }} 12 + </a> 13 + </div> 14 + <a 15 + href="/{{ resolve .Repo.Did }}/{{ .Repo.Name }}/issues/{{ .IssueId }}" 16 + class="no-underline hover:underline" 17 + > 18 + {{ .Title | description }} 19 + <span class="text-gray-500">#{{ .IssueId }}</span> 20 + </a> 21 + </div> 22 + <div class="text-sm text-gray-500 dark:text-gray-400 flex flex-wrap items-center gap-1"> 23 + {{ $bgColor := "bg-gray-800 dark:bg-gray-700" }} 24 + {{ $icon := "ban" }} 25 + {{ $state := "closed" }} 26 + {{ if .Open }} 27 + {{ $bgColor = "bg-green-600 dark:bg-green-700" }} 28 + {{ $icon = "circle-dot" }} 29 + {{ $state = "open" }} 30 + {{ end }} 31 + 32 + <span class="inline-flex items-center rounded px-2 py-[5px] {{ $bgColor }} text-sm"> 33 + {{ i $icon "w-3 h-3 mr-1.5 text-white dark:text-white" }} 34 + <span class="text-white dark:text-white">{{ $state }}</span> 35 + </span> 36 + 37 + <span class="ml-1"> 38 + {{ template "user/fragments/picHandleLink" .Did }} 39 + </span> 40 + 41 + <span class="before:content-['ยท']"> 42 + {{ template "repo/fragments/time" .Created }} 43 + </span> 44 + 45 + <span class="before:content-['ยท']"> 46 + {{ $s := "s" }} 47 + {{ if eq (len .Comments) 1 }} 48 + {{ $s = "" }} 49 + {{ end }} 50 + <a href="/{{ resolve .Repo.Did }}/{{ .Repo.Name }}/issues/{{ .IssueId }}" class="text-gray-500 dark:text-gray-400">{{ len .Comments }} comment{{$s}}</a> 51 + </span> 52 + 53 + {{ $state := .Labels }} 54 + {{ range $k, $d := $.LabelDefs }} 55 + {{ range $v, $s := $state.GetValSet $d.AtUri.String }} 56 + {{ template "labels/fragments/label" (dict "def" $d "val" $v "withPrefix" true) }} 57 + {{ end }} 58 + {{ end }} 59 + </div> 60 + </div> 61 + {{ end }} 62 + </div> 63 + {{ end }}
+1 -2
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
··· 1 {{ define "repo/issues/fragments/issueCommentHeader" }} 2 <div class="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400 "> 3 - {{ resolve .Comment.Did }} 4 {{ template "hats" $ }} 5 - <span class="before:content-['ยท']"></span> 6 {{ template "timestamp" . }} 7 {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did) }} 8 {{ if and $isCommentOwner (not .Comment.Deleted) }}
··· 1 {{ define "repo/issues/fragments/issueCommentHeader" }} 2 <div class="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400 "> 3 + {{ template "user/fragments/picHandleLink" .Comment.Did }} 4 {{ template "hats" $ }} 5 {{ template "timestamp" . }} 6 {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did) }} 7 {{ if and $isCommentOwner (not .Comment.Deleted) }}
+2 -2
appview/pages/templates/repo/issues/fragments/issueListing.html
··· 21 {{ $state = "open" }} 22 {{ end }} 23 24 - <span class="inline-flex items-center rounded px-2 py-[5px] {{ $bgColor }}"> 25 {{ i $icon "w-3 h-3 mr-1.5 text-white dark:text-white" }} 26 - <span class="text-white dark:text-white text-sm">{{ $state }}</span> 27 </span> 28 29 <span class="ml-1">
··· 21 {{ $state = "open" }} 22 {{ end }} 23 24 + <span class="inline-flex items-center rounded px-2 py-[5px] {{ $bgColor }} text-sm"> 25 {{ i $icon "w-3 h-3 mr-1.5 text-white dark:text-white" }} 26 + <span class="text-white dark:text-white">{{ $state }}</span> 27 </span> 28 29 <span class="ml-1">
+1 -1
appview/pages/templates/repo/issues/fragments/putIssue.html
··· 18 <textarea 19 name="body" 20 id="body" 21 - rows="15" 22 class="w-full resize-y" 23 placeholder="Describe your issue. Markdown is supported." 24 >{{ if .Issue }}{{ .Issue.Body }}{{ end }}</textarea>
··· 18 <textarea 19 name="body" 20 id="body" 21 + rows="6" 22 class="w-full resize-y" 23 placeholder="Describe your issue. Markdown is supported." 24 >{{ if .Issue }}{{ .Issue.Body }}{{ end }}</textarea>
+3 -3
appview/pages/templates/repo/issues/fragments/replyIssueCommentPlaceholder.html
··· 1 {{ define "repo/issues/fragments/replyIssueCommentPlaceholder" }} 2 - <div class="py-2 px-6 border-t flex gap-2 items-center border-gray-300 dark:border-gray-700"> 3 {{ if .LoggedInUser }} 4 <img 5 src="{{ tinyAvatar .LoggedInUser.Did }}" 6 alt="" 7 - class="rounded-full size-8 mr-1 border-2 border-gray-300 dark:border-gray-700" 8 /> 9 {{ end }} 10 <input 11 - class="w-full p-0 border-none focus:outline-none" 12 placeholder="Leave a reply..." 13 hx-get="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/reply" 14 hx-trigger="focus"
··· 1 {{ define "repo/issues/fragments/replyIssueCommentPlaceholder" }} 2 + <div class="p-2 border-t flex gap-2 items-center border-gray-300 dark:border-gray-700"> 3 {{ if .LoggedInUser }} 4 <img 5 src="{{ tinyAvatar .LoggedInUser.Did }}" 6 alt="" 7 + class="rounded-full h-6 w-6 mr-1 border border-gray-300 dark:border-gray-700" 8 /> 9 {{ end }} 10 <input 11 + class="w-full py-2 border-none focus:outline-none" 12 placeholder="Leave a reply..." 13 hx-get="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/reply" 14 hx-trigger="focus"
+5 -5
appview/pages/templates/repo/issues/issue.html
··· 58 {{ $icon = "circle-dot" }} 59 {{ end }} 60 <div class="inline-flex items-center gap-2"> 61 - <span class="inline-flex items-center rounded px-2 py-[5px] {{ $bgColor }}"> 62 - {{ i $icon "w-3 h-3 mr-1.5 text-white dark:text-white" }} 63 - <span class="text-white dark:text-white text-sm">{{ .Issue.State }}</span> 64 - </span> 65 - 66 <span class="text-gray-500 dark:text-gray-400 text-sm flex flex-wrap items-center gap-1"> 67 opened by 68 {{ template "user/fragments/picHandleLink" .Issue.Did }}
··· 58 {{ $icon = "circle-dot" }} 59 {{ end }} 60 <div class="inline-flex items-center gap-2"> 61 + <div id="state" 62 + class="inline-flex items-center rounded px-3 py-1 {{ $bgColor }}"> 63 + {{ i $icon "w-4 h-4 mr-1.5 text-white" }} 64 + <span class="text-white">{{ .Issue.State }}</span> 65 + </div> 66 <span class="text-gray-500 dark:text-gray-400 text-sm flex flex-wrap items-center gap-1"> 67 opened by 68 {{ template "user/fragments/picHandleLink" .Issue.Did }}
+69 -60
appview/pages/templates/repo/pipelines/fragments/pipelineSymbol.html
··· 1 {{ define "repo/pipelines/fragments/pipelineSymbol" }} 2 - <div class="cursor-pointer flex gap-2 items-center"> 3 - {{ template "symbol" .Pipeline }} 4 - {{ if .ShortSummary }} 5 - {{ .Pipeline.ShortStatusSummary }} 6 {{ else }} 7 - {{ .Pipeline.LongStatusSummary }} 8 {{ end }} 9 </div> 10 {{ end }} 11 - 12 - {{ define "symbol" }} 13 - {{ $c := .Counts }} 14 - {{ $statuses := .Statuses }} 15 - {{ $total := len $statuses }} 16 - {{ $success := index $c "success" }} 17 - {{ $fail := index $c "failed" }} 18 - {{ $timeout := index $c "timeout" }} 19 - {{ $empty := eq $total 0 }} 20 - {{ $allPass := eq $success $total }} 21 - {{ $allFail := eq $fail $total }} 22 - {{ $allTimeout := eq $timeout $total }} 23 - 24 - {{ if $empty }} 25 - {{ i "hourglass" "size-4 text-gray-600 dark:text-gray-400 " }} 26 - {{ else if $allPass }} 27 - {{ i "check" "size-4 text-green-600 dark:text-green-500" }} 28 - {{ else if $allFail }} 29 - {{ i "x" "size-4 text-red-600 dark:text-red-500" }} 30 - {{ else if $allTimeout }} 31 - {{ i "clock-alert" "size-4 text-orange-500" }} 32 - {{ else }} 33 - {{ $radius := f64 8 }} 34 - {{ $circumference := mulf64 2.0 (mulf64 3.1416 $radius) }} 35 - {{ $offset := 0.0 }} 36 - <svg class="w-4 h-4 transform -rotate-90" viewBox="0 0 20 20"> 37 - <circle cx="10" cy="10" r="{{ $radius }}" fill="none" class="stroke-gray-200 dark:stroke-gray-700" stroke-width="2"/> 38 - {{ range $kind, $count := $c }} 39 - {{ $colorClass := "" }} 40 - {{ if or (eq $kind "pending") (eq $kind "running") }} 41 - {{ $colorClass = "stroke-yellow-600 dark:stroke-yellow-500" }} 42 - {{ else if eq $kind "success" }} 43 - {{ $colorClass = "stroke-green-600 dark:stroke-green-500" }} 44 - {{ else if eq $kind "cancelled" }} 45 - {{ $colorClass = "stroke-gray-600 dark:stroke-gray-500" }} 46 - {{ else if eq $kind "timeout" }} 47 - {{ $colorClass = "stroke-orange-600 dark:stroke-orange-500" }} 48 - {{ else }} 49 - {{ $colorClass = "stroke-red-600 dark:stroke-red-500" }} 50 - {{ end }} 51 - {{ $percent := divf64 (f64 $count) (f64 $total) }} 52 - {{ $length := mulf64 $percent $circumference }} 53 - <circle 54 - cx="10" cy="10" r="{{ $radius }}" 55 - fill="none" 56 - class="{{ $colorClass }}" 57 - stroke-width="2" 58 - stroke-dasharray="{{ printf "%.2f %.2f" $length (subf64 $circumference $length) }}" 59 - stroke-dashoffset="{{ printf "%.2f" (negf64 $offset) }}" 60 - /> 61 - {{ $offset = addf64 $offset $length }} 62 - {{ end }} 63 - </svg> 64 - {{ end }} 65 - {{ end }}
··· 1 {{ define "repo/pipelines/fragments/pipelineSymbol" }} 2 + <div class="cursor-pointer"> 3 + {{ $c := .Counts }} 4 + {{ $statuses := .Statuses }} 5 + {{ $total := len $statuses }} 6 + {{ $success := index $c "success" }} 7 + {{ $fail := index $c "failed" }} 8 + {{ $timeout := index $c "timeout" }} 9 + {{ $empty := eq $total 0 }} 10 + {{ $allPass := eq $success $total }} 11 + {{ $allFail := eq $fail $total }} 12 + {{ $allTimeout := eq $timeout $total }} 13 + 14 + {{ if $empty }} 15 + <div class="flex gap-1 items-center"> 16 + {{ i "hourglass" "size-4 text-gray-600 dark:text-gray-400 " }} 17 + <span>0/{{ $total }}</span> 18 + </div> 19 + {{ else if $allPass }} 20 + <div class="flex gap-1 items-center"> 21 + {{ i "check" "size-4 text-green-600" }} 22 + <span>{{ $total }}/{{ $total }}</span> 23 + </div> 24 + {{ else if $allFail }} 25 + <div class="flex gap-1 items-center"> 26 + {{ i "x" "size-4 text-red-500" }} 27 + <span>0/{{ $total }}</span> 28 + </div> 29 + {{ else if $allTimeout }} 30 + <div class="flex gap-1 items-center"> 31 + {{ i "clock-alert" "size-4 text-orange-500" }} 32 + <span>0/{{ $total }}</span> 33 + </div> 34 {{ else }} 35 + {{ $radius := f64 8 }} 36 + {{ $circumference := mulf64 2.0 (mulf64 3.1416 $radius) }} 37 + {{ $offset := 0.0 }} 38 + <div class="flex gap-1 items-center"> 39 + <svg class="w-4 h-4 transform -rotate-90" viewBox="0 0 20 20"> 40 + <circle cx="10" cy="10" r="{{ $radius }}" fill="none" stroke="#f3f4f633" stroke-width="2"/> 41 + 42 + {{ range $kind, $count := $c }} 43 + {{ $color := "" }} 44 + {{ if or (eq $kind "pending") (eq $kind "running") }} 45 + {{ $color = "#eab308" }} {{/* amber-500 */}} 46 + {{ else if eq $kind "success" }} 47 + {{ $color = "#10b981" }} {{/* green-500 */}} 48 + {{ else if eq $kind "cancelled" }} 49 + {{ $color = "#6b7280" }} {{/* gray-500 */}} 50 + {{ else if eq $kind "timeout" }} 51 + {{ $color = "#fb923c" }} {{/* orange-400 */}} 52 + {{ else }} 53 + {{ $color = "#ef4444" }} {{/* red-500 for failed or unknown */}} 54 + {{ end }} 55 + 56 + {{ $percent := divf64 (f64 $count) (f64 $total) }} 57 + {{ $length := mulf64 $percent $circumference }} 58 + 59 + <circle 60 + cx="10" cy="10" r="{{ $radius }}" 61 + fill="none" 62 + stroke="{{ $color }}" 63 + stroke-width="2" 64 + stroke-dasharray="{{ printf "%.2f %.2f" $length (subf64 $circumference $length) }}" 65 + stroke-dashoffset="{{ printf "%.2f" (negf64 $offset) }}" 66 + /> 67 + {{ $offset = addf64 $offset $length }} 68 + {{ end }} 69 + </svg> 70 + <span>{{ $success }}/{{ $total }}</span> 71 + </div> 72 {{ end }} 73 </div> 74 {{ end }}
+1 -1
appview/pages/templates/repo/pipelines/fragments/pipelineSymbolLong.html
··· 4 <div class="relative inline-block"> 5 <details class="relative"> 6 <summary class="cursor-pointer list-none"> 7 - {{ template "repo/pipelines/fragments/pipelineSymbol" (dict "Pipeline" $pipeline "ShortSummary" true) }} 8 </summary> 9 {{ template "repo/pipelines/fragments/tooltip" $ }} 10 </details>
··· 4 <div class="relative inline-block"> 5 <details class="relative"> 6 <summary class="cursor-pointer list-none"> 7 + {{ template "repo/pipelines/fragments/pipelineSymbol" .Pipeline }} 8 </summary> 9 {{ template "repo/pipelines/fragments/tooltip" $ }} 10 </details>
-14
appview/pages/templates/repo/pipelines/workflow.html
··· 12 {{ block "sidebar" . }} {{ end }} 13 </div> 14 <div class="col-span-1 md:col-span-3"> 15 - <!-- TODO(boltless): explictly check for pipeline cancel permission --> 16 - {{ if $.RepoInfo.Roles.IsOwner }} 17 - <div class="flex justify-between mb-2"> 18 - <div id="workflow-error" class="text-red-500 dark:text-red-400"></div> 19 - <button 20 - class="btn" 21 - hx-post="/{{ $.RepoInfo.FullName }}/pipelines/{{ .Pipeline.Id }}/workflow/{{ .Workflow }}/cancel" 22 - hx-swap="none" 23 - {{ if (index .Pipeline.Statuses .Workflow).Latest.Status.IsFinish -}} 24 - disabled 25 - {{- end }} 26 - >Cancel</button> 27 - </div> 28 - {{ end }} 29 {{ block "logs" . }} {{ end }} 30 </div> 31 </section>
··· 12 {{ block "sidebar" . }} {{ end }} 13 </div> 14 <div class="col-span-1 md:col-span-3"> 15 {{ block "logs" . }} {{ end }} 16 </div> 17 </section>
+17 -17
appview/pages/templates/repo/pulls/fragments/pullActions.html
··· 22 {{ $isLastRound := eq $roundNumber $lastIdx }} 23 {{ $isSameRepoBranch := .Pull.IsBranchBased }} 24 {{ $isUpToDate := .ResubmitCheck.No }} 25 - <div id="actions-{{$roundNumber}}" class="flex flex-wrap gap-2 relative p-2"> 26 <button 27 hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ $roundNumber }}/comment" 28 hx-target="#actions-{{$roundNumber}}" 29 hx-swap="outerHtml" 30 - class="btn-flat p-2 flex items-center gap-2 no-underline hover:no-underline group"> 31 - {{ i "message-square-plus" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 32 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 33 - comment 34 </button> 35 {{ if .BranchDeleteStatus }} 36 <button 37 hx-delete="/{{ .BranchDeleteStatus.Repo.Did }}/{{ .BranchDeleteStatus.Repo.Name }}/branches" 38 hx-vals='{"branch": "{{ .BranchDeleteStatus.Branch }}" }' 39 hx-swap="none" 40 - class="btn-flat p-2 flex items-center gap-2 no-underline hover:no-underline group text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"> 41 {{ i "git-branch" "w-4 h-4" }} 42 <span>delete branch</span> 43 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} ··· 52 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge" 53 hx-swap="none" 54 hx-confirm="Are you sure you want to merge pull #{{ .Pull.PullId }} into the `{{ .Pull.TargetBranch }}` branch?" 55 - class="btn-flat p-2 flex items-center gap-2 group" {{ $disabled }}> 56 - {{ i "git-merge" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 57 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 58 - merge{{if $stackCount}} {{$stackCount}}{{end}} 59 </button> 60 {{ end }} 61 ··· 74 {{ end }} 75 76 hx-disabled-elt="#resubmitBtn" 77 - class="btn-flat p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed group" {{ $disabled }} 78 79 {{ if $disabled }} 80 title="Update this branch to resubmit this pull request" ··· 82 title="Resubmit this pull request" 83 {{ end }} 84 > 85 - {{ i "rotate-ccw" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 86 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 87 - resubmit 88 </button> 89 {{ end }} 90 ··· 92 <button 93 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/close" 94 hx-swap="none" 95 - class="btn-flat p-2 flex items-center gap-2 group"> 96 - {{ i "ban" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 97 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 98 - close 99 </button> 100 {{ end }} 101 ··· 103 <button 104 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/reopen" 105 hx-swap="none" 106 - class="btn-flat p-2 flex items-center gap-2 group"> 107 - {{ i "refresh-ccw-dot" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 108 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 109 - reopen 110 </button> 111 {{ end }} 112 </div>
··· 22 {{ $isLastRound := eq $roundNumber $lastIdx }} 23 {{ $isSameRepoBranch := .Pull.IsBranchBased }} 24 {{ $isUpToDate := .ResubmitCheck.No }} 25 + <div id="actions-{{$roundNumber}}" class="flex flex-wrap gap-2 relative"> 26 <button 27 hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ $roundNumber }}/comment" 28 hx-target="#actions-{{$roundNumber}}" 29 hx-swap="outerHtml" 30 + class="btn p-2 flex items-center gap-2 no-underline hover:no-underline group"> 31 + {{ i "message-square-plus" "w-4 h-4" }} 32 + <span>comment</span> 33 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 34 </button> 35 {{ if .BranchDeleteStatus }} 36 <button 37 hx-delete="/{{ .BranchDeleteStatus.Repo.Did }}/{{ .BranchDeleteStatus.Repo.Name }}/branches" 38 hx-vals='{"branch": "{{ .BranchDeleteStatus.Branch }}" }' 39 hx-swap="none" 40 + class="btn p-2 flex items-center gap-2 no-underline hover:no-underline group text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"> 41 {{ i "git-branch" "w-4 h-4" }} 42 <span>delete branch</span> 43 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} ··· 52 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge" 53 hx-swap="none" 54 hx-confirm="Are you sure you want to merge pull #{{ .Pull.PullId }} into the `{{ .Pull.TargetBranch }}` branch?" 55 + class="btn p-2 flex items-center gap-2 group" {{ $disabled }}> 56 + {{ i "git-merge" "w-4 h-4" }} 57 + <span>merge{{if $stackCount}} {{$stackCount}}{{end}}</span> 58 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 59 </button> 60 {{ end }} 61 ··· 74 {{ end }} 75 76 hx-disabled-elt="#resubmitBtn" 77 + class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed group" {{ $disabled }} 78 79 {{ if $disabled }} 80 title="Update this branch to resubmit this pull request" ··· 82 title="Resubmit this pull request" 83 {{ end }} 84 > 85 + {{ i "rotate-ccw" "w-4 h-4" }} 86 + <span>resubmit</span> 87 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 88 </button> 89 {{ end }} 90 ··· 92 <button 93 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/close" 94 hx-swap="none" 95 + class="btn p-2 flex items-center gap-2 group"> 96 + {{ i "ban" "w-4 h-4" }} 97 + <span>close</span> 98 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 99 </button> 100 {{ end }} 101 ··· 103 <button 104 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/reopen" 105 hx-swap="none" 106 + class="btn p-2 flex items-center gap-2 group"> 107 + {{ i "refresh-ccw-dot" "w-4 h-4" }} 108 + <span>reopen</span> 109 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 110 </button> 111 {{ end }} 112 </div>
+7 -6
appview/pages/templates/repo/pulls/fragments/pullHeader.html
··· 1 {{ define "repo/pulls/fragments/pullHeader" }} 2 - <header class="pb-2"> 3 <h1 class="text-2xl dark:text-white"> 4 {{ .Pull.Title | description }} 5 <span class="text-gray-500 dark:text-gray-400">#{{ .Pull.PullId }}</span> ··· 17 {{ $icon = "git-merge" }} 18 {{ end }} 19 20 - <section> 21 <div class="flex items-center gap-2"> 22 - <span 23 - class="inline-flex items-center rounded px-2 py-[5px] {{ $bgColor }} text-sm" 24 > 25 - {{ i $icon "w-3 h-3 mr-1.5 text-white" }} 26 <span class="text-white">{{ .Pull.State.String }}</span> 27 - </span> 28 <span class="text-gray-500 dark:text-gray-400 text-sm flex flex-wrap items-center gap-1"> 29 opened by 30 {{ template "user/fragments/picHandleLink" .Pull.OwnerDid }}
··· 1 {{ define "repo/pulls/fragments/pullHeader" }} 2 + <header class="pb-4"> 3 <h1 class="text-2xl dark:text-white"> 4 {{ .Pull.Title | description }} 5 <span class="text-gray-500 dark:text-gray-400">#{{ .Pull.PullId }}</span> ··· 17 {{ $icon = "git-merge" }} 18 {{ end }} 19 20 + <section class="mt-2"> 21 <div class="flex items-center gap-2"> 22 + <div 23 + id="state" 24 + class="inline-flex items-center rounded px-3 py-1 {{ $bgColor }}" 25 > 26 + {{ i $icon "w-4 h-4 mr-1.5 text-white" }} 27 <span class="text-white">{{ .Pull.State.String }}</span> 28 + </div> 29 <span class="text-gray-500 dark:text-gray-400 text-sm flex flex-wrap items-center gap-1"> 30 opened by 31 {{ template "user/fragments/picHandleLink" .Pull.OwnerDid }}
+24 -39
appview/pages/templates/repo/pulls/fragments/pullNewComment.html
··· 1 {{ define "repo/pulls/fragments/pullNewComment" }} 2 <div 3 id="pull-comment-card-{{ .RoundNumber }}" 4 - class="w-full flex flex-col gap-2"> 5 - {{ template "user/fragments/picHandleLink" .LoggedInUser.Did }} 6 <form 7 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/comment" 8 hx-swap="none" 9 - hx-on::after-request="if(event.detail.successful) this.reset()" 10 - hx-disabled-elt="#reply-{{ .RoundNumber }}" 11 - class="w-full flex flex-wrap gap-2 group" 12 > 13 <textarea 14 name="body" 15 class="w-full p-2 rounded border border-gray-200" 16 - rows=8 17 placeholder="Add to the discussion..."></textarea 18 > 19 - {{ template "replyActions" . }} 20 <div id="pull-comment"></div> 21 </form> 22 </div> 23 {{ end }} 24 - 25 - {{ define "replyActions" }} 26 - <div class="flex flex-wrap items-stretch justify-end gap-2 text-gray-500 dark:text-gray-400 text-sm w-full"> 27 - {{ template "cancel" . }} 28 - {{ template "reply" . }} 29 - </div> 30 - {{ end }} 31 - 32 - {{ define "cancel" }} 33 - <button 34 - type="button" 35 - class="btn text-red-500 dark:text-red-400 flex gap-2 items-center group" 36 - hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/actions" 37 - hx-swap="outerHTML" 38 - hx-target="#actions-{{.RoundNumber}}" 39 - > 40 - {{ i "x" "w-4 h-4" }} 41 - <span>cancel</span> 42 - </button> 43 - {{ end }} 44 - 45 - {{ define "reply" }} 46 - <button 47 - type="submit" 48 - id="reply-{{ .RoundNumber }}" 49 - class="btn-create flex items-center gap-2"> 50 - {{ i "reply" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 51 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 52 - reply 53 - </button> 54 - {{ end }} 55 -
··· 1 {{ define "repo/pulls/fragments/pullNewComment" }} 2 <div 3 id="pull-comment-card-{{ .RoundNumber }}" 4 + class="bg-white dark:bg-gray-800 rounded drop-shadow-sm p-4 relative w-full flex flex-col gap-2"> 5 + <div class="text-sm text-gray-500 dark:text-gray-400"> 6 + {{ resolve .LoggedInUser.Did }} 7 + </div> 8 <form 9 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/comment" 10 + hx-indicator="#create-comment-spinner" 11 hx-swap="none" 12 + class="w-full flex flex-wrap gap-2" 13 > 14 <textarea 15 name="body" 16 class="w-full p-2 rounded border border-gray-200" 17 placeholder="Add to the discussion..."></textarea 18 > 19 + <button type="submit" class="btn flex items-center gap-2"> 20 + {{ i "message-square" "w-4 h-4" }} 21 + <span>comment</span> 22 + <span id="create-comment-spinner" class="group"> 23 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 24 + </span> 25 + </button> 26 + <button 27 + type="button" 28 + class="btn flex items-center gap-2 group" 29 + hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/actions" 30 + hx-swap="outerHTML" 31 + hx-target="#pull-comment-card-{{ .RoundNumber }}" 32 + > 33 + {{ i "x" "w-4 h-4" }} 34 + <span>cancel</span> 35 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 36 + </button> 37 <div id="pull-comment"></div> 38 </form> 39 </div> 40 {{ end }}
+3 -2
appview/pages/templates/repo/pulls/fragments/summarizedPullHeader.html
··· 15 16 <div class="flex-shrink-0 flex items-center gap-2"> 17 {{ $latestRound := .LastRoundNumber }} 18 - {{ $commentCount := .TotalComments }} 19 {{ if and $pipeline $pipeline.Id }} 20 - {{ template "repo/pipelines/fragments/pipelineSymbol" (dict "Pipeline" $pipeline "ShortSummary" true) }} 21 <span class="before:content-['ยท'] before:select-none text-gray-500 dark:text-gray-400"></span> 22 {{ end }} 23 <span>
··· 15 16 <div class="flex-shrink-0 flex items-center gap-2"> 17 {{ $latestRound := .LastRoundNumber }} 18 + {{ $lastSubmission := index .Submissions $latestRound }} 19 + {{ $commentCount := len $lastSubmission.Comments }} 20 {{ if and $pipeline $pipeline.Id }} 21 + {{ template "repo/pipelines/fragments/pipelineSymbol" $pipeline }} 22 <span class="before:content-['ยท'] before:select-none text-gray-500 dark:text-gray-400"></span> 23 {{ end }} 24 <span>
+21 -2
appview/pages/templates/repo/pulls/interdiff.html
··· 25 {{ template "repo/pulls/fragments/pullHeader" . }} 26 </header> 27 </section> 28 {{ end }} 29 30 {{ define "mainLayout" }} ··· 33 {{ block "content" . }}{{ end }} 34 {{ end }} 35 36 - {{ block "contentAfter" . }}{{ end }} 37 </div> 38 {{ end }} 39 40 {{ define "contentAfter" }} 41 - {{ template "repo/fragments/diff" (list .Interdiff .DiffOpts) }} 42 {{end}}
··· 25 {{ template "repo/pulls/fragments/pullHeader" . }} 26 </header> 27 </section> 28 + 29 {{ end }} 30 31 {{ define "mainLayout" }} ··· 34 {{ block "content" . }}{{ end }} 35 {{ end }} 36 37 + {{ block "contentAfterLayout" . }} 38 + <div class="flex-grow grid grid-cols-1 md:grid-cols-12 gap-4"> 39 + <div class="flex flex-col gap-4 col-span-1 md:col-span-2"> 40 + {{ block "contentAfterLeft" . }} {{ end }} 41 + </div> 42 + <main class="col-span-1 md:col-span-10"> 43 + {{ block "contentAfter" . }}{{ end }} 44 + </main> 45 + </div> 46 + {{ end }} 47 </div> 48 {{ end }} 49 50 {{ define "contentAfter" }} 51 + {{ template "repo/fragments/interdiff" (list .RepoInfo.FullName .Interdiff .DiffOpts) }} 52 + {{end}} 53 + 54 + {{ define "contentAfterLeft" }} 55 + <div class="flex flex-col gap-4 col-span-1 md:col-span-2"> 56 + {{ template "repo/fragments/diffOpts" .DiffOpts }} 57 + </div> 58 + <div class="sticky top-0 flex-grow max-h-screen overflow-y-auto"> 59 + {{ template "repo/fragments/interdiffFiles" .Interdiff }} 60 + </div> 61 {{end}}
+232 -448
appview/pages/templates/repo/pulls/pull.html
··· 6 {{ template "repo/pulls/fragments/og" (dict "RepoInfo" .RepoInfo "Pull" .Pull) }} 7 {{ end }} 8 9 - {{ define "mainLayout" }} 10 - <div class="px-1 flex-grow flex flex-col gap-4"> 11 - <div class="max-w-screen-lg mx-auto"> 12 - {{ block "contentLayout" . }} 13 - {{ block "content" . }}{{ end }} 14 - {{ end }} 15 </div> 16 - {{ block "contentAfterLayout" . }} 17 - <main> 18 - {{ block "contentAfter" . }}{{ end }} 19 - </main> 20 - {{ end }} 21 - </div> 22 - <script> 23 - (function() { 24 - const details = document.getElementById('bottomSheet'); 25 - const isDesktop = () => window.matchMedia('(min-width: 768px)').matches; 26 - 27 - // close on mobile initially 28 - if (!isDesktop()) { 29 - details.open = false; 30 - } 31 - 32 - // prevent closing on desktop 33 - details.addEventListener('toggle', function(e) { 34 - if (isDesktop() && !this.open) { 35 - this.open = true; 36 - } 37 - }); 38 - 39 - const mediaQuery = window.matchMedia('(min-width: 768px)'); 40 - mediaQuery.addEventListener('change', function(e) { 41 - if (e.matches) { 42 - // switched to desktop - keep open 43 - details.open = true; 44 - } else { 45 - // switched to mobile - close 46 - details.open = false; 47 - } 48 - }); 49 - })(); 50 - </script> 51 - {{ end }} 52 - 53 - {{ define "repoContentLayout" }} 54 - <div class="grid grid-cols-1 md:grid-cols-10 gap-4"> 55 - <section class="bg-white col-span-1 md:col-span-8 dark:bg-gray-800 p-6 rounded relative w-full mx-auto dark:text-white h-full flex-shrink"> 56 - {{ block "repoContent" . }}{{ end }} 57 - </section> 58 - <div class="flex flex-col gap-6 col-span-1 md:col-span-2"> 59 {{ template "repo/fragments/labelPanel" 60 (dict "RepoInfo" $.RepoInfo 61 "Defs" $.LabelDefs ··· 70 </div> 71 {{ end }} 72 73 - {{ define "contentAfter" }} 74 - {{ template "repo/fragments/diff" (list .Diff .DiffOpts $) }} 75 - {{ end }} 76 - 77 {{ define "repoContent" }} 78 {{ template "repo/pulls/fragments/pullHeader" . }} 79 {{ if .Pull.IsStacked }} 80 <div class="mt-8"> 81 {{ template "repo/pulls/fragments/pullStack" . }} ··· 83 {{ end }} 84 {{ end }} 85 86 - {{ define "diffLayout" }} 87 - {{ $diff := index . 0 }} 88 - {{ $opts := index . 1 }} 89 - {{ $root := index . 2 }} 90 - 91 - <div class="flex col-span-full"> 92 - <!-- left panel --> 93 - <div id="files" class="w-0 hidden md:block overflow-hidden sticky top-12 max-h-screen overflow-y-auto pb-12"> 94 - <section class="overflow-x-auto text-sm px-6 py-2 border-b border-x border-gray-200 dark:border-gray-700 w-full mx-auto min-h-full rounded-b rounded-t-none bg-white dark:bg-gray-800 drop-shadow-sm"> 95 - {{ template "repo/fragments/fileTree" $diff.FileTree }} 96 - </section> 97 - </div> 98 - 99 - <!-- main content --> 100 - <div class="flex-1 min-w-0 sticky top-12 pb-12"> 101 - {{ template "diffFiles" (list $diff $opts) }} 102 - </div> 103 - 104 - <!-- right panel --> 105 - {{ template "subsPanel" $ }} 106 - </div> 107 - {{ end }} 108 - 109 - {{ define "subsPanel" }} 110 - {{ $root := index . 2 }} 111 - {{ $pull := $root.Pull }} 112 - 113 - <!-- backdrop overlay - only visible on mobile when open --> 114 - <div class=" 115 - fixed inset-0 bg-black/50 z-50 md:hidden opacity-0 116 - pointer-events-none transition-opacity duration-300 117 - has-[~#subs_details[open]]:opacity-100 has-[~#subs_details[open]]:pointer-events-auto"> 118 - </div> 119 - <!-- right panel - bottom sheet on mobile, side panel on desktop --> 120 - <div id="subs" class="fixed bottom-0 left-0 right-0 z-50 w-full md:static md:z-auto md:max-h-screen md:sticky md:top-12 overflow-hidden"> 121 - <details open id="bottomSheet" class="group rounded-t-2xl md:rounded-t drop-shadow-lg md:drop-shadow-none"> 122 - <summary class=" 123 - flex gap-4 items-center justify-between 124 - rounded-t-2xl md:rounded-t cursor-pointer list-none p-4 md:h-12 125 - text-white md:text-black md:dark:text-white 126 - bg-green-600 dark:bg-green-700 127 - md:bg-white md:dark:bg-gray-800 128 - drop-shadow-sm 129 - border-t md:border-x md:border-t-0 border-gray-200 dark:border-gray-700"> 130 - <h2 class="">Submissions</h2> 131 - {{ template "subsPanelSummary" $ }} 132 - </summary> 133 - <div class="max-h-[85vh] md:max-h-[calc(100vh-3rem-3rem)] w-full flex flex-col-reverse gap-4 overflow-y-auto bg-slate-100 dark:bg-gray-900 md:bg-transparent"> 134 - {{ template "submissions" $root }} 135 </div> 136 - </details> 137 - </div> 138 - {{ end }} 139 140 - {{ define "subsPanelSummary" }} 141 - {{ $root := index . 2 }} 142 - {{ $pull := $root.Pull }} 143 - {{ $latest := $pull.LastRoundNumber }} 144 - <div class="flex items-center gap-2 text-sm"> 145 - <!--{{ if $root.IsInterdiff }} 146 - <span> 147 - viewing interdiff of 148 - <span class="font-mono">#{{ $root.ActiveRound }}</span> 149 - and 150 - <span class="font-mono">#{{ sub $root.ActiveRound 1 }}</span> 151 - </span> 152 - {{ else }} 153 - {{ if ne $root.ActiveRound $latest }} 154 - <span>(outdated)</span> 155 - <span class="before:content-['ยท']"></span> 156 - <a class="underline" href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $root.Pull.PullId }}/round/{{ $latest }}?{{ safeUrl $root.DiffOpts.Encode }}"> 157 - view latest 158 - </a> 159 - {{ end }} 160 - {{ end }}--> 161 - <span class="md:hidden inline"> 162 - <span class="inline group-open:hidden">{{ i "chevron-up" "size-4" }}</span> 163 - <span class="hidden group-open:inline">{{ i "chevron-down" "size-4" }}</span> 164 - </span> 165 - </div> 166 - {{ end }} 167 - 168 - {{ define "subsCheckbox" }} 169 - <input type="checkbox" id="subsToggle" class="peer/subs hidden" checked/> 170 - {{ end }} 171 - 172 - {{ define "subsToggle" }} 173 - <style> 174 - /* Mobile: full width */ 175 - #subsToggle:checked ~ div div#subs { 176 - width: 100%; 177 - margin-left: 0; 178 - } 179 - #subsToggle:checked ~ div label[for="subsToggle"] .show-toggle { display: none; } 180 - #subsToggle:checked ~ div label[for="subsToggle"] .hide-toggle { display: flex; } 181 - #subsToggle:not(:checked) ~ div label[for="subsToggle"] .hide-toggle { display: none; } 182 - 183 - /* Desktop: 25vw with left margin */ 184 - @media (min-width: 768px) { 185 - #subsToggle:checked ~ div div#subs { 186 - width: 25vw; 187 - margin-left: 1rem; 188 - } 189 - /* Unchecked state */ 190 - #subsToggle:not(:checked) ~ div div#subs { 191 - width: 0; 192 - display: none; 193 - margin-left: 0; 194 - } 195 - } 196 - </style> 197 - <label title="Toggle review panel" for="subsToggle" class="hidden md:flex items-center justify-end rounded cursor-pointer"> 198 - <span class="show-toggle">{{ i "message-square-more" "size-4" }}</span> 199 - <span class="hide-toggle w-[25vw] flex justify-end">{{ i "message-square" "size-4" }}</span> 200 - </label> 201 {{ end }} 202 - 203 204 {{ define "submissions" }} 205 {{ $lastIdx := sub (len .Pull.Submissions) 1 }} 206 - {{ if not .LoggedInUser }} 207 - {{ template "loginPrompt" $ }} 208 - {{ end }} 209 - {{ range $ridx, $item := reverse .Pull.Submissions }} 210 - {{ $idx := sub $lastIdx $ridx }} 211 - {{ template "submission" (list $item $idx $lastIdx $) }} 212 - {{ end }} 213 - {{ end }} 214 215 - {{ define "submission" }} 216 - {{ $item := index . 0 }} 217 - {{ $idx := index . 1 }} 218 - {{ $lastIdx := index . 2 }} 219 - {{ $root := index . 3 }} 220 - <div class="{{ if eq $item.RoundNumber 0 }}rounded-b border-t-0{{ else }}rounded{{ end }} border border-gray-200 dark:border-gray-700 w-full shadow-sm bg-gray-50 dark:bg-gray-800/50"> 221 - {{ template "submissionHeader" $ }} 222 - {{ template "submissionComments" $ }} 223 224 - {{ if eq $lastIdx $item.RoundNumber }} 225 - {{ block "mergeStatus" $root }} {{ end }} 226 - {{ block "resubmitStatus" $root }} {{ end }} 227 - {{ end }} 228 229 - {{ if $root.LoggedInUser }} 230 - {{ template "repo/pulls/fragments/pullActions" 231 - (dict 232 - "LoggedInUser" $root.LoggedInUser 233 - "Pull" $root.Pull 234 - "RepoInfo" $root.RepoInfo 235 - "RoundNumber" $item.RoundNumber 236 - "MergeCheck" $root.MergeCheck 237 - "ResubmitCheck" $root.ResubmitCheck 238 - "BranchDeleteStatus" $root.BranchDeleteStatus 239 - "Stack" $root.Stack) }} 240 - {{ end }} 241 - </div> 242 - {{ end }} 243 244 - {{ define "submissionHeader" }} 245 - {{ $item := index . 0 }} 246 - {{ $lastIdx := index . 2 }} 247 - {{ $root := index . 3 }} 248 - {{ $round := $item.RoundNumber }} 249 - <div class="{{ if eq $round 0 }}rounded-b{{ else }}rounded{{ end }} px-6 py-4 pr-2 pt-2 {{ if eq $root.ActiveRound $round }}bg-blue-100 dark:bg-blue-900/50 border-b border-blue-200 dark:border-blue-700{{ else }}bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700{{ end }} flex gap-2 sticky top-0 z-20"> 250 - <!-- left column: just profile picture --> 251 - <div class="flex-shrink-0 pt-2"> 252 - <img 253 - src="{{ tinyAvatar $root.Pull.OwnerDid }}" 254 - alt="" 255 - class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900" 256 - /> 257 - </div> 258 - <!-- right column --> 259 - <div class="flex-1 min-w-0 flex flex-col gap-1"> 260 - {{ template "submissionInfo" $ }} 261 - {{ template "submissionCommits" $ }} 262 - {{ template "submissionPipeline" $ }} 263 - {{ if eq $lastIdx $round }} 264 - {{ block "mergeCheck" $root }} {{ end }} 265 - {{ end }} 266 - </div> 267 - </div> 268 - {{ end }} 269 270 - {{ define "submissionInfo" }} 271 - {{ $item := index . 0 }} 272 - {{ $idx := index . 1 }} 273 - {{ $root := index . 3 }} 274 - {{ $round := $item.RoundNumber }} 275 - <div class="flex gap-2 items-center justify-between mb-1"> 276 - <span class="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 pt-2"> 277 - {{ resolve $root.Pull.OwnerDid }} submitted 278 - <span class="px-2 py-0.5 {{ if eq $root.ActiveRound $round }}text-white bg-blue-600 dark:bg-blue-500 border-blue-700 dark:border-blue-600{{ else }}text-black dark:text-white bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600{{ end }} rounded font-mono text-xs border"> 279 - #{{ $round }} 280 - </span> 281 - <span class="select-none before:content-['\00B7']"></span> 282 - <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500" href="#round-#{{ $round }}"> 283 - {{ template "repo/fragments/shortTime" $item.Created }} 284 - </a> 285 - </span> 286 - <div class="flex gap-2 items-center"> 287 - {{ if ne $root.ActiveRound $round }} 288 - <a class="btn-flat flex items-center gap-2 no-underline hover:no-underline text-sm" 289 - href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $root.Pull.PullId }}/round/{{ $round }}?{{ safeUrl $root.DiffOpts.Encode }}"> 290 - {{ i "diff" "w-4 h-4" }} 291 - diff 292 - </a> 293 - {{ end }} 294 - {{ if ne $idx 0 }} 295 - <a class="btn-flat flex items-center gap-2 no-underline hover:no-underline text-sm" 296 - href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $root.Pull.PullId }}/round/{{ $round }}/interdiff?{{ safeUrl $root.DiffOpts.Encode }}"> 297 - {{ i "chevrons-left-right-ellipsis" "w-4 h-4 rotate-90" }} 298 - interdiff 299 - </a> 300 - {{ end }} 301 - </div> 302 - </div> 303 - {{ end }} 304 305 - {{ define "submissionCommits" }} 306 - {{ $item := index . 0 }} 307 - {{ $root := index . 3 }} 308 - {{ $round := $item.RoundNumber }} 309 - {{ $patches := $item.AsFormatPatch }} 310 - {{ if $patches }} 311 - <details class="group/commit"> 312 - <summary class="list-none cursor-pointer flex items-center gap-2"> 313 - <span>{{ i "git-commit-horizontal" "w-4 h-4" }}</span> 314 - {{ len $patches }} commit{{ if ne (len $patches) 1 }}s{{ end }} 315 - <div class="text-sm text-gray-500 dark:text-gray-400"> 316 - <span class="group-open/commit:hidden inline">expand</span> 317 - <span class="hidden group-open/commit:inline">collapse</span> 318 - </div> 319 - </summary> 320 - {{ range $patches }} 321 - {{ template "submissionCommit" (list . $item $root) }} 322 - {{ end }} 323 - </details> 324 - {{ end }} 325 - {{ end }} 326 327 - {{ define "submissionCommit" }} 328 - {{ $patch := index . 0 }} 329 - {{ $item := index . 1 }} 330 - {{ $root := index . 2 }} 331 - {{ $round := $item.RoundNumber }} 332 - {{ with $patch }} 333 - <div id="commit-{{.SHA}}" class="py-1 relative w-full md:max-w-3/5 md:w-fit flex flex-col text-gray-600 dark:text-gray-300"> 334 - <div class="flex items-baseline gap-2"> 335 - <div class="text-xs"> 336 - <!-- attempt to resolve $fullRepo: this is possible only on non-deleted forks and branches --> 337 - {{ $fullRepo := "" }} 338 - {{ if and $root.Pull.IsForkBased $root.Pull.PullSource.Repo }} 339 - {{ $fullRepo = printf "%s/%s" $root.Pull.OwnerDid $root.Pull.PullSource.Repo.Name }} 340 - {{ else if $root.Pull.IsBranchBased }} 341 - {{ $fullRepo = $root.RepoInfo.FullName }} 342 {{ end }} 343 344 - <!-- if $fullRepo was resolved, link to it, otherwise just span without a link --> 345 - {{ if $fullRepo }} 346 - <a href="/{{ $fullRepo }}/commit/{{ .SHA }}" class="font-mono text-gray-600 dark:text-gray-300">{{ slice .SHA 0 8 }}</a> 347 {{ else }} 348 - <span class="font-mono">{{ slice .SHA 0 8 }}</span> 349 {{ end }} 350 </div> 351 - 352 - <div> 353 - <span>{{ .Title | description }}</span> 354 - {{ if gt (len .Body) 0 }} 355 - <button 356 - class="py-1/2 px-1 mx-2 bg-gray-200 hover:bg-gray-400 rounded dark:bg-gray-700 dark:hover:bg-gray-600" 357 - hx-on:click="document.getElementById('body-{{$round}}-{{.SHA}}').classList.toggle('hidden')" 358 - > 359 - {{ i "ellipsis" "w-3 h-3" }} 360 - </button> 361 - {{ end }} 362 - {{ if gt (len .Body) 0 }} 363 - <p id="body-{{$round}}-{{.SHA}}" class="hidden mt-1 pb-2">{{ nl2br .Body }}</p> 364 - {{ end }} 365 - </div> 366 - </div> 367 - </div> 368 - {{ end }} 369 - {{ end }} 370 - 371 - {{ define "mergeCheck" }} 372 - {{ $isOpen := .Pull.State.IsOpen }} 373 - {{ if and $isOpen .MergeCheck .MergeCheck.Error }} 374 - <div class="flex items-center gap-2"> 375 - {{ i "triangle-alert" "w-4 h-4 text-red-600 dark:text-red-500" }} 376 - {{ .MergeCheck.Error }} 377 - </div> 378 - {{ else if and $isOpen .MergeCheck .MergeCheck.IsConflicted }} 379 - <details class="group/conflict"> 380 - <summary class="flex items-center justify-between cursor-pointer list-none"> 381 - <div class="flex items-center gap-2 "> 382 - {{ i "triangle-alert" "text-red-600 dark:text-red-500 w-4 h-4" }} 383 - <span class="font-medium">merge conflicts detected</span> 384 - <div class="text-sm text-gray-500 dark:text-gray-400"> 385 - <span class="group-open/conflict:hidden inline">expand</span> 386 - <span class="hidden group-open/conflict:inline">collapse</span> 387 - </div> 388 - </div> 389 - </summary> 390 - {{ if gt (len .MergeCheck.Conflicts) 0 }} 391 - <ul class="space-y-1 mt-2 overflow-x-auto"> 392 - {{ range .MergeCheck.Conflicts }} 393 - {{ if .Filename }} 394 - <li class="flex items-center whitespace-nowrap"> 395 - {{ i "file-warning" "inline-flex w-4 h-4 mr-1.5 text-red-600 dark:text-red-500 flex-shrink-0" }} 396 - <span class="font-mono">{{ .Filename }}</span> 397 - </li> 398 - {{ else if .Reason }} 399 - <li class="flex items-center whitespace-nowrap"> 400 - {{ i "file-warning" "w-4 h-4 mr-1.5 text-red-600 dark:text-red-500 " }} 401 - <span>{{.Reason}}</span> 402 - </li> 403 - {{ end }} 404 - {{ end }} 405 - </ul> 406 - {{ end }} 407 </details> 408 - {{ else if and $isOpen .MergeCheck }} 409 - <div class="flex items-center gap-2"> 410 - {{ i "check" "w-4 h-4 text-green-600 dark:text-green-500" }} 411 - <span>no conflicts, ready to merge</span> 412 - </div> 413 {{ end }} 414 {{ end }} 415 416 {{ define "mergeStatus" }} 417 {{ if .Pull.State.IsClosed }} 418 - <div class="bg-gray-50 dark:bg-gray-700 border border-black dark:border-gray-500 rounded drop-shadow-sm px-6 py-2 relative"> 419 <div class="flex items-center gap-2 text-black dark:text-white"> 420 {{ i "ban" "w-4 h-4" }} 421 <span class="font-medium">closed without merging</span ··· 423 </div> 424 </div> 425 {{ else if .Pull.State.IsMerged }} 426 - <div class="bg-purple-50 dark:bg-purple-900 border border-purple-500 rounded drop-shadow-sm px-6 py-2 relative"> 427 <div class="flex items-center gap-2 text-purple-500 dark:text-purple-300"> 428 {{ i "git-merge" "w-4 h-4" }} 429 <span class="font-medium">pull request successfully merged</span ··· 431 </div> 432 </div> 433 {{ else if .Pull.State.IsDeleted }} 434 - <div class="bg-red-50 dark:bg-red-900 border border-red-500 rounded drop-shadow-sm px-6 py-2 relative"> 435 <div class="flex items-center gap-2 text-red-500 dark:text-red-300"> 436 {{ i "git-pull-request-closed" "w-4 h-4" }} 437 <span class="font-medium">This pull has been deleted (possibly by jj abandon or jj squash)</span> 438 </div> 439 </div> 440 {{ end }} 441 {{ end }} 442 443 {{ define "resubmitStatus" }} 444 {{ if .ResubmitCheck.Yes }} 445 - <div class="bg-amber-50 dark:bg-amber-900 border border-amber-500 rounded drop-shadow-sm px-6 py-2 relative"> 446 <div class="flex items-center gap-2 text-amber-500 dark:text-amber-300"> 447 {{ i "triangle-alert" "w-4 h-4" }} 448 <span class="font-medium">this branch has been updated, consider resubmitting</span> ··· 451 {{ end }} 452 {{ end }} 453 454 - {{ define "submissionPipeline" }} 455 - {{ $item := index . 0 }} 456 - {{ $root := index . 3 }} 457 - {{ $pipeline := index $root.Pipelines $item.SourceRev }} 458 {{ with $pipeline }} 459 {{ $id := .Id }} 460 {{ if .Statuses }} 461 - <details class="group/pipeline"> 462 - <summary class="cursor-pointer list-none flex items-center gap-2"> 463 - {{ template "repo/pipelines/fragments/pipelineSymbol" (dict "Pipeline" $pipeline "ShortSummary" false) }} 464 - <div class="text-sm text-gray-500 dark:text-gray-400"> 465 - <span class="group-open/pipeline:hidden inline">expand</span> 466 - <span class="hidden group-open/pipeline:inline">collapse</span> 467 - </div> 468 - </summary> 469 - <div class="my-2 grid grid-cols-1 bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700"> 470 - {{ range $name, $all := .Statuses }} 471 - <a href="/{{ $root.RepoInfo.FullName }}/pipelines/{{ $id }}/workflow/{{ $name }}" class="no-underline hover:no-underline hover:bg-gray-100/25 hover:dark:bg-gray-700/25"> 472 - <div 473 - class="flex gap-2 items-center justify-between p-2"> 474 - {{ $lastStatus := $all.Latest }} 475 - {{ $kind := $lastStatus.Status.String }} 476 477 - <div id="left" class="flex items-center gap-2 flex-shrink-0"> 478 - {{ template "repo/pipelines/fragments/workflowSymbol" $all }} 479 - {{ $name }} 480 - </div> 481 - <div id="right" class="flex items-center gap-2 flex-shrink-0"> 482 - <span class="font-bold">{{ $kind }}</span> 483 - {{ if .TimeTaken }} 484 - {{ template "repo/fragments/duration" .TimeTaken }} 485 - {{ else }} 486 - {{ template "repo/fragments/shortTimeAgo" $lastStatus.Created }} 487 - {{ end }} 488 - </div> 489 - </div> 490 - </a> 491 - {{ end }} 492 </div> 493 - </details> 494 {{ end }} 495 {{ end }} 496 {{ end }} 497 - 498 - {{ define "submissionComments" }} 499 - {{ $item := index . 0 }} 500 - <div class="relative ml-10 border-l-2 border-gray-200 dark:border-gray-700"> 501 - {{ range $item.Comments }} 502 - {{ template "submissionComment" . }} 503 - {{ end }} 504 - </div> 505 - {{ end }} 506 - 507 - {{ define "submissionComment" }} 508 - <div id="comment-{{.ID}}" class="flex gap-2 -ml-4 py-4 w-full mx-auto"> 509 - <!-- left column: profile picture --> 510 - <div class="flex-shrink-0"> 511 - <img 512 - src="{{ tinyAvatar .OwnerDid }}" 513 - alt="" 514 - class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900" 515 - /> 516 - </div> 517 - <!-- right column: name and body in two rows --> 518 - <div class="flex-1 min-w-0"> 519 - <!-- Row 1: Author and timestamp --> 520 - <div class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-1"> 521 - <span>{{ resolve .OwnerDid }}</span> 522 - <span class="before:content-['ยท']"></span> 523 - <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="#comment-{{.ID}}"> 524 - {{ template "repo/fragments/time" .Created }} 525 - </a> 526 - </div> 527 - <!-- Row 2: Body text --> 528 - <div class="prose dark:prose-invert mt-1"> 529 - {{ .Body | markdown }} 530 - </div> 531 - </div> 532 - </div> 533 - {{ end }} 534 - 535 - {{ define "loginPrompt" }} 536 - <div class="bg-amber-50 dark:bg-amber-900 border border-amber-500 rounded drop-shadow-sm p-2 relative flex gap-2 items-center"> 537 - <a href="/signup" class="btn-create py-0 hover:no-underline hover:text-white flex items-center gap-2"> 538 - sign up 539 - </a> 540 - <span class="text-gray-500 dark:text-gray-400">or</span> 541 - <a href="/login" class="underline">login</a> 542 - to add to the discussion 543 - </div> 544 - {{ end }}
··· 6 {{ template "repo/pulls/fragments/og" (dict "RepoInfo" .RepoInfo "Pull" .Pull) }} 7 {{ end }} 8 9 + {{ define "repoContentLayout" }} 10 + <div class="grid grid-cols-1 md:grid-cols-10 gap-4 w-full"> 11 + <div class="col-span-1 md:col-span-8"> 12 + <section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto dark:text-white"> 13 + {{ block "repoContent" . }}{{ end }} 14 + </section> 15 + {{ block "repoAfter" . }}{{ end }} 16 </div> 17 + <div class="col-span-1 md:col-span-2 flex flex-col gap-6"> 18 {{ template "repo/fragments/labelPanel" 19 (dict "RepoInfo" $.RepoInfo 20 "Defs" $.LabelDefs ··· 29 </div> 30 {{ end }} 31 32 {{ define "repoContent" }} 33 {{ template "repo/pulls/fragments/pullHeader" . }} 34 + 35 {{ if .Pull.IsStacked }} 36 <div class="mt-8"> 37 {{ template "repo/pulls/fragments/pullStack" . }} ··· 39 {{ end }} 40 {{ end }} 41 42 + {{ define "repoAfter" }} 43 + <section id="submissions" class="mt-4"> 44 + <div class="flex flex-col gap-4"> 45 + {{ block "submissions" . }} {{ end }} 46 </div> 47 + </section> 48 49 + <div id="pull-close"></div> 50 + <div id="pull-reopen"></div> 51 {{ end }} 52 53 {{ define "submissions" }} 54 {{ $lastIdx := sub (len .Pull.Submissions) 1 }} 55 + {{ $targetBranch := .Pull.TargetBranch }} 56 + {{ $repoName := .RepoInfo.FullName }} 57 + {{ range $idx, $item := .Pull.Submissions }} 58 + {{ with $item }} 59 + <details {{ if eq $idx $lastIdx }}open{{ end }}> 60 + <summary id="round-#{{ .RoundNumber }}" class="list-none cursor-pointer"> 61 + <div class="flex flex-wrap gap-2 items-stretch"> 62 + <!-- round number --> 63 + <div class="rounded bg-white dark:bg-gray-800 drop-shadow-sm px-3 py-2 dark:text-white"> 64 + <span class="flex items-center">{{ i "hash" "w-4 h-4" }}{{ .RoundNumber }}</span> 65 + </div> 66 + <!-- round summary --> 67 + <div class="flex-1 rounded drop-shadow-sm bg-white dark:bg-gray-800 p-2 text-gray-500 dark:text-gray-400"> 68 + <span class="gap-1 flex items-center"> 69 + {{ $owner := resolve $.Pull.OwnerDid }} 70 + {{ $re := "re" }} 71 + {{ if eq .RoundNumber 0 }} 72 + {{ $re = "" }} 73 + {{ end }} 74 + <span class="hidden md:inline">{{$re}}submitted</span> 75 + by {{ template "user/fragments/picHandleLink" $.Pull.OwnerDid }} 76 + <span class="select-none before:content-['\00B7']"></span> 77 + <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500" href="#round-#{{ .RoundNumber }}">{{ template "repo/fragments/shortTime" .Created }}</a> 78 + <span class="select-none before:content-['ยท']"></span> 79 + {{ $s := "s" }} 80 + {{ if eq (len .Comments) 1 }} 81 + {{ $s = "" }} 82 + {{ end }} 83 + {{ len .Comments }} comment{{$s}} 84 + </span> 85 + </div> 86 87 + <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group" 88 + hx-boost="true" 89 + href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}"> 90 + {{ i "file-diff" "w-4 h-4" }} 91 + <span class="hidden md:inline">diff</span> 92 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 93 + </a> 94 + {{ if ne $idx 0 }} 95 + <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group" 96 + hx-boost="true" 97 + href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}/interdiff"> 98 + {{ i "chevrons-left-right-ellipsis" "w-4 h-4 rotate-90" }} 99 + <span class="hidden md:inline">interdiff</span> 100 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 101 + </a> 102 + {{ end }} 103 + <span id="interdiff-error-{{.RoundNumber}}"></span> 104 + </div> 105 + </summary> 106 107 + {{ if .IsFormatPatch }} 108 + {{ $patches := .AsFormatPatch }} 109 + {{ $round := .RoundNumber }} 110 + <details class="group py-2 md:ml-[3.5rem] text-gray-500 dark:text-gray-400 flex flex-col gap-2 relative text-sm"> 111 + <summary class="py-1 list-none cursor-pointer hover:text-gray-500 hover:dark:text-gray-400"> 112 + {{ $s := "s" }} 113 + {{ if eq (len $patches) 1 }} 114 + {{ $s = "" }} 115 + {{ end }} 116 + <div class="group-open:hidden flex items-center gap-2 ml-2"> 117 + {{ i "chevrons-up-down" "w-4 h-4" }} expand {{ len $patches }} commit{{$s}} 118 + </div> 119 + <div class="hidden group-open:flex items-center gap-2 ml-2"> 120 + {{ i "chevrons-down-up" "w-4 h-4" }} hide {{ len $patches }} commit{{$s}} 121 + </div> 122 + </summary> 123 + {{ range $patches }} 124 + <div id="commit-{{.SHA}}" class="py-1 px-2 relative w-full md:max-w-3/5 md:w-fit flex flex-col"> 125 + <div class="flex items-center gap-2"> 126 + {{ i "git-commit-horizontal" "w-4 h-4" }} 127 + <div class="text-sm text-gray-500 dark:text-gray-400"> 128 + <!-- attempt to resolve $fullRepo: this is possible only on non-deleted forks and branches --> 129 + {{ $fullRepo := "" }} 130 + {{ if and $.Pull.IsForkBased $.Pull.PullSource.Repo }} 131 + {{ $fullRepo = printf "%s/%s" $owner $.Pull.PullSource.Repo.Name }} 132 + {{ else if $.Pull.IsBranchBased }} 133 + {{ $fullRepo = $.RepoInfo.FullName }} 134 + {{ end }} 135 136 + <!-- if $fullRepo was resolved, link to it, otherwise just span without a link --> 137 + {{ if $fullRepo }} 138 + <a href="/{{ $fullRepo }}/commit/{{ .SHA }}" class="font-mono text-gray-500 dark:text-gray-400">{{ slice .SHA 0 8 }}</a> 139 + {{ else }} 140 + <span class="font-mono">{{ slice .SHA 0 8 }}</span> 141 + {{ end }} 142 + </div> 143 + <div class="flex items-center"> 144 + <span>{{ .Title | description }}</span> 145 + {{ if gt (len .Body) 0 }} 146 + <button 147 + class="py-1/2 px-1 mx-2 bg-gray-200 hover:bg-gray-400 rounded dark:bg-gray-700 dark:hover:bg-gray-600" 148 + hx-on:click="document.getElementById('body-{{$round}}-{{.SHA}}').classList.toggle('hidden')" 149 + > 150 + {{ i "ellipsis" "w-3 h-3" }} 151 + </button> 152 + {{ end }} 153 + </div> 154 + </div> 155 + {{ if gt (len .Body) 0 }} 156 + <p id="body-{{$round}}-{{.SHA}}" class="hidden mt-1 text-sm pb-2"> 157 + {{ nl2br .Body }} 158 + </p> 159 + {{ end }} 160 + </div> 161 + {{ end }} 162 + </details> 163 + {{ end }} 164 165 166 + <div class="md:pl-[3.5rem] flex flex-col gap-2 mt-2 relative"> 167 + {{ range $cidx, $c := .Comments }} 168 + <div id="comment-{{$c.ID}}" class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-2 px-4 relative w-full"> 169 + {{ if gt $cidx 0 }} 170 + <div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div> 171 + {{ end }} 172 + <div class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-1"> 173 + {{ template "user/fragments/picHandleLink" $c.OwnerDid }} 174 + <span class="before:content-['ยท']"></span> 175 + <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="#comment-{{.ID}}">{{ template "repo/fragments/time" $c.Created }}</a> 176 + </div> 177 + <div class="prose dark:prose-invert"> 178 + {{ $c.Body | markdown }} 179 + </div> 180 + </div> 181 + {{ end }} 182 183 + {{ block "pipelineStatus" (list $ .) }} {{ end }} 184 185 + {{ if eq $lastIdx .RoundNumber }} 186 + {{ block "mergeStatus" $ }} {{ end }} 187 + {{ block "resubmitStatus" $ }} {{ end }} 188 {{ end }} 189 190 + {{ if $.LoggedInUser }} 191 + {{ template "repo/pulls/fragments/pullActions" 192 + (dict 193 + "LoggedInUser" $.LoggedInUser 194 + "Pull" $.Pull 195 + "RepoInfo" $.RepoInfo 196 + "RoundNumber" .RoundNumber 197 + "MergeCheck" $.MergeCheck 198 + "ResubmitCheck" $.ResubmitCheck 199 + "BranchDeleteStatus" $.BranchDeleteStatus 200 + "Stack" $.Stack) }} 201 {{ else }} 202 + <div class="bg-amber-50 dark:bg-amber-900 border border-amber-500 rounded drop-shadow-sm p-2 relative flex gap-2 items-center w-fit"> 203 + <a href="/signup" class="btn-create py-0 hover:no-underline hover:text-white flex items-center gap-2"> 204 + sign up 205 + </a> 206 + <span class="text-gray-500 dark:text-gray-400">or</span> 207 + <a href="/login" class="underline">login</a> 208 + to add to the discussion 209 + </div> 210 {{ end }} 211 </div> 212 </details> 213 + {{ end }} 214 {{ end }} 215 {{ end }} 216 217 {{ define "mergeStatus" }} 218 {{ if .Pull.State.IsClosed }} 219 + <div class="bg-gray-50 dark:bg-gray-700 border border-black dark:border-gray-500 rounded drop-shadow-sm px-6 py-2 relative w-fit"> 220 <div class="flex items-center gap-2 text-black dark:text-white"> 221 {{ i "ban" "w-4 h-4" }} 222 <span class="font-medium">closed without merging</span ··· 224 </div> 225 </div> 226 {{ else if .Pull.State.IsMerged }} 227 + <div class="bg-purple-50 dark:bg-purple-900 border border-purple-500 rounded drop-shadow-sm px-6 py-2 relative w-fit"> 228 <div class="flex items-center gap-2 text-purple-500 dark:text-purple-300"> 229 {{ i "git-merge" "w-4 h-4" }} 230 <span class="font-medium">pull request successfully merged</span ··· 232 </div> 233 </div> 234 {{ else if .Pull.State.IsDeleted }} 235 + <div class="bg-red-50 dark:bg-red-900 border border-red-500 rounded drop-shadow-sm px-6 py-2 relative w-fit"> 236 <div class="flex items-center gap-2 text-red-500 dark:text-red-300"> 237 {{ i "git-pull-request-closed" "w-4 h-4" }} 238 <span class="font-medium">This pull has been deleted (possibly by jj abandon or jj squash)</span> 239 </div> 240 </div> 241 + {{ else if and .MergeCheck .MergeCheck.Error }} 242 + <div class="bg-red-50 dark:bg-red-900 border border-red-500 rounded drop-shadow-sm px-6 py-2 relative w-fit"> 243 + <div class="flex items-center gap-2 text-red-500 dark:text-red-300"> 244 + {{ i "triangle-alert" "w-4 h-4" }} 245 + <span class="font-medium">{{ .MergeCheck.Error }}</span> 246 + </div> 247 + </div> 248 + {{ else if and .MergeCheck .MergeCheck.IsConflicted }} 249 + <div class="bg-red-50 dark:bg-red-900 border border-red-500 rounded drop-shadow-sm px-6 py-2 relative w-fit"> 250 + <div class="flex flex-col gap-2 text-red-500 dark:text-red-300"> 251 + <div class="flex items-center gap-2"> 252 + {{ i "triangle-alert" "w-4 h-4" }} 253 + <span class="font-medium">merge conflicts detected</span> 254 + </div> 255 + {{ if gt (len .MergeCheck.Conflicts) 0 }} 256 + <ul class="space-y-1"> 257 + {{ range .MergeCheck.Conflicts }} 258 + {{ if .Filename }} 259 + <li class="flex items-center"> 260 + {{ i "file-warning" "w-4 h-4 mr-1.5 text-red-500 dark:text-red-300" }} 261 + <span class="font-mono">{{ .Filename }}</span> 262 + </li> 263 + {{ else if .Reason }} 264 + <li class="flex items-center"> 265 + {{ i "file-warning" "w-4 h-4 mr-1.5 text-red-500 dark:text-red-300" }} 266 + <span>{{.Reason}}</span> 267 + </li> 268 + {{ end }} 269 + {{ end }} 270 + </ul> 271 + {{ end }} 272 + </div> 273 + </div> 274 + {{ else if .MergeCheck }} 275 + <div class="bg-green-50 dark:bg-green-900 border border-green-500 rounded drop-shadow-sm px-6 py-2 relative w-fit"> 276 + <div class="flex items-center gap-2 text-green-500 dark:text-green-300"> 277 + {{ i "circle-check-big" "w-4 h-4" }} 278 + <span class="font-medium">no conflicts, ready to merge</span> 279 + </div> 280 + </div> 281 {{ end }} 282 {{ end }} 283 284 {{ define "resubmitStatus" }} 285 {{ if .ResubmitCheck.Yes }} 286 + <div class="bg-amber-50 dark:bg-amber-900 border border-amber-500 rounded drop-shadow-sm px-6 py-2 relative w-fit"> 287 <div class="flex items-center gap-2 text-amber-500 dark:text-amber-300"> 288 {{ i "triangle-alert" "w-4 h-4" }} 289 <span class="font-medium">this branch has been updated, consider resubmitting</span> ··· 292 {{ end }} 293 {{ end }} 294 295 + {{ define "pipelineStatus" }} 296 + {{ $root := index . 0 }} 297 + {{ $submission := index . 1 }} 298 + {{ $pipeline := index $root.Pipelines $submission.SourceRev }} 299 {{ with $pipeline }} 300 {{ $id := .Id }} 301 {{ if .Statuses }} 302 + <div class="max-w-80 grid grid-cols-1 bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700"> 303 + {{ range $name, $all := .Statuses }} 304 + <a href="/{{ $root.RepoInfo.FullName }}/pipelines/{{ $id }}/workflow/{{ $name }}" class="no-underline hover:no-underline hover:bg-gray-100/25 hover:dark:bg-gray-700/25"> 305 + <div 306 + class="flex gap-2 items-center justify-between p-2"> 307 + {{ $lastStatus := $all.Latest }} 308 + {{ $kind := $lastStatus.Status.String }} 309 310 + <div id="left" class="flex items-center gap-2 flex-shrink-0"> 311 + {{ template "repo/pipelines/fragments/workflowSymbol" $all }} 312 + {{ $name }} 313 + </div> 314 + <div id="right" class="flex items-center gap-2 flex-shrink-0"> 315 + <span class="font-bold">{{ $kind }}</span> 316 + {{ if .TimeTaken }} 317 + {{ template "repo/fragments/duration" .TimeTaken }} 318 + {{ else }} 319 + {{ template "repo/fragments/shortTimeAgo" $lastStatus.Created }} 320 + {{ end }} 321 + </div> 322 </div> 323 + </a> 324 + {{ end }} 325 + </div> 326 {{ end }} 327 {{ end }} 328 {{ end }}
+12 -3
appview/pages/templates/repo/pulls/pulls.html
··· 112 {{ template "repo/fragments/time" .Created }} 113 </span> 114 115 <span class="before:content-['ยท']"> 116 - {{ $commentCount := .TotalComments }} 117 - {{ $commentCount }} comment{{ if ne $commentCount 1 }}s{{ end }} 118 </span> 119 120 <span class="before:content-['ยท']"> ··· 127 {{ $pipeline := index $.Pipelines .LatestSha }} 128 {{ if and $pipeline $pipeline.Id }} 129 <span class="before:content-['ยท']"></span> 130 - {{ template "repo/pipelines/fragments/pipelineSymbol" (dict "Pipeline" $pipeline "ShortSummary" true) }} 131 {{ end }} 132 133 {{ $state := .Labels }}
··· 112 {{ template "repo/fragments/time" .Created }} 113 </span> 114 115 + 116 + {{ $latestRound := .LastRoundNumber }} 117 + {{ $lastSubmission := index .Submissions $latestRound }} 118 + 119 <span class="before:content-['ยท']"> 120 + {{ $commentCount := len $lastSubmission.Comments }} 121 + {{ $s := "s" }} 122 + {{ if eq $commentCount 1 }} 123 + {{ $s = "" }} 124 + {{ end }} 125 + 126 + {{ len $lastSubmission.Comments}} comment{{$s}} 127 </span> 128 129 <span class="before:content-['ยท']"> ··· 136 {{ $pipeline := index $.Pipelines .LatestSha }} 137 {{ if and $pipeline $pipeline.Id }} 138 <span class="before:content-['ยท']"></span> 139 + {{ template "repo/pipelines/fragments/pipelineSymbol" $pipeline }} 140 {{ end }} 141 142 {{ $state := .Labels }}
+16 -24
appview/pages/templates/strings/string.html
··· 10 11 {{ define "content" }} 12 {{ $ownerId := resolve .Owner.DID.String }} 13 - <section id="string-header" class="mb-2 py-2 px-4 dark:text-white"> 14 - <div class="text-lg flex flex-col sm:flex-row items-start gap-4 justify-between"> 15 - <!-- left items --> 16 - <div class="flex flex-col gap-2"> 17 - <!-- string owner / string name --> 18 - <div class="flex items-center gap-2 flex-wrap"> 19 - {{ template "user/fragments/picHandleLink" .Owner.DID.String }} 20 - <span class="select-none">/</span> 21 - <a href="/strings/{{ $ownerId }}/{{ .String.Rkey }}" class="font-bold">{{ .String.Filename }}</a> 22 - </div> 23 - 24 - <span class="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-600 dark:text-gray-300"> 25 - {{ if .String.Description }} 26 - {{ .String.Description }} 27 - {{ else }} 28 - <span class="italic">this string has no description</span> 29 - {{ end }} 30 - </span> 31 </div> 32 - 33 - <div class="w-full sm:w-fit grid grid-cols-3 gap-2 z-auto"> 34 {{ if and .LoggedInUser (eq .LoggedInUser.Did .String.Did) }} 35 - <a class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group" 36 hx-boost="true" 37 href="/strings/{{ .String.Did }}/{{ .String.Rkey }}/edit"> 38 - {{ i "pencil" "w-4 h-4" }} 39 <span class="hidden md:inline">edit</span> 40 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 41 </a> 42 <button 43 - class="btn text-sm text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 flex items-center gap-2 group" 44 title="Delete string" 45 hx-delete="/strings/{{ .String.Did }}/{{ .String.Rkey }}/" 46 hx-swap="none" 47 hx-confirm="Are you sure you want to delete the string `{{ .String.Filename }}`?" 48 > 49 - {{ i "trash-2" "w-4 h-4" }} 50 <span class="hidden md:inline">delete</span> 51 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 52 </button> ··· 57 "StarCount" .StarCount) }} 58 </div> 59 </div> 60 </section> 61 <section class="bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full dark:text-white"> 62 <div class="flex flex-col md:flex-row md:justify-between md:items-center text-gray-500 dark:text-gray-400 text-sm md:text-base pb-2 mb-3 text-base border-b border-gray-200 dark:border-gray-700">
··· 10 11 {{ define "content" }} 12 {{ $ownerId := resolve .Owner.DID.String }} 13 + <section id="string-header" class="mb-4 py-2 px-6 dark:text-white"> 14 + <div class="text-lg flex items-center justify-between"> 15 + <div> 16 + <a href="/strings/{{ $ownerId }}">{{ $ownerId }}</a> 17 + <span class="select-none">/</span> 18 + <a href="/strings/{{ $ownerId }}/{{ .String.Rkey }}" class="font-bold">{{ .String.Filename }}</a> 19 </div> 20 + <div class="flex gap-2 items-stretch text-base"> 21 {{ if and .LoggedInUser (eq .LoggedInUser.Did .String.Did) }} 22 + <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group" 23 hx-boost="true" 24 href="/strings/{{ .String.Did }}/{{ .String.Rkey }}/edit"> 25 + {{ i "pencil" "size-4" }} 26 <span class="hidden md:inline">edit</span> 27 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 28 </a> 29 <button 30 + class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group p-2" 31 title="Delete string" 32 hx-delete="/strings/{{ .String.Did }}/{{ .String.Rkey }}/" 33 hx-swap="none" 34 hx-confirm="Are you sure you want to delete the string `{{ .String.Filename }}`?" 35 > 36 + {{ i "trash-2" "size-4" }} 37 <span class="hidden md:inline">delete</span> 38 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 39 </button> ··· 44 "StarCount" .StarCount) }} 45 </div> 46 </div> 47 + <span> 48 + {{ with .String.Description }} 49 + {{ . }} 50 + {{ end }} 51 + </span> 52 </section> 53 <section class="bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full dark:text-white"> 54 <div class="flex flex-col md:flex-row md:justify-between md:items-center text-gray-500 dark:text-gray-400 text-sm md:text-base pb-2 mb-3 text-base border-b border-gray-200 dark:border-gray-700">
-53
appview/pages/templates/user/login.html
··· 20 <h2 class="text-center text-xl italic dark:text-white"> 21 tightly-knit social coding. 22 </h2> 23 - 24 - {{ if .AddAccount }} 25 - <div class="flex gap-2 my-4 bg-blue-50 dark:bg-blue-900/30 border border-blue-300 dark:border-sky-800 rounded px-3 py-2 text-blue-600 dark:text-blue-300"> 26 - <span class="py-1">{{ i "user-plus" "w-4 h-4" }}</span> 27 - <div> 28 - <h5 class="font-medium">Add another account</h5> 29 - <p class="text-sm">Sign in with a different account to add it to your account list.</p> 30 - </div> 31 - </div> 32 - {{ end }} 33 - 34 - {{ if and .LoggedInUser .LoggedInUser.Accounts }} 35 - {{ $accounts := .LoggedInUser.Accounts }} 36 - {{ if $accounts }} 37 - <div class="my-4 border border-gray-200 dark:border-gray-700 rounded overflow-hidden"> 38 - <div class="px-3 py-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700"> 39 - <span class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide font-medium">Saved accounts</span> 40 - </div> 41 - <div class="divide-y divide-gray-200 dark:divide-gray-700"> 42 - {{ range $accounts }} 43 - <div class="flex items-center justify-between px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"> 44 - <button 45 - type="button" 46 - hx-post="/account/switch" 47 - hx-vals='{"did": "{{ .Did }}"}' 48 - hx-swap="none" 49 - class="flex items-center gap-2 flex-1 text-left min-w-0" 50 - > 51 - <img src="{{ tinyAvatar .Did }}" alt="" class="rounded-full h-8 w-8 flex-shrink-0 border border-gray-300 dark:border-gray-700" /> 52 - <div class="flex flex-col min-w-0"> 53 - <span class="text-sm font-medium dark:text-white truncate">{{ .Did | resolve | truncateAt30 }}</span> 54 - <span class="text-xs text-gray-500 dark:text-gray-400">Click to switch</span> 55 - </div> 56 - </button> 57 - <button 58 - type="button" 59 - hx-delete="/account/{{ .Did }}" 60 - hx-swap="none" 61 - class="p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 flex-shrink-0" 62 - title="Remove account" 63 - > 64 - {{ i "x" "w-4 h-4" }} 65 - </button> 66 - </div> 67 - {{ end }} 68 - </div> 69 - </div> 70 - {{ end }} 71 - {{ end }} 72 - 73 <form 74 class="mt-4" 75 hx-post="/login" ··· 96 </span> 97 </div> 98 <input type="hidden" name="return_url" value="{{ .ReturnUrl }}"> 99 - <input type="hidden" name="add_account" value="{{ if .AddAccount }}true{{ end }}"> 100 101 <button 102 class="btn w-full my-2 mt-6 text-base " ··· 117 You have not authorized the app. 118 {{ else if eq .ErrorCode "session" }} 119 Server failed to create user session. 120 - {{ else if eq .ErrorCode "max_accounts" }} 121 - You have reached the maximum of 20 linked accounts. Please remove an account before adding a new one. 122 {{ else }} 123 Internal Server error. 124 {{ end }}
··· 20 <h2 class="text-center text-xl italic dark:text-white"> 21 tightly-knit social coding. 22 </h2> 23 <form 24 class="mt-4" 25 hx-post="/login" ··· 46 </span> 47 </div> 48 <input type="hidden" name="return_url" value="{{ .ReturnUrl }}"> 49 50 <button 51 class="btn w-full my-2 mt-6 text-base " ··· 66 You have not authorized the app. 67 {{ else if eq .ErrorCode "session" }} 68 Server failed to create user session. 69 {{ else }} 70 Internal Server error. 71 {{ end }}
+3 -87
appview/pipelines/pipelines.go
··· 4 "bytes" 5 "context" 6 "encoding/json" 7 - "fmt" 8 "log/slog" 9 "net/http" 10 "strings" 11 "time" 12 13 - "tangled.org/core/api/tangled" 14 "tangled.org/core/appview/config" 15 "tangled.org/core/appview/db" 16 - "tangled.org/core/appview/middleware" 17 - "tangled.org/core/appview/models" 18 "tangled.org/core/appview/oauth" 19 "tangled.org/core/appview/pages" 20 "tangled.org/core/appview/reporesolver" ··· 40 logger *slog.Logger 41 } 42 43 - func (p *Pipelines) Router(mw *middleware.Middleware) http.Handler { 44 r := chi.NewRouter() 45 r.Get("/", p.Index) 46 r.Get("/{pipeline}/workflow/{workflow}", p.Workflow) 47 r.Get("/{pipeline}/workflow/{workflow}/logs", p.Logs) 48 - r. 49 - With(mw.RepoPermissionMiddleware("repo:owner")). 50 - Post("/{pipeline}/workflow/{workflow}/cancel", p.Cancel) 51 52 return r 53 } ··· 77 } 78 79 func (p *Pipelines) Index(w http.ResponseWriter, r *http.Request) { 80 - user := p.oauth.GetMultiAccountUser(r) 81 l := p.logger.With("handler", "Index") 82 83 f, err := p.repoResolver.Resolve(r) ··· 106 } 107 108 func (p *Pipelines) Workflow(w http.ResponseWriter, r *http.Request) { 109 - user := p.oauth.GetMultiAccountUser(r) 110 l := p.logger.With("handler", "Workflow") 111 112 f, err := p.repoResolver.Resolve(r) ··· 321 } 322 } 323 } 324 - } 325 - 326 - func (p *Pipelines) Cancel(w http.ResponseWriter, r *http.Request) { 327 - l := p.logger.With("handler", "Cancel") 328 - 329 - var ( 330 - pipelineId = chi.URLParam(r, "pipeline") 331 - workflow = chi.URLParam(r, "workflow") 332 - ) 333 - if pipelineId == "" || workflow == "" { 334 - http.Error(w, "missing pipeline ID or workflow", http.StatusBadRequest) 335 - return 336 - } 337 - 338 - f, err := p.repoResolver.Resolve(r) 339 - if err != nil { 340 - l.Error("failed to get repo and knot", "err", err) 341 - http.Error(w, "bad repo/knot", http.StatusBadRequest) 342 - return 343 - } 344 - 345 - pipeline, err := func() (models.Pipeline, error) { 346 - ps, err := db.GetPipelineStatuses( 347 - p.db, 348 - 1, 349 - orm.FilterEq("repo_owner", f.Did), 350 - orm.FilterEq("repo_name", f.Name), 351 - orm.FilterEq("knot", f.Knot), 352 - orm.FilterEq("id", pipelineId), 353 - ) 354 - if err != nil { 355 - return models.Pipeline{}, err 356 - } 357 - if len(ps) != 1 { 358 - return models.Pipeline{}, fmt.Errorf("wrong pipeline count %d", len(ps)) 359 - } 360 - return ps[0], nil 361 - }() 362 - if err != nil { 363 - l.Error("pipeline query failed", "err", err) 364 - http.Error(w, "pipeline not found", http.StatusNotFound) 365 - } 366 - var ( 367 - spindle = f.Spindle 368 - knot = f.Knot 369 - rkey = pipeline.Rkey 370 - ) 371 - 372 - if spindle == "" || knot == "" || rkey == "" { 373 - http.Error(w, "invalid repo info", http.StatusBadRequest) 374 - return 375 - } 376 - 377 - spindleClient, err := p.oauth.ServiceClient( 378 - r, 379 - oauth.WithService(f.Spindle), 380 - oauth.WithLxm(tangled.PipelineCancelPipelineNSID), 381 - oauth.WithDev(p.config.Core.Dev), 382 - oauth.WithTimeout(time.Second*30), // workflow cleanup usually takes time 383 - ) 384 - 385 - err = tangled.PipelineCancelPipeline( 386 - r.Context(), 387 - spindleClient, 388 - &tangled.PipelineCancelPipeline_Input{ 389 - Repo: string(f.RepoAt()), 390 - Pipeline: pipeline.AtUri().String(), 391 - Workflow: workflow, 392 - }, 393 - ) 394 - errorId := "workflow-error" 395 - if err != nil { 396 - l.Error("failed to cancel workflow", "err", err) 397 - p.pages.Notice(w, errorId, "Failed to cancel workflow") 398 - return 399 - } 400 - l.Debug("canceled pipeline", "uri", pipeline.AtUri()) 401 } 402 403 // either a message or an error
··· 4 "bytes" 5 "context" 6 "encoding/json" 7 "log/slog" 8 "net/http" 9 "strings" 10 "time" 11 12 "tangled.org/core/appview/config" 13 "tangled.org/core/appview/db" 14 "tangled.org/core/appview/oauth" 15 "tangled.org/core/appview/pages" 16 "tangled.org/core/appview/reporesolver" ··· 36 logger *slog.Logger 37 } 38 39 + func (p *Pipelines) Router() http.Handler { 40 r := chi.NewRouter() 41 r.Get("/", p.Index) 42 r.Get("/{pipeline}/workflow/{workflow}", p.Workflow) 43 r.Get("/{pipeline}/workflow/{workflow}/logs", p.Logs) 44 45 return r 46 } ··· 70 } 71 72 func (p *Pipelines) Index(w http.ResponseWriter, r *http.Request) { 73 + user := p.oauth.GetUser(r) 74 l := p.logger.With("handler", "Index") 75 76 f, err := p.repoResolver.Resolve(r) ··· 99 } 100 101 func (p *Pipelines) Workflow(w http.ResponseWriter, r *http.Request) { 102 + user := p.oauth.GetUser(r) 103 l := p.logger.With("handler", "Workflow") 104 105 f, err := p.repoResolver.Resolve(r) ··· 314 } 315 } 316 } 317 } 318 319 // either a message or an error
+2 -2
appview/pulls/opengraph.go
··· 18 "tangled.org/core/types" 19 ) 20 21 - func (s *Pulls) drawPullSummaryCard(pull *models.Pull, repo *models.Repo, commentCount int, diffStats types.DiffFileStat, filesChanged int) (*ogcard.Card, error) { 22 width, height := ogcard.DefaultSize() 23 mainCard, err := ogcard.NewCard(width, height) 24 if err != nil { ··· 284 commentCount := len(comments) 285 286 // Calculate diff stats from latest submission using patchutil 287 - var diffStats types.DiffFileStat 288 filesChanged := 0 289 if len(pull.Submissions) > 0 { 290 latestSubmission := pull.Submissions[len(pull.Submissions)-1]
··· 18 "tangled.org/core/types" 19 ) 20 21 + func (s *Pulls) drawPullSummaryCard(pull *models.Pull, repo *models.Repo, commentCount int, diffStats types.DiffStat, filesChanged int) (*ogcard.Card, error) { 22 width, height := ogcard.DefaultSize() 23 mainCard, err := ogcard.NewCard(width, height) 24 if err != nil { ··· 284 commentCount := len(comments) 285 286 // Calculate diff stats from latest submission using patchutil 287 + var diffStats types.DiffStat 288 filesChanged := 0 289 if len(pull.Submissions) > 0 { 290 latestSubmission := pull.Submissions[len(pull.Submissions)-1]
+164 -137
appview/pulls/pulls.go
··· 1 package pulls 2 3 import ( 4 - "bytes" 5 - "compress/gzip" 6 "context" 7 "database/sql" 8 "encoding/json" 9 "errors" 10 "fmt" 11 - "io" 12 "log" 13 "log/slog" 14 "net/http" ··· 97 func (s *Pulls) PullActions(w http.ResponseWriter, r *http.Request) { 98 switch r.Method { 99 case http.MethodGet: 100 - user := s.oauth.GetMultiAccountUser(r) 101 f, err := s.repoResolver.Resolve(r) 102 if err != nil { 103 log.Println("failed to get repo and knot", err) ··· 128 mergeCheckResponse := s.mergeCheck(r, f, pull, stack) 129 branchDeleteStatus := s.branchDeleteStatus(r, f, pull) 130 resubmitResult := pages.Unknown 131 - if user.Active.Did == pull.OwnerDid { 132 resubmitResult = s.resubmitCheck(r, f, pull, stack) 133 } 134 ··· 146 } 147 } 148 149 - func (s *Pulls) repoPullHelper(w http.ResponseWriter, r *http.Request, interdiff bool) { 150 - user := s.oauth.GetMultiAccountUser(r) 151 f, err := s.repoResolver.Resolve(r) 152 if err != nil { 153 log.Println("failed to get repo and knot", err) ··· 168 return 169 } 170 171 - roundId := chi.URLParam(r, "round") 172 - roundIdInt := pull.LastRoundNumber() 173 - if r, err := strconv.Atoi(roundId); err == nil { 174 - roundIdInt = r 175 - } 176 - if roundIdInt >= len(pull.Submissions) { 177 - http.Error(w, "bad round id", http.StatusBadRequest) 178 - log.Println("failed to parse round id", err) 179 - return 180 - } 181 - 182 - var diffOpts types.DiffOpts 183 - if d := r.URL.Query().Get("diff"); d == "split" { 184 - diffOpts.Split = true 185 - } 186 - 187 // can be nil if this pull is not stacked 188 stack, _ := r.Context().Value("stack").(models.Stack) 189 abandonedPulls, _ := r.Context().Value("abandonedPulls").([]*models.Pull) ··· 191 mergeCheckResponse := s.mergeCheck(r, f, pull, stack) 192 branchDeleteStatus := s.branchDeleteStatus(r, f, pull) 193 resubmitResult := pages.Unknown 194 - if user != nil && user.Active != nil && user.Active.Did == pull.OwnerDid { 195 resubmitResult = s.resubmitCheck(r, f, pull, stack) 196 } 197 ··· 233 234 userReactions := map[models.ReactionKind]bool{} 235 if user != nil { 236 - userReactions = db.GetReactionStatusMap(s.db, user.Active.Did, pull.AtUri()) 237 } 238 239 labelDefs, err := db.GetLabelDefinitions( ··· 252 defs[l.AtUri().String()] = &l 253 } 254 255 - patch := pull.Submissions[roundIdInt].CombinedPatch() 256 - var diff types.DiffRenderer 257 - diff = patchutil.AsNiceDiff(patch, pull.TargetBranch) 258 - 259 - if interdiff { 260 - currentPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt].CombinedPatch()) 261 - if err != nil { 262 - log.Println("failed to interdiff; current patch malformed") 263 - s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; current patch is invalid.") 264 - return 265 - } 266 - 267 - previousPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt-1].CombinedPatch()) 268 - if err != nil { 269 - log.Println("failed to interdiff; previous patch malformed") 270 - s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; previous patch is invalid.") 271 - return 272 - } 273 - 274 - diff = patchutil.Interdiff(previousPatch, currentPatch) 275 - } 276 - 277 s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 278 LoggedInUser: user, 279 RepoInfo: s.repoResolver.GetRepoInfo(r, user), ··· 285 MergeCheck: mergeCheckResponse, 286 ResubmitCheck: resubmitResult, 287 Pipelines: m, 288 - Diff: diff, 289 - DiffOpts: diffOpts, 290 - ActiveRound: roundIdInt, 291 - IsInterdiff: interdiff, 292 293 OrderedReactionKinds: models.OrderedReactionKinds, 294 Reactions: reactionMap, ··· 298 }) 299 } 300 301 - func (s *Pulls) RepoSinglePull(w http.ResponseWriter, r *http.Request) { 302 - s.repoPullHelper(w, r, false) 303 - } 304 - 305 func (s *Pulls) mergeCheck(r *http.Request, f *models.Repo, pull *models.Pull, stack models.Stack) types.MergeCheckResponse { 306 if pull.State == models.PullMerged { 307 return types.MergeCheckResponse{} ··· 374 return nil 375 } 376 377 - user := s.oauth.GetMultiAccountUser(r) 378 if user == nil { 379 return nil 380 } ··· 397 } 398 399 // user can only delete branch if they are a collaborator in the repo that the branch belongs to 400 - perms := s.enforcer.GetPermissionsInRepo(user.Active.Did, repo.Knot, repo.DidSlashRepo()) 401 if !slices.Contains(perms, "repo:push") { 402 return nil 403 } ··· 484 } 485 486 func (s *Pulls) RepoPullPatch(w http.ResponseWriter, r *http.Request) { 487 - s.repoPullHelper(w, r, false) 488 } 489 490 func (s *Pulls) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) { 491 - s.repoPullHelper(w, r, true) 492 } 493 494 func (s *Pulls) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) { ··· 514 func (s *Pulls) RepoPulls(w http.ResponseWriter, r *http.Request) { 515 l := s.logger.With("handler", "RepoPulls") 516 517 - user := s.oauth.GetMultiAccountUser(r) 518 params := r.URL.Query() 519 520 state := models.PullOpen ··· 545 546 keyword := params.Get("q") 547 548 - var pulls []*models.Pull 549 searchOpts := models.PullSearchOptions{ 550 Keyword: keyword, 551 RepoAt: f.RepoAt().String(), ··· 559 l.Error("failed to search for pulls", "err", err) 560 return 561 } 562 totalPulls = int(res.Total) 563 - l.Debug("searched pulls with indexer", "count", len(res.Hits)) 564 - 565 - pulls, err = db.GetPulls( 566 - s.db, 567 - orm.FilterIn("id", res.Hits), 568 - ) 569 - if err != nil { 570 - log.Println("failed to get pulls", err) 571 - s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.") 572 - return 573 - } 574 } else { 575 - pulls, err = db.GetPullsPaginated( 576 - s.db, 577 - page, 578 - orm.FilterEq("repo_at", f.RepoAt()), 579 - orm.FilterEq("state", searchOpts.State), 580 - ) 581 if err != nil { 582 - log.Println("failed to get pulls", err) 583 - s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.") 584 return 585 } 586 } 587 588 for _, p := range pulls { ··· 659 } 660 661 s.pages.RepoPulls(w, pages.RepoPullsParams{ 662 - LoggedInUser: s.oauth.GetMultiAccountUser(r), 663 RepoInfo: s.repoResolver.GetRepoInfo(r, user), 664 Pulls: pulls, 665 LabelDefs: defs, ··· 673 } 674 675 func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) { 676 - user := s.oauth.GetMultiAccountUser(r) 677 f, err := s.repoResolver.Resolve(r) 678 if err != nil { 679 log.Println("failed to get repo and knot", err) ··· 732 } 733 atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 734 Collection: tangled.RepoPullCommentNSID, 735 - Repo: user.Active.Did, 736 Rkey: tid.TID(), 737 Record: &lexutil.LexiconTypeDecoder{ 738 Val: &tangled.RepoPullComment{ ··· 749 } 750 751 comment := &models.PullComment{ 752 - OwnerDid: user.Active.Did, 753 RepoAt: f.RepoAt().String(), 754 PullId: pull.PullId, 755 Body: body, ··· 783 } 784 785 func (s *Pulls) NewPull(w http.ResponseWriter, r *http.Request) { 786 - user := s.oauth.GetMultiAccountUser(r) 787 f, err := s.repoResolver.Resolve(r) 788 if err != nil { 789 log.Println("failed to get repo and knot", err) ··· 851 } 852 853 // Determine PR type based on input parameters 854 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 855 isPushAllowed := roles.IsPushAllowed() 856 isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == "" 857 isForkBased := fromFork != "" && sourceBranch != "" ··· 951 w http.ResponseWriter, 952 r *http.Request, 953 repo *models.Repo, 954 - user *oauth.MultiAccountUser, 955 title, 956 body, 957 targetBranch, ··· 1008 s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked) 1009 } 1010 1011 - func (s *Pulls) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, user *oauth.MultiAccountUser, title, body, targetBranch, patch string, isStacked bool) { 1012 if err := s.validator.ValidatePatch(&patch); err != nil { 1013 s.logger.Error("patch validation failed", "err", err) 1014 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") ··· 1018 s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, "", "", nil, nil, isStacked) 1019 } 1020 1021 - func (s *Pulls) handleForkBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, user *oauth.MultiAccountUser, forkRepo string, title, body, targetBranch, sourceBranch string, isStacked bool) { 1022 repoString := strings.SplitN(forkRepo, "/", 2) 1023 forkOwnerDid := repoString[0] 1024 repoName := repoString[1] ··· 1127 w http.ResponseWriter, 1128 r *http.Request, 1129 repo *models.Repo, 1130 - user *oauth.MultiAccountUser, 1131 title, body, targetBranch string, 1132 patch string, 1133 combined string, ··· 1199 Title: title, 1200 Body: body, 1201 TargetBranch: targetBranch, 1202 - OwnerDid: user.Active.Did, 1203 RepoAt: repo.RepoAt(), 1204 Rkey: rkey, 1205 Mentions: mentions, ··· 1222 return 1223 } 1224 1225 - blob, err := comatproto.RepoUploadBlob(r.Context(), client, gz(patch)) 1226 if err != nil { 1227 log.Println("failed to upload patch", err) 1228 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") ··· 1231 1232 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1233 Collection: tangled.RepoPullNSID, 1234 - Repo: user.Active.Did, 1235 Rkey: rkey, 1236 Record: &lexutil.LexiconTypeDecoder{ 1237 Val: &tangled.RepoPull{ ··· 1268 w http.ResponseWriter, 1269 r *http.Request, 1270 repo *models.Repo, 1271 - user *oauth.MultiAccountUser, 1272 targetBranch string, 1273 patch string, 1274 sourceRev string, ··· 1316 // apply all record creations at once 1317 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1318 for _, p := range stack { 1319 - blob, err := comatproto.RepoUploadBlob(r.Context(), client, gz(p.LatestPatch())) 1320 if err != nil { 1321 log.Println("failed to upload patch blob", err) 1322 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") ··· 1336 }) 1337 } 1338 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 1339 - Repo: user.Active.Did, 1340 Writes: writes, 1341 }) 1342 if err != nil { ··· 1408 } 1409 1410 func (s *Pulls) PatchUploadFragment(w http.ResponseWriter, r *http.Request) { 1411 - user := s.oauth.GetMultiAccountUser(r) 1412 1413 s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{ 1414 RepoInfo: s.repoResolver.GetRepoInfo(r, user), ··· 1416 } 1417 1418 func (s *Pulls) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) { 1419 - user := s.oauth.GetMultiAccountUser(r) 1420 f, err := s.repoResolver.Resolve(r) 1421 if err != nil { 1422 log.Println("failed to get repo and knot", err) ··· 1471 } 1472 1473 func (s *Pulls) CompareForksFragment(w http.ResponseWriter, r *http.Request) { 1474 - user := s.oauth.GetMultiAccountUser(r) 1475 1476 - forks, err := db.GetForksByDid(s.db, user.Active.Did) 1477 if err != nil { 1478 log.Println("failed to get forks", err) 1479 return ··· 1487 } 1488 1489 func (s *Pulls) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) { 1490 - user := s.oauth.GetMultiAccountUser(r) 1491 1492 f, err := s.repoResolver.Resolve(r) 1493 if err != nil { ··· 1580 } 1581 1582 func (s *Pulls) ResubmitPull(w http.ResponseWriter, r *http.Request) { 1583 - user := s.oauth.GetMultiAccountUser(r) 1584 1585 pull, ok := r.Context().Value("pull").(*models.Pull) 1586 if !ok { ··· 1611 } 1612 1613 func (s *Pulls) resubmitPatch(w http.ResponseWriter, r *http.Request) { 1614 - user := s.oauth.GetMultiAccountUser(r) 1615 1616 pull, ok := r.Context().Value("pull").(*models.Pull) 1617 if !ok { ··· 1626 return 1627 } 1628 1629 - if user.Active.Did != pull.OwnerDid { 1630 log.Println("unauthorized user") 1631 w.WriteHeader(http.StatusUnauthorized) 1632 return ··· 1638 } 1639 1640 func (s *Pulls) resubmitBranch(w http.ResponseWriter, r *http.Request) { 1641 - user := s.oauth.GetMultiAccountUser(r) 1642 1643 pull, ok := r.Context().Value("pull").(*models.Pull) 1644 if !ok { ··· 1653 return 1654 } 1655 1656 - if user.Active.Did != pull.OwnerDid { 1657 log.Println("unauthorized user") 1658 w.WriteHeader(http.StatusUnauthorized) 1659 return 1660 } 1661 1662 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 1663 if !roles.IsPushAllowed() { 1664 log.Println("unauthorized user") 1665 w.WriteHeader(http.StatusUnauthorized) ··· 1703 } 1704 1705 func (s *Pulls) resubmitFork(w http.ResponseWriter, r *http.Request) { 1706 - user := s.oauth.GetMultiAccountUser(r) 1707 1708 pull, ok := r.Context().Value("pull").(*models.Pull) 1709 if !ok { ··· 1718 return 1719 } 1720 1721 - if user.Active.Did != pull.OwnerDid { 1722 log.Println("unauthorized user") 1723 w.WriteHeader(http.StatusUnauthorized) 1724 return ··· 1803 w http.ResponseWriter, 1804 r *http.Request, 1805 repo *models.Repo, 1806 - user *oauth.MultiAccountUser, 1807 pull *models.Pull, 1808 patch string, 1809 combined string, ··· 1859 return 1860 } 1861 1862 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Active.Did, pull.Rkey) 1863 if err != nil { 1864 // failed to get record 1865 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1866 return 1867 } 1868 1869 - blob, err := comatproto.RepoUploadBlob(r.Context(), client, gz(patch)) 1870 if err != nil { 1871 log.Println("failed to upload patch blob", err) 1872 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") ··· 1878 1879 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1880 Collection: tangled.RepoPullNSID, 1881 - Repo: user.Active.Did, 1882 Rkey: pull.Rkey, 1883 SwapRecord: ex.Cid, 1884 Record: &lexutil.LexiconTypeDecoder{ ··· 1905 w http.ResponseWriter, 1906 r *http.Request, 1907 repo *models.Repo, 1908 - user *oauth.MultiAccountUser, 1909 pull *models.Pull, 1910 patch string, 1911 stackId string, ··· 2008 return 2009 } 2010 2011 - blob, err := comatproto.RepoUploadBlob(r.Context(), client, gz(patch)) 2012 if err != nil { 2013 log.Println("failed to upload patch blob", err) 2014 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") ··· 2050 return 2051 } 2052 2053 - blob, err := comatproto.RepoUploadBlob(r.Context(), client, gz(patch)) 2054 if err != nil { 2055 log.Println("failed to upload patch blob", err) 2056 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") ··· 2095 } 2096 2097 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 2098 - Repo: user.Active.Did, 2099 Writes: writes, 2100 }) 2101 if err != nil { ··· 2109 } 2110 2111 func (s *Pulls) MergePull(w http.ResponseWriter, r *http.Request) { 2112 - user := s.oauth.GetMultiAccountUser(r) 2113 f, err := s.repoResolver.Resolve(r) 2114 if err != nil { 2115 log.Println("failed to resolve repo:", err) ··· 2220 2221 // notify about the pull merge 2222 for _, p := range pullsToMerge { 2223 - s.notifier.NewPullState(r.Context(), syntax.DID(user.Active.Did), p) 2224 } 2225 2226 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) ··· 2228 } 2229 2230 func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) { 2231 - user := s.oauth.GetMultiAccountUser(r) 2232 2233 f, err := s.repoResolver.Resolve(r) 2234 if err != nil { ··· 2244 } 2245 2246 // auth filter: only owner or collaborators can close 2247 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 2248 isOwner := roles.IsOwner() 2249 isCollaborator := roles.IsCollaborator() 2250 - isPullAuthor := user.Active.Did == pull.OwnerDid 2251 isCloseAllowed := isOwner || isCollaborator || isPullAuthor 2252 if !isCloseAllowed { 2253 log.Println("failed to close pull") ··· 2293 } 2294 2295 for _, p := range pullsToClose { 2296 - s.notifier.NewPullState(r.Context(), syntax.DID(user.Active.Did), p) 2297 } 2298 2299 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) ··· 2301 } 2302 2303 func (s *Pulls) ReopenPull(w http.ResponseWriter, r *http.Request) { 2304 - user := s.oauth.GetMultiAccountUser(r) 2305 2306 f, err := s.repoResolver.Resolve(r) 2307 if err != nil { ··· 2318 } 2319 2320 // auth filter: only owner or collaborators can close 2321 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 2322 isOwner := roles.IsOwner() 2323 isCollaborator := roles.IsCollaborator() 2324 - isPullAuthor := user.Active.Did == pull.OwnerDid 2325 isCloseAllowed := isOwner || isCollaborator || isPullAuthor 2326 if !isCloseAllowed { 2327 log.Println("failed to close pull") ··· 2367 } 2368 2369 for _, p := range pullsToReopen { 2370 - s.notifier.NewPullState(r.Context(), syntax.DID(user.Active.Did), p) 2371 } 2372 2373 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 2374 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId)) 2375 } 2376 2377 - func (s *Pulls) newStack(ctx context.Context, repo *models.Repo, user *oauth.MultiAccountUser, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) { 2378 formatPatches, err := patchutil.ExtractPatches(patch) 2379 if err != nil { 2380 return nil, fmt.Errorf("Failed to extract patches: %v", err) ··· 2410 Title: title, 2411 Body: body, 2412 TargetBranch: targetBranch, 2413 - OwnerDid: user.Active.Did, 2414 RepoAt: repo.RepoAt(), 2415 Rkey: rkey, 2416 Mentions: mentions, ··· 2433 2434 return stack, nil 2435 } 2436 - 2437 - func gz(s string) io.Reader { 2438 - var b bytes.Buffer 2439 - w := gzip.NewWriter(&b) 2440 - w.Write([]byte(s)) 2441 - w.Close() 2442 - return &b 2443 - }
··· 1 package pulls 2 3 import ( 4 "context" 5 "database/sql" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "log" 10 "log/slog" 11 "net/http" ··· 94 func (s *Pulls) PullActions(w http.ResponseWriter, r *http.Request) { 95 switch r.Method { 96 case http.MethodGet: 97 + user := s.oauth.GetUser(r) 98 f, err := s.repoResolver.Resolve(r) 99 if err != nil { 100 log.Println("failed to get repo and knot", err) ··· 125 mergeCheckResponse := s.mergeCheck(r, f, pull, stack) 126 branchDeleteStatus := s.branchDeleteStatus(r, f, pull) 127 resubmitResult := pages.Unknown 128 + if user.Did == pull.OwnerDid { 129 resubmitResult = s.resubmitCheck(r, f, pull, stack) 130 } 131 ··· 143 } 144 } 145 146 + func (s *Pulls) RepoSinglePull(w http.ResponseWriter, r *http.Request) { 147 + user := s.oauth.GetUser(r) 148 f, err := s.repoResolver.Resolve(r) 149 if err != nil { 150 log.Println("failed to get repo and knot", err) ··· 165 return 166 } 167 168 // can be nil if this pull is not stacked 169 stack, _ := r.Context().Value("stack").(models.Stack) 170 abandonedPulls, _ := r.Context().Value("abandonedPulls").([]*models.Pull) ··· 172 mergeCheckResponse := s.mergeCheck(r, f, pull, stack) 173 branchDeleteStatus := s.branchDeleteStatus(r, f, pull) 174 resubmitResult := pages.Unknown 175 + if user != nil && user.Did == pull.OwnerDid { 176 resubmitResult = s.resubmitCheck(r, f, pull, stack) 177 } 178 ··· 214 215 userReactions := map[models.ReactionKind]bool{} 216 if user != nil { 217 + userReactions = db.GetReactionStatusMap(s.db, user.Did, pull.AtUri()) 218 } 219 220 labelDefs, err := db.GetLabelDefinitions( ··· 233 defs[l.AtUri().String()] = &l 234 } 235 236 s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 237 LoggedInUser: user, 238 RepoInfo: s.repoResolver.GetRepoInfo(r, user), ··· 244 MergeCheck: mergeCheckResponse, 245 ResubmitCheck: resubmitResult, 246 Pipelines: m, 247 248 OrderedReactionKinds: models.OrderedReactionKinds, 249 Reactions: reactionMap, ··· 253 }) 254 } 255 256 func (s *Pulls) mergeCheck(r *http.Request, f *models.Repo, pull *models.Pull, stack models.Stack) types.MergeCheckResponse { 257 if pull.State == models.PullMerged { 258 return types.MergeCheckResponse{} ··· 325 return nil 326 } 327 328 + user := s.oauth.GetUser(r) 329 if user == nil { 330 return nil 331 } ··· 348 } 349 350 // user can only delete branch if they are a collaborator in the repo that the branch belongs to 351 + perms := s.enforcer.GetPermissionsInRepo(user.Did, repo.Knot, repo.DidSlashRepo()) 352 if !slices.Contains(perms, "repo:push") { 353 return nil 354 } ··· 435 } 436 437 func (s *Pulls) RepoPullPatch(w http.ResponseWriter, r *http.Request) { 438 + user := s.oauth.GetUser(r) 439 + 440 + var diffOpts types.DiffOpts 441 + if d := r.URL.Query().Get("diff"); d == "split" { 442 + diffOpts.Split = true 443 + } 444 + 445 + pull, ok := r.Context().Value("pull").(*models.Pull) 446 + if !ok { 447 + log.Println("failed to get pull") 448 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 449 + return 450 + } 451 + 452 + stack, _ := r.Context().Value("stack").(models.Stack) 453 + 454 + roundId := chi.URLParam(r, "round") 455 + roundIdInt, err := strconv.Atoi(roundId) 456 + if err != nil || roundIdInt >= len(pull.Submissions) { 457 + http.Error(w, "bad round id", http.StatusBadRequest) 458 + log.Println("failed to parse round id", err) 459 + return 460 + } 461 + 462 + patch := pull.Submissions[roundIdInt].CombinedPatch() 463 + diff := patchutil.AsNiceDiff(patch, pull.TargetBranch) 464 + 465 + s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{ 466 + LoggedInUser: user, 467 + RepoInfo: s.repoResolver.GetRepoInfo(r, user), 468 + Pull: pull, 469 + Stack: stack, 470 + Round: roundIdInt, 471 + Submission: pull.Submissions[roundIdInt], 472 + Diff: &diff, 473 + DiffOpts: diffOpts, 474 + }) 475 + 476 } 477 478 func (s *Pulls) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) { 479 + user := s.oauth.GetUser(r) 480 + 481 + var diffOpts types.DiffOpts 482 + if d := r.URL.Query().Get("diff"); d == "split" { 483 + diffOpts.Split = true 484 + } 485 + 486 + pull, ok := r.Context().Value("pull").(*models.Pull) 487 + if !ok { 488 + log.Println("failed to get pull") 489 + s.pages.Notice(w, "pull-error", "Failed to get pull.") 490 + return 491 + } 492 + 493 + roundId := chi.URLParam(r, "round") 494 + roundIdInt, err := strconv.Atoi(roundId) 495 + if err != nil || roundIdInt >= len(pull.Submissions) { 496 + http.Error(w, "bad round id", http.StatusBadRequest) 497 + log.Println("failed to parse round id", err) 498 + return 499 + } 500 + 501 + if roundIdInt == 0 { 502 + http.Error(w, "bad round id", http.StatusBadRequest) 503 + log.Println("cannot interdiff initial submission") 504 + return 505 + } 506 + 507 + currentPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt].CombinedPatch()) 508 + if err != nil { 509 + log.Println("failed to interdiff; current patch malformed") 510 + s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; current patch is invalid.") 511 + return 512 + } 513 + 514 + previousPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt-1].CombinedPatch()) 515 + if err != nil { 516 + log.Println("failed to interdiff; previous patch malformed") 517 + s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; previous patch is invalid.") 518 + return 519 + } 520 + 521 + interdiff := patchutil.Interdiff(previousPatch, currentPatch) 522 + 523 + s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{ 524 + LoggedInUser: s.oauth.GetUser(r), 525 + RepoInfo: s.repoResolver.GetRepoInfo(r, user), 526 + Pull: pull, 527 + Round: roundIdInt, 528 + Interdiff: interdiff, 529 + DiffOpts: diffOpts, 530 + }) 531 } 532 533 func (s *Pulls) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) { ··· 553 func (s *Pulls) RepoPulls(w http.ResponseWriter, r *http.Request) { 554 l := s.logger.With("handler", "RepoPulls") 555 556 + user := s.oauth.GetUser(r) 557 params := r.URL.Query() 558 559 state := models.PullOpen ··· 584 585 keyword := params.Get("q") 586 587 + var ids []int64 588 searchOpts := models.PullSearchOptions{ 589 Keyword: keyword, 590 RepoAt: f.RepoAt().String(), ··· 598 l.Error("failed to search for pulls", "err", err) 599 return 600 } 601 + ids = res.Hits 602 totalPulls = int(res.Total) 603 + l.Debug("searched pulls with indexer", "count", len(ids)) 604 } else { 605 + ids, err = db.GetPullIDs(s.db, searchOpts) 606 if err != nil { 607 + l.Error("failed to get all pull ids", "err", err) 608 return 609 } 610 + l.Debug("indexed all pulls from the db", "count", len(ids)) 611 + } 612 + 613 + pulls, err := db.GetPulls( 614 + s.db, 615 + orm.FilterIn("id", ids), 616 + ) 617 + if err != nil { 618 + log.Println("failed to get pulls", err) 619 + s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.") 620 + return 621 } 622 623 for _, p := range pulls { ··· 694 } 695 696 s.pages.RepoPulls(w, pages.RepoPullsParams{ 697 + LoggedInUser: s.oauth.GetUser(r), 698 RepoInfo: s.repoResolver.GetRepoInfo(r, user), 699 Pulls: pulls, 700 LabelDefs: defs, ··· 708 } 709 710 func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) { 711 + user := s.oauth.GetUser(r) 712 f, err := s.repoResolver.Resolve(r) 713 if err != nil { 714 log.Println("failed to get repo and knot", err) ··· 767 } 768 atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 769 Collection: tangled.RepoPullCommentNSID, 770 + Repo: user.Did, 771 Rkey: tid.TID(), 772 Record: &lexutil.LexiconTypeDecoder{ 773 Val: &tangled.RepoPullComment{ ··· 784 } 785 786 comment := &models.PullComment{ 787 + OwnerDid: user.Did, 788 RepoAt: f.RepoAt().String(), 789 PullId: pull.PullId, 790 Body: body, ··· 818 } 819 820 func (s *Pulls) NewPull(w http.ResponseWriter, r *http.Request) { 821 + user := s.oauth.GetUser(r) 822 f, err := s.repoResolver.Resolve(r) 823 if err != nil { 824 log.Println("failed to get repo and knot", err) ··· 886 } 887 888 // Determine PR type based on input parameters 889 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())} 890 isPushAllowed := roles.IsPushAllowed() 891 isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == "" 892 isForkBased := fromFork != "" && sourceBranch != "" ··· 986 w http.ResponseWriter, 987 r *http.Request, 988 repo *models.Repo, 989 + user *oauth.User, 990 title, 991 body, 992 targetBranch, ··· 1043 s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked) 1044 } 1045 1046 + func (s *Pulls) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, user *oauth.User, title, body, targetBranch, patch string, isStacked bool) { 1047 if err := s.validator.ValidatePatch(&patch); err != nil { 1048 s.logger.Error("patch validation failed", "err", err) 1049 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") ··· 1053 s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, "", "", nil, nil, isStacked) 1054 } 1055 1056 + func (s *Pulls) handleForkBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, user *oauth.User, forkRepo string, title, body, targetBranch, sourceBranch string, isStacked bool) { 1057 repoString := strings.SplitN(forkRepo, "/", 2) 1058 forkOwnerDid := repoString[0] 1059 repoName := repoString[1] ··· 1162 w http.ResponseWriter, 1163 r *http.Request, 1164 repo *models.Repo, 1165 + user *oauth.User, 1166 title, body, targetBranch string, 1167 patch string, 1168 combined string, ··· 1234 Title: title, 1235 Body: body, 1236 TargetBranch: targetBranch, 1237 + OwnerDid: user.Did, 1238 RepoAt: repo.RepoAt(), 1239 Rkey: rkey, 1240 Mentions: mentions, ··· 1257 return 1258 } 1259 1260 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 1261 if err != nil { 1262 log.Println("failed to upload patch", err) 1263 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") ··· 1266 1267 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1268 Collection: tangled.RepoPullNSID, 1269 + Repo: user.Did, 1270 Rkey: rkey, 1271 Record: &lexutil.LexiconTypeDecoder{ 1272 Val: &tangled.RepoPull{ ··· 1303 w http.ResponseWriter, 1304 r *http.Request, 1305 repo *models.Repo, 1306 + user *oauth.User, 1307 targetBranch string, 1308 patch string, 1309 sourceRev string, ··· 1351 // apply all record creations at once 1352 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1353 for _, p := range stack { 1354 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(p.LatestPatch())) 1355 if err != nil { 1356 log.Println("failed to upload patch blob", err) 1357 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") ··· 1371 }) 1372 } 1373 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 1374 + Repo: user.Did, 1375 Writes: writes, 1376 }) 1377 if err != nil { ··· 1443 } 1444 1445 func (s *Pulls) PatchUploadFragment(w http.ResponseWriter, r *http.Request) { 1446 + user := s.oauth.GetUser(r) 1447 1448 s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{ 1449 RepoInfo: s.repoResolver.GetRepoInfo(r, user), ··· 1451 } 1452 1453 func (s *Pulls) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) { 1454 + user := s.oauth.GetUser(r) 1455 f, err := s.repoResolver.Resolve(r) 1456 if err != nil { 1457 log.Println("failed to get repo and knot", err) ··· 1506 } 1507 1508 func (s *Pulls) CompareForksFragment(w http.ResponseWriter, r *http.Request) { 1509 + user := s.oauth.GetUser(r) 1510 1511 + forks, err := db.GetForksByDid(s.db, user.Did) 1512 if err != nil { 1513 log.Println("failed to get forks", err) 1514 return ··· 1522 } 1523 1524 func (s *Pulls) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) { 1525 + user := s.oauth.GetUser(r) 1526 1527 f, err := s.repoResolver.Resolve(r) 1528 if err != nil { ··· 1615 } 1616 1617 func (s *Pulls) ResubmitPull(w http.ResponseWriter, r *http.Request) { 1618 + user := s.oauth.GetUser(r) 1619 1620 pull, ok := r.Context().Value("pull").(*models.Pull) 1621 if !ok { ··· 1646 } 1647 1648 func (s *Pulls) resubmitPatch(w http.ResponseWriter, r *http.Request) { 1649 + user := s.oauth.GetUser(r) 1650 1651 pull, ok := r.Context().Value("pull").(*models.Pull) 1652 if !ok { ··· 1661 return 1662 } 1663 1664 + if user.Did != pull.OwnerDid { 1665 log.Println("unauthorized user") 1666 w.WriteHeader(http.StatusUnauthorized) 1667 return ··· 1673 } 1674 1675 func (s *Pulls) resubmitBranch(w http.ResponseWriter, r *http.Request) { 1676 + user := s.oauth.GetUser(r) 1677 1678 pull, ok := r.Context().Value("pull").(*models.Pull) 1679 if !ok { ··· 1688 return 1689 } 1690 1691 + if user.Did != pull.OwnerDid { 1692 log.Println("unauthorized user") 1693 w.WriteHeader(http.StatusUnauthorized) 1694 return 1695 } 1696 1697 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())} 1698 if !roles.IsPushAllowed() { 1699 log.Println("unauthorized user") 1700 w.WriteHeader(http.StatusUnauthorized) ··· 1738 } 1739 1740 func (s *Pulls) resubmitFork(w http.ResponseWriter, r *http.Request) { 1741 + user := s.oauth.GetUser(r) 1742 1743 pull, ok := r.Context().Value("pull").(*models.Pull) 1744 if !ok { ··· 1753 return 1754 } 1755 1756 + if user.Did != pull.OwnerDid { 1757 log.Println("unauthorized user") 1758 w.WriteHeader(http.StatusUnauthorized) 1759 return ··· 1838 w http.ResponseWriter, 1839 r *http.Request, 1840 repo *models.Repo, 1841 + user *oauth.User, 1842 pull *models.Pull, 1843 patch string, 1844 combined string, ··· 1894 return 1895 } 1896 1897 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1898 if err != nil { 1899 // failed to get record 1900 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1901 return 1902 } 1903 1904 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 1905 if err != nil { 1906 log.Println("failed to upload patch blob", err) 1907 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") ··· 1913 1914 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1915 Collection: tangled.RepoPullNSID, 1916 + Repo: user.Did, 1917 Rkey: pull.Rkey, 1918 SwapRecord: ex.Cid, 1919 Record: &lexutil.LexiconTypeDecoder{ ··· 1940 w http.ResponseWriter, 1941 r *http.Request, 1942 repo *models.Repo, 1943 + user *oauth.User, 1944 pull *models.Pull, 1945 patch string, 1946 stackId string, ··· 2043 return 2044 } 2045 2046 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 2047 if err != nil { 2048 log.Println("failed to upload patch blob", err) 2049 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") ··· 2085 return 2086 } 2087 2088 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 2089 if err != nil { 2090 log.Println("failed to upload patch blob", err) 2091 s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") ··· 2130 } 2131 2132 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 2133 + Repo: user.Did, 2134 Writes: writes, 2135 }) 2136 if err != nil { ··· 2144 } 2145 2146 func (s *Pulls) MergePull(w http.ResponseWriter, r *http.Request) { 2147 + user := s.oauth.GetUser(r) 2148 f, err := s.repoResolver.Resolve(r) 2149 if err != nil { 2150 log.Println("failed to resolve repo:", err) ··· 2255 2256 // notify about the pull merge 2257 for _, p := range pullsToMerge { 2258 + s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p) 2259 } 2260 2261 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) ··· 2263 } 2264 2265 func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) { 2266 + user := s.oauth.GetUser(r) 2267 2268 f, err := s.repoResolver.Resolve(r) 2269 if err != nil { ··· 2279 } 2280 2281 // auth filter: only owner or collaborators can close 2282 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())} 2283 isOwner := roles.IsOwner() 2284 isCollaborator := roles.IsCollaborator() 2285 + isPullAuthor := user.Did == pull.OwnerDid 2286 isCloseAllowed := isOwner || isCollaborator || isPullAuthor 2287 if !isCloseAllowed { 2288 log.Println("failed to close pull") ··· 2328 } 2329 2330 for _, p := range pullsToClose { 2331 + s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p) 2332 } 2333 2334 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) ··· 2336 } 2337 2338 func (s *Pulls) ReopenPull(w http.ResponseWriter, r *http.Request) { 2339 + user := s.oauth.GetUser(r) 2340 2341 f, err := s.repoResolver.Resolve(r) 2342 if err != nil { ··· 2353 } 2354 2355 // auth filter: only owner or collaborators can close 2356 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())} 2357 isOwner := roles.IsOwner() 2358 isCollaborator := roles.IsCollaborator() 2359 + isPullAuthor := user.Did == pull.OwnerDid 2360 isCloseAllowed := isOwner || isCollaborator || isPullAuthor 2361 if !isCloseAllowed { 2362 log.Println("failed to close pull") ··· 2402 } 2403 2404 for _, p := range pullsToReopen { 2405 + s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p) 2406 } 2407 2408 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 2409 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId)) 2410 } 2411 2412 + func (s *Pulls) newStack(ctx context.Context, repo *models.Repo, user *oauth.User, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) { 2413 formatPatches, err := patchutil.ExtractPatches(patch) 2414 if err != nil { 2415 return nil, fmt.Errorf("Failed to extract patches: %v", err) ··· 2445 Title: title, 2446 Body: body, 2447 TargetBranch: targetBranch, 2448 + OwnerDid: user.Did, 2449 RepoAt: repo.RepoAt(), 2450 Rkey: rkey, 2451 Mentions: mentions, ··· 2468 2469 return stack, nil 2470 }
+6 -6
appview/repo/artifact.go
··· 30 31 // TODO: proper statuses here on early exit 32 func (rp *Repo) AttachArtifact(w http.ResponseWriter, r *http.Request) { 33 - user := rp.oauth.GetMultiAccountUser(r) 34 tagParam := chi.URLParam(r, "tag") 35 f, err := rp.repoResolver.Resolve(r) 36 if err != nil { ··· 75 76 putRecordResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 77 Collection: tangled.RepoArtifactNSID, 78 - Repo: user.Active.Did, 79 Rkey: rkey, 80 Record: &lexutil.LexiconTypeDecoder{ 81 Val: &tangled.RepoArtifact{ ··· 104 defer tx.Rollback() 105 106 artifact := models.Artifact{ 107 - Did: user.Active.Did, 108 Rkey: rkey, 109 RepoAt: f.RepoAt(), 110 Tag: tag.Tag.Hash, ··· 220 221 // TODO: proper statuses here on early exit 222 func (rp *Repo) DeleteArtifact(w http.ResponseWriter, r *http.Request) { 223 - user := rp.oauth.GetMultiAccountUser(r) 224 tagParam := chi.URLParam(r, "tag") 225 filename := chi.URLParam(r, "file") 226 f, err := rp.repoResolver.Resolve(r) ··· 251 252 artifact := artifacts[0] 253 254 - if user.Active.Did != artifact.Did { 255 log.Println("user not authorized to delete artifact", err) 256 rp.pages.Notice(w, "remove", "Unauthorized deletion of artifact.") 257 return ··· 259 260 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 261 Collection: tangled.RepoArtifactNSID, 262 - Repo: user.Active.Did, 263 Rkey: artifact.Rkey, 264 }) 265 if err != nil {
··· 30 31 // TODO: proper statuses here on early exit 32 func (rp *Repo) AttachArtifact(w http.ResponseWriter, r *http.Request) { 33 + user := rp.oauth.GetUser(r) 34 tagParam := chi.URLParam(r, "tag") 35 f, err := rp.repoResolver.Resolve(r) 36 if err != nil { ··· 75 76 putRecordResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 77 Collection: tangled.RepoArtifactNSID, 78 + Repo: user.Did, 79 Rkey: rkey, 80 Record: &lexutil.LexiconTypeDecoder{ 81 Val: &tangled.RepoArtifact{ ··· 104 defer tx.Rollback() 105 106 artifact := models.Artifact{ 107 + Did: user.Did, 108 Rkey: rkey, 109 RepoAt: f.RepoAt(), 110 Tag: tag.Tag.Hash, ··· 220 221 // TODO: proper statuses here on early exit 222 func (rp *Repo) DeleteArtifact(w http.ResponseWriter, r *http.Request) { 223 + user := rp.oauth.GetUser(r) 224 tagParam := chi.URLParam(r, "tag") 225 filename := chi.URLParam(r, "file") 226 f, err := rp.repoResolver.Resolve(r) ··· 251 252 artifact := artifacts[0] 253 254 + if user.Did != artifact.Did { 255 log.Println("user not authorized to delete artifact", err) 256 rp.pages.Notice(w, "remove", "Unauthorized deletion of artifact.") 257 return ··· 259 260 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 261 Collection: tangled.RepoArtifactNSID, 262 + Repo: user.Did, 263 Rkey: artifact.Rkey, 264 }) 265 if err != nil {
+1 -1
appview/repo/blob.go
··· 76 // Create the blob view 77 blobView := NewBlobView(resp, rp.config, f, ref, filePath, r.URL.Query()) 78 79 - user := rp.oauth.GetMultiAccountUser(r) 80 81 rp.pages.RepoBlob(w, pages.RepoBlobParams{ 82 LoggedInUser: user,
··· 76 // Create the blob view 77 blobView := NewBlobView(resp, rp.config, f, ref, filePath, r.URL.Query()) 78 79 + user := rp.oauth.GetUser(r) 80 81 rp.pages.RepoBlob(w, pages.RepoBlobParams{ 82 LoggedInUser: user,
+1 -1
appview/repo/branches.go
··· 43 return 44 } 45 sortBranches(result.Branches) 46 - user := rp.oauth.GetMultiAccountUser(r) 47 rp.pages.RepoBranches(w, pages.RepoBranchesParams{ 48 LoggedInUser: user, 49 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
··· 43 return 44 } 45 sortBranches(result.Branches) 46 + user := rp.oauth.GetUser(r) 47 rp.pages.RepoBranches(w, pages.RepoBranchesParams{ 48 LoggedInUser: user, 49 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
+2 -2
appview/repo/compare.go
··· 20 func (rp *Repo) CompareNew(w http.ResponseWriter, r *http.Request) { 21 l := rp.logger.With("handler", "RepoCompareNew") 22 23 - user := rp.oauth.GetMultiAccountUser(r) 24 f, err := rp.repoResolver.Resolve(r) 25 if err != nil { 26 l.Error("failed to get repo and knot", "err", err) ··· 101 func (rp *Repo) Compare(w http.ResponseWriter, r *http.Request) { 102 l := rp.logger.With("handler", "RepoCompare") 103 104 - user := rp.oauth.GetMultiAccountUser(r) 105 f, err := rp.repoResolver.Resolve(r) 106 if err != nil { 107 l.Error("failed to get repo and knot", "err", err)
··· 20 func (rp *Repo) CompareNew(w http.ResponseWriter, r *http.Request) { 21 l := rp.logger.With("handler", "RepoCompareNew") 22 23 + user := rp.oauth.GetUser(r) 24 f, err := rp.repoResolver.Resolve(r) 25 if err != nil { 26 l.Error("failed to get repo and knot", "err", err) ··· 101 func (rp *Repo) Compare(w http.ResponseWriter, r *http.Request) { 102 l := rp.logger.With("handler", "RepoCompare") 103 104 + user := rp.oauth.GetUser(r) 105 f, err := rp.repoResolver.Resolve(r) 106 if err != nil { 107 l.Error("failed to get repo and knot", "err", err)
+3 -3
appview/repo/feed.go
··· 19 ) 20 21 func (rp *Repo) getRepoFeed(ctx context.Context, repo *models.Repo, ownerSlashRepo string) (*feeds.Feed, error) { 22 - feedPagePerType := pagination.Page{Limit: 100} 23 24 - pulls, err := db.GetPullsPaginated(rp.db, feedPagePerType, orm.FilterEq("repo_at", repo.RepoAt())) 25 if err != nil { 26 return nil, err 27 } 28 29 issues, err := db.GetIssuesPaginated( 30 rp.db, 31 - feedPagePerType, 32 orm.FilterEq("repo_at", repo.RepoAt()), 33 ) 34 if err != nil {
··· 19 ) 20 21 func (rp *Repo) getRepoFeed(ctx context.Context, repo *models.Repo, ownerSlashRepo string) (*feeds.Feed, error) { 22 + const feedLimitPerType = 100 23 24 + pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, orm.FilterEq("repo_at", repo.RepoAt())) 25 if err != nil { 26 return nil, err 27 } 28 29 issues, err := db.GetIssuesPaginated( 30 rp.db, 31 + pagination.Page{Limit: feedLimitPerType}, 32 orm.FilterEq("repo_at", repo.RepoAt()), 33 ) 34 if err != nil {
+1 -1
appview/repo/index.go
··· 51 Host: host, 52 } 53 54 - user := rp.oauth.GetMultiAccountUser(r) 55 56 // Build index response from multiple XRPC calls 57 result, err := rp.buildIndexResponse(r.Context(), xrpcc, f, ref)
··· 51 Host: host, 52 } 53 54 + user := rp.oauth.GetUser(r) 55 56 // Build index response from multiple XRPC calls 57 result, err := rp.buildIndexResponse(r.Context(), xrpcc, f, ref)
+2 -2
appview/repo/log.go
··· 109 } 110 } 111 112 - user := rp.oauth.GetMultiAccountUser(r) 113 114 emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(xrpcResp.Commits), true) 115 if err != nil { ··· 197 l.Error("failed to GetVerifiedCommits", "err", err) 198 } 199 200 - user := rp.oauth.GetMultiAccountUser(r) 201 pipelines, err := getPipelineStatuses(rp.db, f, []string{result.Diff.Commit.This}) 202 if err != nil { 203 l.Error("failed to getPipelineStatuses", "err", err)
··· 109 } 110 } 111 112 + user := rp.oauth.GetUser(r) 113 114 emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(xrpcResp.Commits), true) 115 if err != nil { ··· 197 l.Error("failed to GetVerifiedCommits", "err", err) 198 } 199 200 + user := rp.oauth.GetUser(r) 201 pipelines, err := getPipelineStatuses(rp.db, f, []string{result.Diff.Commit.This}) 202 if err != nil { 203 l.Error("failed to getPipelineStatuses", "err", err)
+34 -34
appview/repo/repo.go
··· 81 82 // modify the spindle configured for this repo 83 func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) { 84 - user := rp.oauth.GetMultiAccountUser(r) 85 l := rp.logger.With("handler", "EditSpindle") 86 - l = l.With("did", user.Active.Did) 87 88 errorId := "operation-error" 89 fail := func(msg string, err error) { ··· 107 108 if !removingSpindle { 109 // ensure that this is a valid spindle for this user 110 - validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Active.Did) 111 if err != nil { 112 fail("Failed to find spindles. Try again later.", err) 113 return ··· 168 } 169 170 func (rp *Repo) AddLabelDef(w http.ResponseWriter, r *http.Request) { 171 - user := rp.oauth.GetMultiAccountUser(r) 172 l := rp.logger.With("handler", "AddLabel") 173 - l = l.With("did", user.Active.Did) 174 175 f, err := rp.repoResolver.Resolve(r) 176 if err != nil { ··· 216 } 217 218 label := models.LabelDefinition{ 219 - Did: user.Active.Did, 220 Rkey: tid.TID(), 221 Name: name, 222 ValueType: valueType, ··· 327 } 328 329 func (rp *Repo) DeleteLabelDef(w http.ResponseWriter, r *http.Request) { 330 - user := rp.oauth.GetMultiAccountUser(r) 331 l := rp.logger.With("handler", "DeleteLabel") 332 - l = l.With("did", user.Active.Did) 333 334 f, err := rp.repoResolver.Resolve(r) 335 if err != nil { ··· 435 } 436 437 func (rp *Repo) SubscribeLabel(w http.ResponseWriter, r *http.Request) { 438 - user := rp.oauth.GetMultiAccountUser(r) 439 l := rp.logger.With("handler", "SubscribeLabel") 440 - l = l.With("did", user.Active.Did) 441 442 f, err := rp.repoResolver.Resolve(r) 443 if err != nil { ··· 521 } 522 523 func (rp *Repo) UnsubscribeLabel(w http.ResponseWriter, r *http.Request) { 524 - user := rp.oauth.GetMultiAccountUser(r) 525 l := rp.logger.With("handler", "UnsubscribeLabel") 526 - l = l.With("did", user.Active.Did) 527 528 f, err := rp.repoResolver.Resolve(r) 529 if err != nil { ··· 633 } 634 state := states[subject] 635 636 - user := rp.oauth.GetMultiAccountUser(r) 637 rp.pages.LabelPanel(w, pages.LabelPanelParams{ 638 LoggedInUser: user, 639 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), ··· 681 } 682 state := states[subject] 683 684 - user := rp.oauth.GetMultiAccountUser(r) 685 rp.pages.EditLabelPanel(w, pages.EditLabelPanelParams{ 686 LoggedInUser: user, 687 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), ··· 692 } 693 694 func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) { 695 - user := rp.oauth.GetMultiAccountUser(r) 696 l := rp.logger.With("handler", "AddCollaborator") 697 - l = l.With("did", user.Active.Did) 698 699 f, err := rp.repoResolver.Resolve(r) 700 if err != nil { ··· 723 return 724 } 725 726 - if collaboratorIdent.DID.String() == user.Active.Did { 727 fail("You seem to be adding yourself as a collaborator.", nil) 728 return 729 } ··· 738 } 739 740 // emit a record 741 - currentUser := rp.oauth.GetMultiAccountUser(r) 742 rkey := tid.TID() 743 createdAt := time.Now() 744 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 745 Collection: tangled.RepoCollaboratorNSID, 746 - Repo: currentUser.Active.Did, 747 Rkey: rkey, 748 Record: &lexutil.LexiconTypeDecoder{ 749 Val: &tangled.RepoCollaborator{ ··· 792 } 793 794 err = db.AddCollaborator(tx, models.Collaborator{ 795 - Did: syntax.DID(currentUser.Active.Did), 796 Rkey: rkey, 797 SubjectDid: collaboratorIdent.DID, 798 RepoAt: f.RepoAt(), ··· 822 } 823 824 func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) { 825 - user := rp.oauth.GetMultiAccountUser(r) 826 l := rp.logger.With("handler", "DeleteRepo") 827 828 noticeId := "operation-error" ··· 840 } 841 _, err = comatproto.RepoDeleteRecord(r.Context(), atpClient, &comatproto.RepoDeleteRecord_Input{ 842 Collection: tangled.RepoNSID, 843 - Repo: user.Active.Did, 844 Rkey: f.Rkey, 845 }) 846 if err != nil { ··· 940 ref := chi.URLParam(r, "ref") 941 ref, _ = url.PathUnescape(ref) 942 943 - user := rp.oauth.GetMultiAccountUser(r) 944 f, err := rp.repoResolver.Resolve(r) 945 if err != nil { 946 l.Error("failed to resolve source repo", "err", err) ··· 969 r.Context(), 970 client, 971 &tangled.RepoForkSync_Input{ 972 - Did: user.Active.Did, 973 Name: f.Name, 974 Source: f.Source, 975 Branch: ref, ··· 988 func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) { 989 l := rp.logger.With("handler", "ForkRepo") 990 991 - user := rp.oauth.GetMultiAccountUser(r) 992 f, err := rp.repoResolver.Resolve(r) 993 if err != nil { 994 l.Error("failed to resolve source repo", "err", err) ··· 997 998 switch r.Method { 999 case http.MethodGet: 1000 - user := rp.oauth.GetMultiAccountUser(r) 1001 - knots, err := rp.enforcer.GetKnotsForUser(user.Active.Did) 1002 if err != nil { 1003 rp.pages.Notice(w, "repo", "Invalid user account.") 1004 return ··· 1020 } 1021 l = l.With("targetKnot", targetKnot) 1022 1023 - ok, err := rp.enforcer.E.Enforce(user.Active.Did, targetKnot, targetKnot, "repo:create") 1024 if err != nil || !ok { 1025 rp.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 1026 return ··· 1037 // in the user's account. 1038 existingRepo, err := db.GetRepo( 1039 rp.db, 1040 - orm.FilterEq("did", user.Active.Did), 1041 orm.FilterEq("name", forkName), 1042 ) 1043 if err != nil { ··· 1066 // create an atproto record for this fork 1067 rkey := tid.TID() 1068 repo := &models.Repo{ 1069 - Did: user.Active.Did, 1070 Name: forkName, 1071 Knot: targetKnot, 1072 Rkey: rkey, ··· 1086 1087 atresp, err := comatproto.RepoPutRecord(r.Context(), atpClient, &comatproto.RepoPutRecord_Input{ 1088 Collection: tangled.RepoNSID, 1089 - Repo: user.Active.Did, 1090 Rkey: rkey, 1091 Record: &lexutil.LexiconTypeDecoder{ 1092 Val: &record, ··· 1165 } 1166 1167 // acls 1168 - p, _ := securejoin.SecureJoin(user.Active.Did, forkName) 1169 - err = rp.enforcer.AddRepo(user.Active.Did, targetKnot, p) 1170 if err != nil { 1171 l.Error("failed to add ACLs", "err", err) 1172 rp.pages.Notice(w, "repo", "Failed to set up repository permissions.") ··· 1191 aturi = "" 1192 1193 rp.notifier.NewRepo(r.Context(), repo) 1194 - rp.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, forkName)) 1195 } 1196 } 1197
··· 81 82 // modify the spindle configured for this repo 83 func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) { 84 + user := rp.oauth.GetUser(r) 85 l := rp.logger.With("handler", "EditSpindle") 86 + l = l.With("did", user.Did) 87 88 errorId := "operation-error" 89 fail := func(msg string, err error) { ··· 107 108 if !removingSpindle { 109 // ensure that this is a valid spindle for this user 110 + validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did) 111 if err != nil { 112 fail("Failed to find spindles. Try again later.", err) 113 return ··· 168 } 169 170 func (rp *Repo) AddLabelDef(w http.ResponseWriter, r *http.Request) { 171 + user := rp.oauth.GetUser(r) 172 l := rp.logger.With("handler", "AddLabel") 173 + l = l.With("did", user.Did) 174 175 f, err := rp.repoResolver.Resolve(r) 176 if err != nil { ··· 216 } 217 218 label := models.LabelDefinition{ 219 + Did: user.Did, 220 Rkey: tid.TID(), 221 Name: name, 222 ValueType: valueType, ··· 327 } 328 329 func (rp *Repo) DeleteLabelDef(w http.ResponseWriter, r *http.Request) { 330 + user := rp.oauth.GetUser(r) 331 l := rp.logger.With("handler", "DeleteLabel") 332 + l = l.With("did", user.Did) 333 334 f, err := rp.repoResolver.Resolve(r) 335 if err != nil { ··· 435 } 436 437 func (rp *Repo) SubscribeLabel(w http.ResponseWriter, r *http.Request) { 438 + user := rp.oauth.GetUser(r) 439 l := rp.logger.With("handler", "SubscribeLabel") 440 + l = l.With("did", user.Did) 441 442 f, err := rp.repoResolver.Resolve(r) 443 if err != nil { ··· 521 } 522 523 func (rp *Repo) UnsubscribeLabel(w http.ResponseWriter, r *http.Request) { 524 + user := rp.oauth.GetUser(r) 525 l := rp.logger.With("handler", "UnsubscribeLabel") 526 + l = l.With("did", user.Did) 527 528 f, err := rp.repoResolver.Resolve(r) 529 if err != nil { ··· 633 } 634 state := states[subject] 635 636 + user := rp.oauth.GetUser(r) 637 rp.pages.LabelPanel(w, pages.LabelPanelParams{ 638 LoggedInUser: user, 639 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), ··· 681 } 682 state := states[subject] 683 684 + user := rp.oauth.GetUser(r) 685 rp.pages.EditLabelPanel(w, pages.EditLabelPanelParams{ 686 LoggedInUser: user, 687 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), ··· 692 } 693 694 func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) { 695 + user := rp.oauth.GetUser(r) 696 l := rp.logger.With("handler", "AddCollaborator") 697 + l = l.With("did", user.Did) 698 699 f, err := rp.repoResolver.Resolve(r) 700 if err != nil { ··· 723 return 724 } 725 726 + if collaboratorIdent.DID.String() == user.Did { 727 fail("You seem to be adding yourself as a collaborator.", nil) 728 return 729 } ··· 738 } 739 740 // emit a record 741 + currentUser := rp.oauth.GetUser(r) 742 rkey := tid.TID() 743 createdAt := time.Now() 744 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 745 Collection: tangled.RepoCollaboratorNSID, 746 + Repo: currentUser.Did, 747 Rkey: rkey, 748 Record: &lexutil.LexiconTypeDecoder{ 749 Val: &tangled.RepoCollaborator{ ··· 792 } 793 794 err = db.AddCollaborator(tx, models.Collaborator{ 795 + Did: syntax.DID(currentUser.Did), 796 Rkey: rkey, 797 SubjectDid: collaboratorIdent.DID, 798 RepoAt: f.RepoAt(), ··· 822 } 823 824 func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) { 825 + user := rp.oauth.GetUser(r) 826 l := rp.logger.With("handler", "DeleteRepo") 827 828 noticeId := "operation-error" ··· 840 } 841 _, err = comatproto.RepoDeleteRecord(r.Context(), atpClient, &comatproto.RepoDeleteRecord_Input{ 842 Collection: tangled.RepoNSID, 843 + Repo: user.Did, 844 Rkey: f.Rkey, 845 }) 846 if err != nil { ··· 940 ref := chi.URLParam(r, "ref") 941 ref, _ = url.PathUnescape(ref) 942 943 + user := rp.oauth.GetUser(r) 944 f, err := rp.repoResolver.Resolve(r) 945 if err != nil { 946 l.Error("failed to resolve source repo", "err", err) ··· 969 r.Context(), 970 client, 971 &tangled.RepoForkSync_Input{ 972 + Did: user.Did, 973 Name: f.Name, 974 Source: f.Source, 975 Branch: ref, ··· 988 func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) { 989 l := rp.logger.With("handler", "ForkRepo") 990 991 + user := rp.oauth.GetUser(r) 992 f, err := rp.repoResolver.Resolve(r) 993 if err != nil { 994 l.Error("failed to resolve source repo", "err", err) ··· 997 998 switch r.Method { 999 case http.MethodGet: 1000 + user := rp.oauth.GetUser(r) 1001 + knots, err := rp.enforcer.GetKnotsForUser(user.Did) 1002 if err != nil { 1003 rp.pages.Notice(w, "repo", "Invalid user account.") 1004 return ··· 1020 } 1021 l = l.With("targetKnot", targetKnot) 1022 1023 + ok, err := rp.enforcer.E.Enforce(user.Did, targetKnot, targetKnot, "repo:create") 1024 if err != nil || !ok { 1025 rp.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 1026 return ··· 1037 // in the user's account. 1038 existingRepo, err := db.GetRepo( 1039 rp.db, 1040 + orm.FilterEq("did", user.Did), 1041 orm.FilterEq("name", forkName), 1042 ) 1043 if err != nil { ··· 1066 // create an atproto record for this fork 1067 rkey := tid.TID() 1068 repo := &models.Repo{ 1069 + Did: user.Did, 1070 Name: forkName, 1071 Knot: targetKnot, 1072 Rkey: rkey, ··· 1086 1087 atresp, err := comatproto.RepoPutRecord(r.Context(), atpClient, &comatproto.RepoPutRecord_Input{ 1088 Collection: tangled.RepoNSID, 1089 + Repo: user.Did, 1090 Rkey: rkey, 1091 Record: &lexutil.LexiconTypeDecoder{ 1092 Val: &record, ··· 1165 } 1166 1167 // acls 1168 + p, _ := securejoin.SecureJoin(user.Did, forkName) 1169 + err = rp.enforcer.AddRepo(user.Did, targetKnot, p) 1170 if err != nil { 1171 l.Error("failed to add ACLs", "err", err) 1172 rp.pages.Notice(w, "repo", "Failed to set up repository permissions.") ··· 1191 aturi = "" 1192 1193 rp.notifier.NewRepo(r.Context(), repo) 1194 + rp.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Did, forkName)) 1195 } 1196 } 1197
+5 -5
appview/repo/settings.go
··· 79 } 80 81 func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) { 82 - user := rp.oauth.GetMultiAccountUser(r) 83 l := rp.logger.With("handler", "Secrets") 84 - l = l.With("did", user.Active.Did) 85 86 f, err := rp.repoResolver.Resolve(r) 87 if err != nil { ··· 185 l := rp.logger.With("handler", "generalSettings") 186 187 f, err := rp.repoResolver.Resolve(r) 188 - user := rp.oauth.GetMultiAccountUser(r) 189 190 scheme := "http" 191 if !rp.config.Core.Dev { ··· 271 l := rp.logger.With("handler", "accessSettings") 272 273 f, err := rp.repoResolver.Resolve(r) 274 - user := rp.oauth.GetMultiAccountUser(r) 275 276 collaborators, err := func(repo *models.Repo) ([]pages.Collaborator, error) { 277 repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(repo.DidSlashRepo(), repo.Knot) ··· 318 l := rp.logger.With("handler", "pipelineSettings") 319 320 f, err := rp.repoResolver.Resolve(r) 321 - user := rp.oauth.GetMultiAccountUser(r) 322 323 // all spindles that the repo owner is a member of 324 spindles, err := rp.enforcer.GetSpindlesForUser(f.Did)
··· 79 } 80 81 func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) { 82 + user := rp.oauth.GetUser(r) 83 l := rp.logger.With("handler", "Secrets") 84 + l = l.With("did", user.Did) 85 86 f, err := rp.repoResolver.Resolve(r) 87 if err != nil { ··· 185 l := rp.logger.With("handler", "generalSettings") 186 187 f, err := rp.repoResolver.Resolve(r) 188 + user := rp.oauth.GetUser(r) 189 190 scheme := "http" 191 if !rp.config.Core.Dev { ··· 271 l := rp.logger.With("handler", "accessSettings") 272 273 f, err := rp.repoResolver.Resolve(r) 274 + user := rp.oauth.GetUser(r) 275 276 collaborators, err := func(repo *models.Repo) ([]pages.Collaborator, error) { 277 repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(repo.DidSlashRepo(), repo.Knot) ··· 318 l := rp.logger.With("handler", "pipelineSettings") 319 320 f, err := rp.repoResolver.Resolve(r) 321 + user := rp.oauth.GetUser(r) 322 323 // all spindles that the repo owner is a member of 324 spindles, err := rp.enforcer.GetSpindlesForUser(f.Did)
+1 -1
appview/repo/tags.go
··· 69 danglingArtifacts = append(danglingArtifacts, a) 70 } 71 } 72 - user := rp.oauth.GetMultiAccountUser(r) 73 rp.pages.RepoTags(w, pages.RepoTagsParams{ 74 LoggedInUser: user, 75 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
··· 69 danglingArtifacts = append(danglingArtifacts, a) 70 } 71 } 72 + user := rp.oauth.GetUser(r) 73 rp.pages.RepoTags(w, pages.RepoTagsParams{ 74 LoggedInUser: user, 75 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
+1 -1
appview/repo/tree.go
··· 88 http.Redirect(w, r, redirectTo, http.StatusFound) 89 return 90 } 91 - user := rp.oauth.GetMultiAccountUser(r) 92 var breadcrumbs [][]string 93 breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", ownerSlashRepo, url.PathEscape(ref))}) 94 if treePath != "" {
··· 88 http.Redirect(w, r, redirectTo, http.StatusFound) 89 return 90 } 91 + user := rp.oauth.GetUser(r) 92 var breadcrumbs [][]string 93 breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", ownerSlashRepo, url.PathEscape(ref))}) 94 if treePath != "" {
+4 -4
appview/reporesolver/resolver.go
··· 55 // 2. [x] remove `rr`, `CurrentDir`, `Ref` fields from `ResolvedRepo` 56 // 3. [x] remove `ResolvedRepo` 57 // 4. [ ] replace reporesolver to reposervice 58 - func (rr *RepoResolver) GetRepoInfo(r *http.Request, user *oauth.MultiAccountUser) repoinfo.RepoInfo { 59 ownerId, ook := r.Context().Value("resolvedId").(identity.Identity) 60 repo, rok := r.Context().Value("repo").(*models.Repo) 61 if !ook || !rok { ··· 69 repoAt := repo.RepoAt() 70 isStarred := false 71 roles := repoinfo.RolesInRepo{} 72 - if user != nil && user.Active != nil { 73 - isStarred = db.GetStarStatus(rr.execer, user.Active.Did, repoAt) 74 - roles.Roles = rr.enforcer.GetPermissionsInRepo(user.Active.Did, repo.Knot, repo.DidSlashRepo()) 75 } 76 77 stats := repo.RepoStats
··· 55 // 2. [x] remove `rr`, `CurrentDir`, `Ref` fields from `ResolvedRepo` 56 // 3. [x] remove `ResolvedRepo` 57 // 4. [ ] replace reporesolver to reposervice 58 + func (rr *RepoResolver) GetRepoInfo(r *http.Request, user *oauth.User) repoinfo.RepoInfo { 59 ownerId, ook := r.Context().Value("resolvedId").(identity.Identity) 60 repo, rok := r.Context().Value("repo").(*models.Repo) 61 if !ook || !rok { ··· 69 repoAt := repo.RepoAt() 70 isStarred := false 71 roles := repoinfo.RolesInRepo{} 72 + if user != nil { 73 + isStarred = db.GetStarStatus(rr.execer, user.Did, repoAt) 74 + roles.Roles = rr.enforcer.GetPermissionsInRepo(user.Did, repo.Knot, repo.DidSlashRepo()) 75 } 76 77 stats := repo.RepoStats
+6 -6
appview/settings/settings.go
··· 81 } 82 83 func (s *Settings) profileSettings(w http.ResponseWriter, r *http.Request) { 84 - user := s.OAuth.GetMultiAccountUser(r) 85 86 s.Pages.UserProfileSettings(w, pages.UserProfileSettingsParams{ 87 LoggedInUser: user, ··· 91 } 92 93 func (s *Settings) notificationsSettings(w http.ResponseWriter, r *http.Request) { 94 - user := s.OAuth.GetMultiAccountUser(r) 95 did := s.OAuth.GetDid(r) 96 97 prefs, err := db.GetNotificationPreference(s.Db, did) ··· 137 } 138 139 func (s *Settings) keysSettings(w http.ResponseWriter, r *http.Request) { 140 - user := s.OAuth.GetMultiAccountUser(r) 141 - pubKeys, err := db.GetPublicKeysForDid(s.Db, user.Active.Did) 142 if err != nil { 143 log.Println(err) 144 } ··· 152 } 153 154 func (s *Settings) emailsSettings(w http.ResponseWriter, r *http.Request) { 155 - user := s.OAuth.GetMultiAccountUser(r) 156 - emails, err := db.GetAllEmails(s.Db, user.Active.Did) 157 if err != nil { 158 log.Println(err) 159 }
··· 81 } 82 83 func (s *Settings) profileSettings(w http.ResponseWriter, r *http.Request) { 84 + user := s.OAuth.GetUser(r) 85 86 s.Pages.UserProfileSettings(w, pages.UserProfileSettingsParams{ 87 LoggedInUser: user, ··· 91 } 92 93 func (s *Settings) notificationsSettings(w http.ResponseWriter, r *http.Request) { 94 + user := s.OAuth.GetUser(r) 95 did := s.OAuth.GetDid(r) 96 97 prefs, err := db.GetNotificationPreference(s.Db, did) ··· 137 } 138 139 func (s *Settings) keysSettings(w http.ResponseWriter, r *http.Request) { 140 + user := s.OAuth.GetUser(r) 141 + pubKeys, err := db.GetPublicKeysForDid(s.Db, user.Did) 142 if err != nil { 143 log.Println(err) 144 } ··· 152 } 153 154 func (s *Settings) emailsSettings(w http.ResponseWriter, r *http.Request) { 155 + user := s.OAuth.GetUser(r) 156 + emails, err := db.GetAllEmails(s.Db, user.Did) 157 if err != nil { 158 log.Println(err) 159 }
+41 -41
appview/spindles/spindles.go
··· 69 } 70 71 func (s *Spindles) spindles(w http.ResponseWriter, r *http.Request) { 72 - user := s.OAuth.GetMultiAccountUser(r) 73 all, err := db.GetSpindles( 74 s.Db, 75 - orm.FilterEq("owner", user.Active.Did), 76 ) 77 if err != nil { 78 s.Logger.Error("failed to fetch spindles", "err", err) ··· 91 func (s *Spindles) dashboard(w http.ResponseWriter, r *http.Request) { 92 l := s.Logger.With("handler", "dashboard") 93 94 - user := s.OAuth.GetMultiAccountUser(r) 95 - l = l.With("user", user.Active.Did) 96 97 instance := chi.URLParam(r, "instance") 98 if instance == "" { ··· 103 spindles, err := db.GetSpindles( 104 s.Db, 105 orm.FilterEq("instance", instance), 106 - orm.FilterEq("owner", user.Active.Did), 107 orm.FilterIsNot("verified", "null"), 108 ) 109 if err != nil || len(spindles) != 1 { ··· 155 // 156 // if the spindle is not up yet, the user is free to retry verification at a later point 157 func (s *Spindles) register(w http.ResponseWriter, r *http.Request) { 158 - user := s.OAuth.GetMultiAccountUser(r) 159 l := s.Logger.With("handler", "register") 160 161 noticeId := "register-error" ··· 176 return 177 } 178 l = l.With("instance", instance) 179 - l = l.With("user", user.Active.Did) 180 181 tx, err := s.Db.Begin() 182 if err != nil { ··· 190 }() 191 192 err = db.AddSpindle(tx, models.Spindle{ 193 - Owner: syntax.DID(user.Active.Did), 194 Instance: instance, 195 }) 196 if err != nil { ··· 214 return 215 } 216 217 - ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.SpindleNSID, user.Active.Did, instance) 218 var exCid *string 219 if ex != nil { 220 exCid = ex.Cid ··· 223 // re-announce by registering under same rkey 224 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 225 Collection: tangled.SpindleNSID, 226 - Repo: user.Active.Did, 227 Rkey: instance, 228 Record: &lexutil.LexiconTypeDecoder{ 229 Val: &tangled.Spindle{ ··· 254 } 255 256 // begin verification 257 - err = serververify.RunVerification(r.Context(), instance, user.Active.Did, s.Config.Core.Dev) 258 if err != nil { 259 l.Error("verification failed", "err", err) 260 s.Pages.HxRefresh(w) 261 return 262 } 263 264 - _, err = serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Active.Did) 265 if err != nil { 266 l.Error("failed to mark verified", "err", err) 267 s.Pages.HxRefresh(w) ··· 273 } 274 275 func (s *Spindles) delete(w http.ResponseWriter, r *http.Request) { 276 - user := s.OAuth.GetMultiAccountUser(r) 277 l := s.Logger.With("handler", "delete") 278 279 noticeId := "operation-error" ··· 291 292 spindles, err := db.GetSpindles( 293 s.Db, 294 - orm.FilterEq("owner", user.Active.Did), 295 orm.FilterEq("instance", instance), 296 ) 297 if err != nil || len(spindles) != 1 { ··· 300 return 301 } 302 303 - if string(spindles[0].Owner) != user.Active.Did { 304 - l.Error("unauthorized", "user", user.Active.Did, "owner", spindles[0].Owner) 305 s.Pages.Notice(w, noticeId, "Failed to delete spindle, unauthorized deletion attempt.") 306 return 307 } ··· 320 // remove spindle members first 321 err = db.RemoveSpindleMember( 322 tx, 323 - orm.FilterEq("did", user.Active.Did), 324 orm.FilterEq("instance", instance), 325 ) 326 if err != nil { ··· 331 332 err = db.DeleteSpindle( 333 tx, 334 - orm.FilterEq("owner", user.Active.Did), 335 orm.FilterEq("instance", instance), 336 ) 337 if err != nil { ··· 359 360 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 361 Collection: tangled.SpindleNSID, 362 - Repo: user.Active.Did, 363 Rkey: instance, 364 }) 365 if err != nil { ··· 391 } 392 393 func (s *Spindles) retry(w http.ResponseWriter, r *http.Request) { 394 - user := s.OAuth.GetMultiAccountUser(r) 395 l := s.Logger.With("handler", "retry") 396 397 noticeId := "operation-error" ··· 407 return 408 } 409 l = l.With("instance", instance) 410 - l = l.With("user", user.Active.Did) 411 412 spindles, err := db.GetSpindles( 413 s.Db, 414 - orm.FilterEq("owner", user.Active.Did), 415 orm.FilterEq("instance", instance), 416 ) 417 if err != nil || len(spindles) != 1 { ··· 420 return 421 } 422 423 - if string(spindles[0].Owner) != user.Active.Did { 424 - l.Error("unauthorized", "user", user.Active.Did, "owner", spindles[0].Owner) 425 s.Pages.Notice(w, noticeId, "Failed to verify spindle, unauthorized verification attempt.") 426 return 427 } 428 429 // begin verification 430 - err = serververify.RunVerification(r.Context(), instance, user.Active.Did, s.Config.Core.Dev) 431 if err != nil { 432 l.Error("verification failed", "err", err) 433 ··· 445 return 446 } 447 448 - rowId, err := serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Active.Did) 449 if err != nil { 450 l.Error("failed to mark verified", "err", err) 451 s.Pages.Notice(w, noticeId, err.Error()) ··· 473 } 474 475 func (s *Spindles) addMember(w http.ResponseWriter, r *http.Request) { 476 - user := s.OAuth.GetMultiAccountUser(r) 477 l := s.Logger.With("handler", "addMember") 478 479 instance := chi.URLParam(r, "instance") ··· 483 return 484 } 485 l = l.With("instance", instance) 486 - l = l.With("user", user.Active.Did) 487 488 spindles, err := db.GetSpindles( 489 s.Db, 490 - orm.FilterEq("owner", user.Active.Did), 491 orm.FilterEq("instance", instance), 492 ) 493 if err != nil || len(spindles) != 1 { ··· 502 s.Pages.Notice(w, noticeId, defaultErr) 503 } 504 505 - if string(spindles[0].Owner) != user.Active.Did { 506 - l.Error("unauthorized", "user", user.Active.Did, "owner", spindles[0].Owner) 507 s.Pages.Notice(w, noticeId, "Failed to add member, unauthorized attempt.") 508 return 509 } ··· 552 553 // add member to db 554 if err = db.AddSpindleMember(tx, models.SpindleMember{ 555 - Did: syntax.DID(user.Active.Did), 556 Rkey: rkey, 557 Instance: instance, 558 Subject: memberId.DID, ··· 570 571 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 572 Collection: tangled.SpindleMemberNSID, 573 - Repo: user.Active.Did, 574 Rkey: rkey, 575 Record: &lexutil.LexiconTypeDecoder{ 576 Val: &tangled.SpindleMember{ ··· 603 } 604 605 func (s *Spindles) removeMember(w http.ResponseWriter, r *http.Request) { 606 - user := s.OAuth.GetMultiAccountUser(r) 607 l := s.Logger.With("handler", "removeMember") 608 609 noticeId := "operation-error" ··· 619 return 620 } 621 l = l.With("instance", instance) 622 - l = l.With("user", user.Active.Did) 623 624 spindles, err := db.GetSpindles( 625 s.Db, 626 - orm.FilterEq("owner", user.Active.Did), 627 orm.FilterEq("instance", instance), 628 ) 629 if err != nil || len(spindles) != 1 { ··· 632 return 633 } 634 635 - if string(spindles[0].Owner) != user.Active.Did { 636 - l.Error("unauthorized", "user", user.Active.Did, "owner", spindles[0].Owner) 637 s.Pages.Notice(w, noticeId, "Failed to remove member, unauthorized attempt.") 638 return 639 } ··· 668 // get the record from the DB first: 669 members, err := db.GetSpindleMembers( 670 s.Db, 671 - orm.FilterEq("did", user.Active.Did), 672 orm.FilterEq("instance", instance), 673 orm.FilterEq("subject", memberId.DID), 674 ) ··· 681 // remove from db 682 if err = db.RemoveSpindleMember( 683 tx, 684 - orm.FilterEq("did", user.Active.Did), 685 orm.FilterEq("instance", instance), 686 orm.FilterEq("subject", memberId.DID), 687 ); err != nil { ··· 707 // remove from pds 708 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 709 Collection: tangled.SpindleMemberNSID, 710 - Repo: user.Active.Did, 711 Rkey: members[0].Rkey, 712 }) 713 if err != nil {
··· 69 } 70 71 func (s *Spindles) spindles(w http.ResponseWriter, r *http.Request) { 72 + user := s.OAuth.GetUser(r) 73 all, err := db.GetSpindles( 74 s.Db, 75 + orm.FilterEq("owner", user.Did), 76 ) 77 if err != nil { 78 s.Logger.Error("failed to fetch spindles", "err", err) ··· 91 func (s *Spindles) dashboard(w http.ResponseWriter, r *http.Request) { 92 l := s.Logger.With("handler", "dashboard") 93 94 + user := s.OAuth.GetUser(r) 95 + l = l.With("user", user.Did) 96 97 instance := chi.URLParam(r, "instance") 98 if instance == "" { ··· 103 spindles, err := db.GetSpindles( 104 s.Db, 105 orm.FilterEq("instance", instance), 106 + orm.FilterEq("owner", user.Did), 107 orm.FilterIsNot("verified", "null"), 108 ) 109 if err != nil || len(spindles) != 1 { ··· 155 // 156 // if the spindle is not up yet, the user is free to retry verification at a later point 157 func (s *Spindles) register(w http.ResponseWriter, r *http.Request) { 158 + user := s.OAuth.GetUser(r) 159 l := s.Logger.With("handler", "register") 160 161 noticeId := "register-error" ··· 176 return 177 } 178 l = l.With("instance", instance) 179 + l = l.With("user", user.Did) 180 181 tx, err := s.Db.Begin() 182 if err != nil { ··· 190 }() 191 192 err = db.AddSpindle(tx, models.Spindle{ 193 + Owner: syntax.DID(user.Did), 194 Instance: instance, 195 }) 196 if err != nil { ··· 214 return 215 } 216 217 + ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.SpindleNSID, user.Did, instance) 218 var exCid *string 219 if ex != nil { 220 exCid = ex.Cid ··· 223 // re-announce by registering under same rkey 224 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 225 Collection: tangled.SpindleNSID, 226 + Repo: user.Did, 227 Rkey: instance, 228 Record: &lexutil.LexiconTypeDecoder{ 229 Val: &tangled.Spindle{ ··· 254 } 255 256 // begin verification 257 + err = serververify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev) 258 if err != nil { 259 l.Error("verification failed", "err", err) 260 s.Pages.HxRefresh(w) 261 return 262 } 263 264 + _, err = serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Did) 265 if err != nil { 266 l.Error("failed to mark verified", "err", err) 267 s.Pages.HxRefresh(w) ··· 273 } 274 275 func (s *Spindles) delete(w http.ResponseWriter, r *http.Request) { 276 + user := s.OAuth.GetUser(r) 277 l := s.Logger.With("handler", "delete") 278 279 noticeId := "operation-error" ··· 291 292 spindles, err := db.GetSpindles( 293 s.Db, 294 + orm.FilterEq("owner", user.Did), 295 orm.FilterEq("instance", instance), 296 ) 297 if err != nil || len(spindles) != 1 { ··· 300 return 301 } 302 303 + if string(spindles[0].Owner) != user.Did { 304 + l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 305 s.Pages.Notice(w, noticeId, "Failed to delete spindle, unauthorized deletion attempt.") 306 return 307 } ··· 320 // remove spindle members first 321 err = db.RemoveSpindleMember( 322 tx, 323 + orm.FilterEq("did", user.Did), 324 orm.FilterEq("instance", instance), 325 ) 326 if err != nil { ··· 331 332 err = db.DeleteSpindle( 333 tx, 334 + orm.FilterEq("owner", user.Did), 335 orm.FilterEq("instance", instance), 336 ) 337 if err != nil { ··· 359 360 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 361 Collection: tangled.SpindleNSID, 362 + Repo: user.Did, 363 Rkey: instance, 364 }) 365 if err != nil { ··· 391 } 392 393 func (s *Spindles) retry(w http.ResponseWriter, r *http.Request) { 394 + user := s.OAuth.GetUser(r) 395 l := s.Logger.With("handler", "retry") 396 397 noticeId := "operation-error" ··· 407 return 408 } 409 l = l.With("instance", instance) 410 + l = l.With("user", user.Did) 411 412 spindles, err := db.GetSpindles( 413 s.Db, 414 + orm.FilterEq("owner", user.Did), 415 orm.FilterEq("instance", instance), 416 ) 417 if err != nil || len(spindles) != 1 { ··· 420 return 421 } 422 423 + if string(spindles[0].Owner) != user.Did { 424 + l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 425 s.Pages.Notice(w, noticeId, "Failed to verify spindle, unauthorized verification attempt.") 426 return 427 } 428 429 // begin verification 430 + err = serververify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev) 431 if err != nil { 432 l.Error("verification failed", "err", err) 433 ··· 445 return 446 } 447 448 + rowId, err := serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Did) 449 if err != nil { 450 l.Error("failed to mark verified", "err", err) 451 s.Pages.Notice(w, noticeId, err.Error()) ··· 473 } 474 475 func (s *Spindles) addMember(w http.ResponseWriter, r *http.Request) { 476 + user := s.OAuth.GetUser(r) 477 l := s.Logger.With("handler", "addMember") 478 479 instance := chi.URLParam(r, "instance") ··· 483 return 484 } 485 l = l.With("instance", instance) 486 + l = l.With("user", user.Did) 487 488 spindles, err := db.GetSpindles( 489 s.Db, 490 + orm.FilterEq("owner", user.Did), 491 orm.FilterEq("instance", instance), 492 ) 493 if err != nil || len(spindles) != 1 { ··· 502 s.Pages.Notice(w, noticeId, defaultErr) 503 } 504 505 + if string(spindles[0].Owner) != user.Did { 506 + l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 507 s.Pages.Notice(w, noticeId, "Failed to add member, unauthorized attempt.") 508 return 509 } ··· 552 553 // add member to db 554 if err = db.AddSpindleMember(tx, models.SpindleMember{ 555 + Did: syntax.DID(user.Did), 556 Rkey: rkey, 557 Instance: instance, 558 Subject: memberId.DID, ··· 570 571 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 572 Collection: tangled.SpindleMemberNSID, 573 + Repo: user.Did, 574 Rkey: rkey, 575 Record: &lexutil.LexiconTypeDecoder{ 576 Val: &tangled.SpindleMember{ ··· 603 } 604 605 func (s *Spindles) removeMember(w http.ResponseWriter, r *http.Request) { 606 + user := s.OAuth.GetUser(r) 607 l := s.Logger.With("handler", "removeMember") 608 609 noticeId := "operation-error" ··· 619 return 620 } 621 l = l.With("instance", instance) 622 + l = l.With("user", user.Did) 623 624 spindles, err := db.GetSpindles( 625 s.Db, 626 + orm.FilterEq("owner", user.Did), 627 orm.FilterEq("instance", instance), 628 ) 629 if err != nil || len(spindles) != 1 { ··· 632 return 633 } 634 635 + if string(spindles[0].Owner) != user.Did { 636 + l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 637 s.Pages.Notice(w, noticeId, "Failed to remove member, unauthorized attempt.") 638 return 639 } ··· 668 // get the record from the DB first: 669 members, err := db.GetSpindleMembers( 670 s.Db, 671 + orm.FilterEq("did", user.Did), 672 orm.FilterEq("instance", instance), 673 orm.FilterEq("subject", memberId.DID), 674 ) ··· 681 // remove from db 682 if err = db.RemoveSpindleMember( 683 tx, 684 + orm.FilterEq("did", user.Did), 685 orm.FilterEq("instance", instance), 686 orm.FilterEq("subject", memberId.DID), 687 ); err != nil { ··· 707 // remove from pds 708 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 709 Collection: tangled.SpindleMemberNSID, 710 + Repo: user.Did, 711 Rkey: members[0].Rkey, 712 }) 713 if err != nil {
-83
appview/state/accounts.go
··· 1 - package state 2 - 3 - import ( 4 - "net/http" 5 - 6 - "github.com/go-chi/chi/v5" 7 - ) 8 - 9 - func (s *State) SwitchAccount(w http.ResponseWriter, r *http.Request) { 10 - l := s.logger.With("handler", "SwitchAccount") 11 - 12 - if err := r.ParseForm(); err != nil { 13 - l.Error("failed to parse form", "err", err) 14 - http.Error(w, "invalid request", http.StatusBadRequest) 15 - return 16 - } 17 - 18 - did := r.FormValue("did") 19 - if did == "" { 20 - http.Error(w, "missing did", http.StatusBadRequest) 21 - return 22 - } 23 - 24 - if err := s.oauth.SwitchAccount(w, r, did); err != nil { 25 - l.Error("failed to switch account", "err", err) 26 - s.pages.HxRedirect(w, "/login?error=session") 27 - return 28 - } 29 - 30 - l.Info("switched account", "did", did) 31 - s.pages.HxRedirect(w, "/") 32 - } 33 - 34 - func (s *State) RemoveAccount(w http.ResponseWriter, r *http.Request) { 35 - l := s.logger.With("handler", "RemoveAccount") 36 - 37 - did := chi.URLParam(r, "did") 38 - if did == "" { 39 - http.Error(w, "missing did", http.StatusBadRequest) 40 - return 41 - } 42 - 43 - currentUser := s.oauth.GetMultiAccountUser(r) 44 - isCurrentAccount := currentUser != nil && currentUser.Active.Did == did 45 - 46 - var remainingAccounts []string 47 - if currentUser != nil { 48 - for _, acc := range currentUser.Accounts { 49 - if acc.Did != did { 50 - remainingAccounts = append(remainingAccounts, acc.Did) 51 - } 52 - } 53 - } 54 - 55 - if err := s.oauth.RemoveAccount(w, r, did); err != nil { 56 - l.Error("failed to remove account", "err", err) 57 - http.Error(w, "failed to remove account", http.StatusInternalServerError) 58 - return 59 - } 60 - 61 - l.Info("removed account", "did", did) 62 - 63 - if isCurrentAccount { 64 - if len(remainingAccounts) > 0 { 65 - nextDid := remainingAccounts[0] 66 - if err := s.oauth.SwitchAccount(w, r, nextDid); err != nil { 67 - l.Error("failed to switch to next account", "err", err) 68 - s.pages.HxRedirect(w, "/login") 69 - return 70 - } 71 - s.pages.HxRefresh(w) 72 - return 73 - } 74 - 75 - if err := s.oauth.DeleteSession(w, r); err != nil { 76 - l.Error("failed to delete session", "err", err) 77 - } 78 - s.pages.HxRedirect(w, "/login") 79 - return 80 - } 81 - 82 - s.pages.HxRefresh(w) 83 - }
···
+7 -7
appview/state/follow.go
··· 15 ) 16 17 func (s *State) Follow(w http.ResponseWriter, r *http.Request) { 18 - currentUser := s.oauth.GetMultiAccountUser(r) 19 20 subject := r.URL.Query().Get("subject") 21 if subject == "" { ··· 29 return 30 } 31 32 - if currentUser.Active.Did == subjectIdent.DID.String() { 33 log.Println("cant follow or unfollow yourself") 34 return 35 } ··· 46 rkey := tid.TID() 47 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 48 Collection: tangled.GraphFollowNSID, 49 - Repo: currentUser.Active.Did, 50 Rkey: rkey, 51 Record: &lexutil.LexiconTypeDecoder{ 52 Val: &tangled.GraphFollow{ ··· 62 log.Println("created atproto record: ", resp.Uri) 63 64 follow := &models.Follow{ 65 - UserDid: currentUser.Active.Did, 66 SubjectDid: subjectIdent.DID.String(), 67 Rkey: rkey, 68 } ··· 89 return 90 case http.MethodDelete: 91 // find the record in the db 92 - follow, err := db.GetFollow(s.db, currentUser.Active.Did, subjectIdent.DID.String()) 93 if err != nil { 94 log.Println("failed to get follow relationship") 95 return ··· 97 98 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 99 Collection: tangled.GraphFollowNSID, 100 - Repo: currentUser.Active.Did, 101 Rkey: follow.Rkey, 102 }) 103 ··· 106 return 107 } 108 109 - err = db.DeleteFollowByRkey(s.db, currentUser.Active.Did, follow.Rkey) 110 if err != nil { 111 log.Println("failed to delete follow from DB") 112 // this is not an issue, the firehose event might have already done this
··· 15 ) 16 17 func (s *State) Follow(w http.ResponseWriter, r *http.Request) { 18 + currentUser := s.oauth.GetUser(r) 19 20 subject := r.URL.Query().Get("subject") 21 if subject == "" { ··· 29 return 30 } 31 32 + if currentUser.Did == subjectIdent.DID.String() { 33 log.Println("cant follow or unfollow yourself") 34 return 35 } ··· 46 rkey := tid.TID() 47 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 48 Collection: tangled.GraphFollowNSID, 49 + Repo: currentUser.Did, 50 Rkey: rkey, 51 Record: &lexutil.LexiconTypeDecoder{ 52 Val: &tangled.GraphFollow{ ··· 62 log.Println("created atproto record: ", resp.Uri) 63 64 follow := &models.Follow{ 65 + UserDid: currentUser.Did, 66 SubjectDid: subjectIdent.DID.String(), 67 Rkey: rkey, 68 } ··· 89 return 90 case http.MethodDelete: 91 // find the record in the db 92 + follow, err := db.GetFollow(s.db, currentUser.Did, subjectIdent.DID.String()) 93 if err != nil { 94 log.Println("failed to get follow relationship") 95 return ··· 97 98 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 99 Collection: tangled.GraphFollowNSID, 100 + Repo: currentUser.Did, 101 Rkey: follow.Rkey, 102 }) 103 ··· 106 return 107 } 108 109 + err = db.DeleteFollowByRkey(s.db, currentUser.Did, follow.Rkey) 110 if err != nil { 111 log.Println("failed to delete follow from DB") 112 // this is not an issue, the firehose event might have already done this
+1 -1
appview/state/gfi.go
··· 15 ) 16 17 func (s *State) GoodFirstIssues(w http.ResponseWriter, r *http.Request) { 18 - user := s.oauth.GetMultiAccountUser(r) 19 20 page := pagination.FromContext(r.Context()) 21
··· 15 ) 16 17 func (s *State) GoodFirstIssues(w http.ResponseWriter, r *http.Request) { 18 + user := s.oauth.GetUser(r) 19 20 page := pagination.FromContext(r.Context()) 21
+7 -57
appview/state/login.go
··· 5 "net/http" 6 "strings" 7 8 - "tangled.org/core/appview/oauth" 9 "tangled.org/core/appview/pages" 10 ) 11 ··· 16 case http.MethodGet: 17 returnURL := r.URL.Query().Get("return_url") 18 errorCode := r.URL.Query().Get("error") 19 - addAccount := r.URL.Query().Get("mode") == "add_account" 20 - 21 - user := s.oauth.GetMultiAccountUser(r) 22 - if user == nil { 23 - registry := s.oauth.GetAccounts(r) 24 - if len(registry.Accounts) > 0 { 25 - user = &oauth.MultiAccountUser{ 26 - Active: nil, 27 - Accounts: registry.Accounts, 28 - } 29 - } 30 - } 31 s.pages.Login(w, pages.LoginParams{ 32 - ReturnUrl: returnURL, 33 - ErrorCode: errorCode, 34 - AddAccount: addAccount, 35 - LoggedInUser: user, 36 }) 37 case http.MethodPost: 38 handle := r.FormValue("handle") 39 - returnURL := r.FormValue("return_url") 40 - addAccount := r.FormValue("add_account") == "true" 41 42 // when users copy their handle from bsky.app, it tends to have these characters around it: 43 // ··· 61 return 62 } 63 64 - if err := s.oauth.SetAuthReturn(w, r, returnURL, addAccount); err != nil { 65 - l.Error("failed to set auth return", "err", err) 66 - } 67 - 68 redirectURL, err := s.oauth.ClientApp.StartAuthFlow(r.Context(), handle) 69 if err != nil { 70 l.Error("failed to start auth", "err", err) ··· 79 func (s *State) Logout(w http.ResponseWriter, r *http.Request) { 80 l := s.logger.With("handler", "Logout") 81 82 - currentUser := s.oauth.GetMultiAccountUser(r) 83 - if currentUser == nil || currentUser.Active == nil { 84 - s.pages.HxRedirect(w, "/login") 85 - return 86 - } 87 - 88 - currentDid := currentUser.Active.Did 89 - 90 - var remainingAccounts []string 91 - for _, acc := range currentUser.Accounts { 92 - if acc.Did != currentDid { 93 - remainingAccounts = append(remainingAccounts, acc.Did) 94 - } 95 - } 96 - 97 - if err := s.oauth.RemoveAccount(w, r, currentDid); err != nil { 98 - l.Error("failed to remove account from registry", "err", err) 99 - } 100 - 101 - if err := s.oauth.DeleteSession(w, r); err != nil { 102 - l.Error("failed to delete session", "err", err) 103 - } 104 - 105 - if len(remainingAccounts) > 0 { 106 - nextDid := remainingAccounts[0] 107 - if err := s.oauth.SwitchAccount(w, r, nextDid); err != nil { 108 - l.Error("failed to switch to next account", "err", err) 109 - s.pages.HxRedirect(w, "/login") 110 - return 111 - } 112 - l.Info("switched to next account after logout", "did", nextDid) 113 - s.pages.HxRefresh(w) 114 - return 115 } 116 117 - l.Info("logged out last account") 118 s.pages.HxRedirect(w, "/login") 119 }
··· 5 "net/http" 6 "strings" 7 8 "tangled.org/core/appview/pages" 9 ) 10 ··· 15 case http.MethodGet: 16 returnURL := r.URL.Query().Get("return_url") 17 errorCode := r.URL.Query().Get("error") 18 s.pages.Login(w, pages.LoginParams{ 19 + ReturnUrl: returnURL, 20 + ErrorCode: errorCode, 21 }) 22 case http.MethodPost: 23 handle := r.FormValue("handle") 24 25 // when users copy their handle from bsky.app, it tends to have these characters around it: 26 // ··· 44 return 45 } 46 47 redirectURL, err := s.oauth.ClientApp.StartAuthFlow(r.Context(), handle) 48 if err != nil { 49 l.Error("failed to start auth", "err", err) ··· 58 func (s *State) Logout(w http.ResponseWriter, r *http.Request) { 59 l := s.logger.With("handler", "Logout") 60 61 + err := s.oauth.DeleteSession(w, r) 62 + if err != nil { 63 + l.Error("failed to logout", "err", err) 64 + } else { 65 + l.Info("logged out successfully") 66 } 67 68 s.pages.HxRedirect(w, "/login") 69 }
+32 -32
appview/state/profile.go
··· 77 return nil, fmt.Errorf("failed to get follower stats: %w", err) 78 } 79 80 - loggedInUser := s.oauth.GetMultiAccountUser(r) 81 followStatus := models.IsNotFollowing 82 if loggedInUser != nil { 83 - followStatus = db.GetFollowStatus(s.db, loggedInUser.Active.Did, did) 84 } 85 86 now := time.Now() ··· 174 } 175 176 s.pages.ProfileOverview(w, pages.ProfileOverviewParams{ 177 - LoggedInUser: s.oauth.GetMultiAccountUser(r), 178 Card: profile, 179 Repos: pinnedRepos, 180 CollaboratingRepos: pinnedCollaboratingRepos, ··· 205 } 206 207 err = s.pages.ProfileRepos(w, pages.ProfileReposParams{ 208 - LoggedInUser: s.oauth.GetMultiAccountUser(r), 209 Repos: repos, 210 Card: profile, 211 }) ··· 234 } 235 236 err = s.pages.ProfileStarred(w, pages.ProfileStarredParams{ 237 - LoggedInUser: s.oauth.GetMultiAccountUser(r), 238 Repos: repos, 239 Card: profile, 240 }) ··· 259 } 260 261 err = s.pages.ProfileStrings(w, pages.ProfileStringsParams{ 262 - LoggedInUser: s.oauth.GetMultiAccountUser(r), 263 Strings: strings, 264 Card: profile, 265 }) ··· 283 } 284 l = l.With("profileDid", profile.UserDid) 285 286 - loggedInUser := s.oauth.GetMultiAccountUser(r) 287 params := FollowsPageParams{ 288 Card: profile, 289 } ··· 316 317 loggedInUserFollowing := make(map[string]struct{}) 318 if loggedInUser != nil { 319 - following, err := db.GetFollowing(s.db, loggedInUser.Active.Did) 320 if err != nil { 321 - l.Error("failed to get follow list", "err", err, "loggedInUser", loggedInUser.Active.Did) 322 return &params, err 323 } 324 loggedInUserFollowing = make(map[string]struct{}, len(following)) ··· 333 followStatus := models.IsNotFollowing 334 if _, exists := loggedInUserFollowing[did]; exists { 335 followStatus = models.IsFollowing 336 - } else if loggedInUser != nil && loggedInUser.Active.Did == did { 337 followStatus = models.IsSelf 338 } 339 ··· 367 } 368 369 s.pages.ProfileFollowers(w, pages.ProfileFollowersParams{ 370 - LoggedInUser: s.oauth.GetMultiAccountUser(r), 371 Followers: followPage.Follows, 372 Card: followPage.Card, 373 }) ··· 381 } 382 383 s.pages.ProfileFollowing(w, pages.ProfileFollowingParams{ 384 - LoggedInUser: s.oauth.GetMultiAccountUser(r), 385 Following: followPage.Follows, 386 Card: followPage.Card, 387 }) ··· 530 } 531 532 func (s *State) UpdateProfileBio(w http.ResponseWriter, r *http.Request) { 533 - user := s.oauth.GetMultiAccountUser(r) 534 535 err := r.ParseForm() 536 if err != nil { ··· 539 return 540 } 541 542 - profile, err := db.GetProfile(s.db, user.Active.Did) 543 if err != nil { 544 - log.Printf("getting profile data for %s: %s", user.Active.Did, err) 545 } 546 547 profile.Description = r.FormValue("description") ··· 578 } 579 580 func (s *State) UpdateProfilePins(w http.ResponseWriter, r *http.Request) { 581 - user := s.oauth.GetMultiAccountUser(r) 582 583 err := r.ParseForm() 584 if err != nil { ··· 587 return 588 } 589 590 - profile, err := db.GetProfile(s.db, user.Active.Did) 591 if err != nil { 592 - log.Printf("getting profile data for %s: %s", user.Active.Did, err) 593 } 594 595 i := 0 ··· 617 } 618 619 func (s *State) updateProfile(profile *models.Profile, w http.ResponseWriter, r *http.Request) { 620 - user := s.oauth.GetMultiAccountUser(r) 621 tx, err := s.db.BeginTx(r.Context(), nil) 622 if err != nil { 623 log.Println("failed to start transaction", err) ··· 644 vanityStats = append(vanityStats, string(v.Kind)) 645 } 646 647 - ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.ActorProfileNSID, user.Active.Did, "self") 648 var cid *string 649 if ex != nil { 650 cid = ex.Cid ··· 652 653 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 654 Collection: tangled.ActorProfileNSID, 655 - Repo: user.Active.Did, 656 Rkey: "self", 657 Record: &lexutil.LexiconTypeDecoder{ 658 Val: &tangled.ActorProfile{ ··· 681 682 s.notifier.UpdateProfile(r.Context(), profile) 683 684 - s.pages.HxRedirect(w, "/"+user.Active.Did) 685 } 686 687 func (s *State) EditBioFragment(w http.ResponseWriter, r *http.Request) { 688 - user := s.oauth.GetMultiAccountUser(r) 689 690 - profile, err := db.GetProfile(s.db, user.Active.Did) 691 if err != nil { 692 - log.Printf("getting profile data for %s: %s", user.Active.Did, err) 693 } 694 695 s.pages.EditBioFragment(w, pages.EditBioParams{ ··· 699 } 700 701 func (s *State) EditPinsFragment(w http.ResponseWriter, r *http.Request) { 702 - user := s.oauth.GetMultiAccountUser(r) 703 704 - profile, err := db.GetProfile(s.db, user.Active.Did) 705 if err != nil { 706 - log.Printf("getting profile data for %s: %s", user.Active.Did, err) 707 } 708 709 - repos, err := db.GetRepos(s.db, 0, orm.FilterEq("did", user.Active.Did)) 710 if err != nil { 711 - log.Printf("getting repos for %s: %s", user.Active.Did, err) 712 } 713 714 - collaboratingRepos, err := db.CollaboratingIn(s.db, user.Active.Did) 715 if err != nil { 716 - log.Printf("getting collaborating repos for %s: %s", user.Active.Did, err) 717 } 718 719 allRepos := []pages.PinnedRepo{}
··· 77 return nil, fmt.Errorf("failed to get follower stats: %w", err) 78 } 79 80 + loggedInUser := s.oauth.GetUser(r) 81 followStatus := models.IsNotFollowing 82 if loggedInUser != nil { 83 + followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, did) 84 } 85 86 now := time.Now() ··· 174 } 175 176 s.pages.ProfileOverview(w, pages.ProfileOverviewParams{ 177 + LoggedInUser: s.oauth.GetUser(r), 178 Card: profile, 179 Repos: pinnedRepos, 180 CollaboratingRepos: pinnedCollaboratingRepos, ··· 205 } 206 207 err = s.pages.ProfileRepos(w, pages.ProfileReposParams{ 208 + LoggedInUser: s.oauth.GetUser(r), 209 Repos: repos, 210 Card: profile, 211 }) ··· 234 } 235 236 err = s.pages.ProfileStarred(w, pages.ProfileStarredParams{ 237 + LoggedInUser: s.oauth.GetUser(r), 238 Repos: repos, 239 Card: profile, 240 }) ··· 259 } 260 261 err = s.pages.ProfileStrings(w, pages.ProfileStringsParams{ 262 + LoggedInUser: s.oauth.GetUser(r), 263 Strings: strings, 264 Card: profile, 265 }) ··· 283 } 284 l = l.With("profileDid", profile.UserDid) 285 286 + loggedInUser := s.oauth.GetUser(r) 287 params := FollowsPageParams{ 288 Card: profile, 289 } ··· 316 317 loggedInUserFollowing := make(map[string]struct{}) 318 if loggedInUser != nil { 319 + following, err := db.GetFollowing(s.db, loggedInUser.Did) 320 if err != nil { 321 + l.Error("failed to get follow list", "err", err, "loggedInUser", loggedInUser.Did) 322 return &params, err 323 } 324 loggedInUserFollowing = make(map[string]struct{}, len(following)) ··· 333 followStatus := models.IsNotFollowing 334 if _, exists := loggedInUserFollowing[did]; exists { 335 followStatus = models.IsFollowing 336 + } else if loggedInUser != nil && loggedInUser.Did == did { 337 followStatus = models.IsSelf 338 } 339 ··· 367 } 368 369 s.pages.ProfileFollowers(w, pages.ProfileFollowersParams{ 370 + LoggedInUser: s.oauth.GetUser(r), 371 Followers: followPage.Follows, 372 Card: followPage.Card, 373 }) ··· 381 } 382 383 s.pages.ProfileFollowing(w, pages.ProfileFollowingParams{ 384 + LoggedInUser: s.oauth.GetUser(r), 385 Following: followPage.Follows, 386 Card: followPage.Card, 387 }) ··· 530 } 531 532 func (s *State) UpdateProfileBio(w http.ResponseWriter, r *http.Request) { 533 + user := s.oauth.GetUser(r) 534 535 err := r.ParseForm() 536 if err != nil { ··· 539 return 540 } 541 542 + profile, err := db.GetProfile(s.db, user.Did) 543 if err != nil { 544 + log.Printf("getting profile data for %s: %s", user.Did, err) 545 } 546 547 profile.Description = r.FormValue("description") ··· 578 } 579 580 func (s *State) UpdateProfilePins(w http.ResponseWriter, r *http.Request) { 581 + user := s.oauth.GetUser(r) 582 583 err := r.ParseForm() 584 if err != nil { ··· 587 return 588 } 589 590 + profile, err := db.GetProfile(s.db, user.Did) 591 if err != nil { 592 + log.Printf("getting profile data for %s: %s", user.Did, err) 593 } 594 595 i := 0 ··· 617 } 618 619 func (s *State) updateProfile(profile *models.Profile, w http.ResponseWriter, r *http.Request) { 620 + user := s.oauth.GetUser(r) 621 tx, err := s.db.BeginTx(r.Context(), nil) 622 if err != nil { 623 log.Println("failed to start transaction", err) ··· 644 vanityStats = append(vanityStats, string(v.Kind)) 645 } 646 647 + ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.ActorProfileNSID, user.Did, "self") 648 var cid *string 649 if ex != nil { 650 cid = ex.Cid ··· 652 653 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 654 Collection: tangled.ActorProfileNSID, 655 + Repo: user.Did, 656 Rkey: "self", 657 Record: &lexutil.LexiconTypeDecoder{ 658 Val: &tangled.ActorProfile{ ··· 681 682 s.notifier.UpdateProfile(r.Context(), profile) 683 684 + s.pages.HxRedirect(w, "/"+user.Did) 685 } 686 687 func (s *State) EditBioFragment(w http.ResponseWriter, r *http.Request) { 688 + user := s.oauth.GetUser(r) 689 690 + profile, err := db.GetProfile(s.db, user.Did) 691 if err != nil { 692 + log.Printf("getting profile data for %s: %s", user.Did, err) 693 } 694 695 s.pages.EditBioFragment(w, pages.EditBioParams{ ··· 699 } 700 701 func (s *State) EditPinsFragment(w http.ResponseWriter, r *http.Request) { 702 + user := s.oauth.GetUser(r) 703 704 + profile, err := db.GetProfile(s.db, user.Did) 705 if err != nil { 706 + log.Printf("getting profile data for %s: %s", user.Did, err) 707 } 708 709 + repos, err := db.GetRepos(s.db, 0, orm.FilterEq("did", user.Did)) 710 if err != nil { 711 + log.Printf("getting repos for %s: %s", user.Did, err) 712 } 713 714 + collaboratingRepos, err := db.CollaboratingIn(s.db, user.Did) 715 if err != nil { 716 + log.Printf("getting collaborating repos for %s: %s", user.Did, err) 717 } 718 719 allRepos := []pages.PinnedRepo{}
+7 -7
appview/state/reaction.go
··· 17 ) 18 19 func (s *State) React(w http.ResponseWriter, r *http.Request) { 20 - currentUser := s.oauth.GetMultiAccountUser(r) 21 22 subject := r.URL.Query().Get("subject") 23 if subject == "" { ··· 49 rkey := tid.TID() 50 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 51 Collection: tangled.FeedReactionNSID, 52 - Repo: currentUser.Active.Did, 53 Rkey: rkey, 54 Record: &lexutil.LexiconTypeDecoder{ 55 Val: &tangled.FeedReaction{ ··· 64 return 65 } 66 67 - err = db.AddReaction(s.db, currentUser.Active.Did, subjectUri, reactionKind, rkey) 68 if err != nil { 69 log.Println("failed to react", err) 70 return ··· 87 88 return 89 case http.MethodDelete: 90 - reaction, err := db.GetReaction(s.db, currentUser.Active.Did, subjectUri, reactionKind) 91 if err != nil { 92 - log.Println("failed to get reaction relationship for", currentUser.Active.Did, subjectUri) 93 return 94 } 95 96 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 97 Collection: tangled.FeedReactionNSID, 98 - Repo: currentUser.Active.Did, 99 Rkey: reaction.Rkey, 100 }) 101 ··· 104 return 105 } 106 107 - err = db.DeleteReactionByRkey(s.db, currentUser.Active.Did, reaction.Rkey) 108 if err != nil { 109 log.Println("failed to delete reaction from DB") 110 // this is not an issue, the firehose event might have already done this
··· 17 ) 18 19 func (s *State) React(w http.ResponseWriter, r *http.Request) { 20 + currentUser := s.oauth.GetUser(r) 21 22 subject := r.URL.Query().Get("subject") 23 if subject == "" { ··· 49 rkey := tid.TID() 50 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 51 Collection: tangled.FeedReactionNSID, 52 + Repo: currentUser.Did, 53 Rkey: rkey, 54 Record: &lexutil.LexiconTypeDecoder{ 55 Val: &tangled.FeedReaction{ ··· 64 return 65 } 66 67 + err = db.AddReaction(s.db, currentUser.Did, subjectUri, reactionKind, rkey) 68 if err != nil { 69 log.Println("failed to react", err) 70 return ··· 87 88 return 89 case http.MethodDelete: 90 + reaction, err := db.GetReaction(s.db, currentUser.Did, subjectUri, reactionKind) 91 if err != nil { 92 + log.Println("failed to get reaction relationship for", currentUser.Did, subjectUri) 93 return 94 } 95 96 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 97 Collection: tangled.FeedReactionNSID, 98 + Repo: currentUser.Did, 99 Rkey: reaction.Rkey, 100 }) 101 ··· 104 return 105 } 106 107 + err = db.DeleteReactionByRkey(s.db, currentUser.Did, reaction.Rkey) 108 if err != nil { 109 log.Println("failed to delete reaction from DB") 110 // this is not an issue, the firehose event might have already done this
+3 -6
appview/state/router.go
··· 94 r.Mount("/", s.RepoRouter(mw)) 95 r.Mount("/issues", s.IssuesRouter(mw)) 96 r.Mount("/pulls", s.PullsRouter(mw)) 97 - r.Mount("/pipelines", s.PipelinesRouter(mw)) 98 r.Mount("/labels", s.LabelsRouter()) 99 100 // These routes get proxied to the knot ··· 129 r.Get("/login", s.Login) 130 r.Post("/login", s.Login) 131 r.Post("/logout", s.Logout) 132 - 133 - r.Post("/account/switch", s.SwitchAccount) 134 - r.With(middleware.AuthMiddleware(s.oauth)).Delete("/account/{did}", s.RemoveAccount) 135 136 r.Route("/repo", func(r chi.Router) { 137 r.Route("/new", func(r chi.Router) { ··· 316 return repo.Router(mw) 317 } 318 319 - func (s *State) PipelinesRouter(mw *middleware.Middleware) http.Handler { 320 pipes := pipelines.New( 321 s.oauth, 322 s.repoResolver, ··· 328 s.enforcer, 329 log.SubLogger(s.logger, "pipelines"), 330 ) 331 - return pipes.Router(mw) 332 } 333 334 func (s *State) LabelsRouter() http.Handler {
··· 94 r.Mount("/", s.RepoRouter(mw)) 95 r.Mount("/issues", s.IssuesRouter(mw)) 96 r.Mount("/pulls", s.PullsRouter(mw)) 97 + r.Mount("/pipelines", s.PipelinesRouter()) 98 r.Mount("/labels", s.LabelsRouter()) 99 100 // These routes get proxied to the knot ··· 129 r.Get("/login", s.Login) 130 r.Post("/login", s.Login) 131 r.Post("/logout", s.Logout) 132 133 r.Route("/repo", func(r chi.Router) { 134 r.Route("/new", func(r chi.Router) { ··· 313 return repo.Router(mw) 314 } 315 316 + func (s *State) PipelinesRouter() http.Handler { 317 pipes := pipelines.New( 318 s.oauth, 319 s.repoResolver, ··· 325 s.enforcer, 326 log.SubLogger(s.logger, "pipelines"), 327 ) 328 + return pipes.Router() 329 } 330 331 func (s *State) LabelsRouter() http.Handler {
+6 -6
appview/state/star.go
··· 16 ) 17 18 func (s *State) Star(w http.ResponseWriter, r *http.Request) { 19 - currentUser := s.oauth.GetMultiAccountUser(r) 20 21 subject := r.URL.Query().Get("subject") 22 if subject == "" { ··· 42 rkey := tid.TID() 43 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 44 Collection: tangled.FeedStarNSID, 45 - Repo: currentUser.Active.Did, 46 Rkey: rkey, 47 Record: &lexutil.LexiconTypeDecoder{ 48 Val: &tangled.FeedStar{ ··· 57 log.Println("created atproto record: ", resp.Uri) 58 59 star := &models.Star{ 60 - Did: currentUser.Active.Did, 61 RepoAt: subjectUri, 62 Rkey: rkey, 63 } ··· 84 return 85 case http.MethodDelete: 86 // find the record in the db 87 - star, err := db.GetStar(s.db, currentUser.Active.Did, subjectUri) 88 if err != nil { 89 log.Println("failed to get star relationship") 90 return ··· 92 93 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 94 Collection: tangled.FeedStarNSID, 95 - Repo: currentUser.Active.Did, 96 Rkey: star.Rkey, 97 }) 98 ··· 101 return 102 } 103 104 - err = db.DeleteStarByRkey(s.db, currentUser.Active.Did, star.Rkey) 105 if err != nil { 106 log.Println("failed to delete star from DB") 107 // this is not an issue, the firehose event might have already done this
··· 16 ) 17 18 func (s *State) Star(w http.ResponseWriter, r *http.Request) { 19 + currentUser := s.oauth.GetUser(r) 20 21 subject := r.URL.Query().Get("subject") 22 if subject == "" { ··· 42 rkey := tid.TID() 43 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 44 Collection: tangled.FeedStarNSID, 45 + Repo: currentUser.Did, 46 Rkey: rkey, 47 Record: &lexutil.LexiconTypeDecoder{ 48 Val: &tangled.FeedStar{ ··· 57 log.Println("created atproto record: ", resp.Uri) 58 59 star := &models.Star{ 60 + Did: currentUser.Did, 61 RepoAt: subjectUri, 62 Rkey: rkey, 63 } ··· 84 return 85 case http.MethodDelete: 86 // find the record in the db 87 + star, err := db.GetStar(s.db, currentUser.Did, subjectUri) 88 if err != nil { 89 log.Println("failed to get star relationship") 90 return ··· 92 93 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 94 Collection: tangled.FeedStarNSID, 95 + Repo: currentUser.Did, 96 Rkey: star.Rkey, 97 }) 98 ··· 101 return 102 } 103 104 + err = db.DeleteStarByRkey(s.db, currentUser.Did, star.Rkey) 105 if err != nil { 106 log.Println("failed to delete star from DB") 107 // this is not an issue, the firehose event might have already done this
+22 -22
appview/state/state.go
··· 213 } 214 215 func (s *State) TermsOfService(w http.ResponseWriter, r *http.Request) { 216 - user := s.oauth.GetMultiAccountUser(r) 217 s.pages.TermsOfService(w, pages.TermsOfServiceParams{ 218 LoggedInUser: user, 219 }) 220 } 221 222 func (s *State) PrivacyPolicy(w http.ResponseWriter, r *http.Request) { 223 - user := s.oauth.GetMultiAccountUser(r) 224 s.pages.PrivacyPolicy(w, pages.PrivacyPolicyParams{ 225 LoggedInUser: user, 226 }) 227 } 228 229 func (s *State) Brand(w http.ResponseWriter, r *http.Request) { 230 - user := s.oauth.GetMultiAccountUser(r) 231 s.pages.Brand(w, pages.BrandParams{ 232 LoggedInUser: user, 233 }) 234 } 235 236 func (s *State) HomeOrTimeline(w http.ResponseWriter, r *http.Request) { 237 - if s.oauth.GetMultiAccountUser(r) != nil { 238 s.Timeline(w, r) 239 return 240 } ··· 242 } 243 244 func (s *State) Timeline(w http.ResponseWriter, r *http.Request) { 245 - user := s.oauth.GetMultiAccountUser(r) 246 247 // TODO: set this flag based on the UI 248 filtered := false 249 250 var userDid string 251 - if user != nil && user.Active != nil { 252 - userDid = user.Active.Did 253 } 254 timeline, err := db.MakeTimeline(s.db, 50, userDid, filtered) 255 if err != nil { ··· 278 } 279 280 func (s *State) UpgradeBanner(w http.ResponseWriter, r *http.Request) { 281 - user := s.oauth.GetMultiAccountUser(r) 282 if user == nil { 283 return 284 } 285 286 l := s.logger.With("handler", "UpgradeBanner") 287 - l = l.With("did", user.Active.Did) 288 289 regs, err := db.GetRegistrations( 290 s.db, 291 - orm.FilterEq("did", user.Active.Did), 292 orm.FilterEq("needs_upgrade", 1), 293 ) 294 if err != nil { ··· 297 298 spindles, err := db.GetSpindles( 299 s.db, 300 - orm.FilterEq("owner", user.Active.Did), 301 orm.FilterEq("needs_upgrade", 1), 302 ) 303 if err != nil { ··· 411 func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { 412 switch r.Method { 413 case http.MethodGet: 414 - user := s.oauth.GetMultiAccountUser(r) 415 - knots, err := s.enforcer.GetKnotsForUser(user.Active.Did) 416 if err != nil { 417 s.pages.Notice(w, "repo", "Invalid user account.") 418 return ··· 426 case http.MethodPost: 427 l := s.logger.With("handler", "NewRepo") 428 429 - user := s.oauth.GetMultiAccountUser(r) 430 - l = l.With("did", user.Active.Did) 431 432 // form validation 433 domain := r.FormValue("domain") ··· 459 description := r.FormValue("description") 460 461 // ACL validation 462 - ok, err := s.enforcer.E.Enforce(user.Active.Did, domain, domain, "repo:create") 463 if err != nil || !ok { 464 l.Info("unauthorized") 465 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") ··· 469 // Check for existing repos 470 existingRepo, err := db.GetRepo( 471 s.db, 472 - orm.FilterEq("did", user.Active.Did), 473 orm.FilterEq("name", repoName), 474 ) 475 if err == nil && existingRepo != nil { ··· 481 // create atproto record for this repo 482 rkey := tid.TID() 483 repo := &models.Repo{ 484 - Did: user.Active.Did, 485 Name: repoName, 486 Knot: domain, 487 Rkey: rkey, ··· 500 501 atresp, err := comatproto.RepoPutRecord(r.Context(), atpClient, &comatproto.RepoPutRecord_Input{ 502 Collection: tangled.RepoNSID, 503 - Repo: user.Active.Did, 504 Rkey: rkey, 505 Record: &lexutil.LexiconTypeDecoder{ 506 Val: &record, ··· 577 } 578 579 // acls 580 - p, _ := securejoin.SecureJoin(user.Active.Did, repoName) 581 - err = s.enforcer.AddRepo(user.Active.Did, domain, p) 582 if err != nil { 583 l.Error("acl setup failed", "err", err) 584 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") ··· 603 aturi = "" 604 605 s.notifier.NewRepo(r.Context(), repo) 606 - s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, repoName)) 607 } 608 } 609
··· 213 } 214 215 func (s *State) TermsOfService(w http.ResponseWriter, r *http.Request) { 216 + user := s.oauth.GetUser(r) 217 s.pages.TermsOfService(w, pages.TermsOfServiceParams{ 218 LoggedInUser: user, 219 }) 220 } 221 222 func (s *State) PrivacyPolicy(w http.ResponseWriter, r *http.Request) { 223 + user := s.oauth.GetUser(r) 224 s.pages.PrivacyPolicy(w, pages.PrivacyPolicyParams{ 225 LoggedInUser: user, 226 }) 227 } 228 229 func (s *State) Brand(w http.ResponseWriter, r *http.Request) { 230 + user := s.oauth.GetUser(r) 231 s.pages.Brand(w, pages.BrandParams{ 232 LoggedInUser: user, 233 }) 234 } 235 236 func (s *State) HomeOrTimeline(w http.ResponseWriter, r *http.Request) { 237 + if s.oauth.GetUser(r) != nil { 238 s.Timeline(w, r) 239 return 240 } ··· 242 } 243 244 func (s *State) Timeline(w http.ResponseWriter, r *http.Request) { 245 + user := s.oauth.GetUser(r) 246 247 // TODO: set this flag based on the UI 248 filtered := false 249 250 var userDid string 251 + if user != nil { 252 + userDid = user.Did 253 } 254 timeline, err := db.MakeTimeline(s.db, 50, userDid, filtered) 255 if err != nil { ··· 278 } 279 280 func (s *State) UpgradeBanner(w http.ResponseWriter, r *http.Request) { 281 + user := s.oauth.GetUser(r) 282 if user == nil { 283 return 284 } 285 286 l := s.logger.With("handler", "UpgradeBanner") 287 + l = l.With("did", user.Did) 288 289 regs, err := db.GetRegistrations( 290 s.db, 291 + orm.FilterEq("did", user.Did), 292 orm.FilterEq("needs_upgrade", 1), 293 ) 294 if err != nil { ··· 297 298 spindles, err := db.GetSpindles( 299 s.db, 300 + orm.FilterEq("owner", user.Did), 301 orm.FilterEq("needs_upgrade", 1), 302 ) 303 if err != nil { ··· 411 func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { 412 switch r.Method { 413 case http.MethodGet: 414 + user := s.oauth.GetUser(r) 415 + knots, err := s.enforcer.GetKnotsForUser(user.Did) 416 if err != nil { 417 s.pages.Notice(w, "repo", "Invalid user account.") 418 return ··· 426 case http.MethodPost: 427 l := s.logger.With("handler", "NewRepo") 428 429 + user := s.oauth.GetUser(r) 430 + l = l.With("did", user.Did) 431 432 // form validation 433 domain := r.FormValue("domain") ··· 459 description := r.FormValue("description") 460 461 // ACL validation 462 + ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create") 463 if err != nil || !ok { 464 l.Info("unauthorized") 465 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") ··· 469 // Check for existing repos 470 existingRepo, err := db.GetRepo( 471 s.db, 472 + orm.FilterEq("did", user.Did), 473 orm.FilterEq("name", repoName), 474 ) 475 if err == nil && existingRepo != nil { ··· 481 // create atproto record for this repo 482 rkey := tid.TID() 483 repo := &models.Repo{ 484 + Did: user.Did, 485 Name: repoName, 486 Knot: domain, 487 Rkey: rkey, ··· 500 501 atresp, err := comatproto.RepoPutRecord(r.Context(), atpClient, &comatproto.RepoPutRecord_Input{ 502 Collection: tangled.RepoNSID, 503 + Repo: user.Did, 504 Rkey: rkey, 505 Record: &lexutil.LexiconTypeDecoder{ 506 Val: &record, ··· 577 } 578 579 // acls 580 + p, _ := securejoin.SecureJoin(user.Did, repoName) 581 + err = s.enforcer.AddRepo(user.Did, domain, p) 582 if err != nil { 583 l.Error("acl setup failed", "err", err) 584 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") ··· 603 aturi = "" 604 605 s.notifier.NewRepo(r.Context(), repo) 606 + s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Did, repoName)) 607 } 608 } 609
+19 -19
appview/strings/strings.go
··· 82 } 83 84 s.Pages.StringsTimeline(w, pages.StringTimelineParams{ 85 - LoggedInUser: s.OAuth.GetMultiAccountUser(r), 86 Strings: strings, 87 }) 88 } ··· 153 if err != nil { 154 l.Error("failed to get star count", "err", err) 155 } 156 - user := s.OAuth.GetMultiAccountUser(r) 157 isStarred := false 158 if user != nil { 159 - isStarred = db.GetStarStatus(s.Db, user.Active.Did, string.AtUri()) 160 } 161 162 s.Pages.SingleString(w, pages.SingleStringParams{ ··· 178 func (s *Strings) edit(w http.ResponseWriter, r *http.Request) { 179 l := s.Logger.With("handler", "edit") 180 181 - user := s.OAuth.GetMultiAccountUser(r) 182 183 id, ok := r.Context().Value("resolvedId").(identity.Identity) 184 if !ok { ··· 216 first := all[0] 217 218 // verify that the logged in user owns this string 219 - if user.Active.Did != id.DID.String() { 220 - l.Error("unauthorized request", "expected", id.DID, "got", user.Active.Did) 221 w.WriteHeader(http.StatusUnauthorized) 222 return 223 } ··· 226 case http.MethodGet: 227 // return the form with prefilled fields 228 s.Pages.PutString(w, pages.PutStringParams{ 229 - LoggedInUser: s.OAuth.GetMultiAccountUser(r), 230 Action: "edit", 231 String: first, 232 }) ··· 299 s.Notifier.EditString(r.Context(), &entry) 300 301 // if that went okay, redir to the string 302 - s.Pages.HxRedirect(w, "/strings/"+user.Active.Did+"/"+entry.Rkey) 303 } 304 305 } 306 307 func (s *Strings) create(w http.ResponseWriter, r *http.Request) { 308 l := s.Logger.With("handler", "create") 309 - user := s.OAuth.GetMultiAccountUser(r) 310 311 switch r.Method { 312 case http.MethodGet: 313 s.Pages.PutString(w, pages.PutStringParams{ 314 - LoggedInUser: s.OAuth.GetMultiAccountUser(r), 315 Action: "new", 316 }) 317 case http.MethodPost: ··· 335 description := r.FormValue("description") 336 337 string := models.String{ 338 - Did: syntax.DID(user.Active.Did), 339 Rkey: tid.TID(), 340 Filename: filename, 341 Description: description, ··· 353 354 resp, err := comatproto.RepoPutRecord(r.Context(), client, &atproto.RepoPutRecord_Input{ 355 Collection: tangled.StringNSID, 356 - Repo: user.Active.Did, 357 Rkey: string.Rkey, 358 Record: &lexutil.LexiconTypeDecoder{ 359 Val: &record, ··· 375 s.Notifier.NewString(r.Context(), &string) 376 377 // successful 378 - s.Pages.HxRedirect(w, "/strings/"+user.Active.Did+"/"+string.Rkey) 379 } 380 } 381 382 func (s *Strings) delete(w http.ResponseWriter, r *http.Request) { 383 l := s.Logger.With("handler", "create") 384 - user := s.OAuth.GetMultiAccountUser(r) 385 fail := func(msg string, err error) { 386 l.Error(msg, "err", err) 387 s.Pages.Notice(w, "error", msg) ··· 402 return 403 } 404 405 - if user.Active.Did != id.DID.String() { 406 - fail("You cannot delete this string", fmt.Errorf("unauthorized deletion, %s != %s", user.Active.Did, id.DID.String())) 407 return 408 } 409 410 if err := db.DeleteString( 411 s.Db, 412 - orm.FilterEq("did", user.Active.Did), 413 orm.FilterEq("rkey", rkey), 414 ); err != nil { 415 fail("Failed to delete string.", err) 416 return 417 } 418 419 - s.Notifier.DeleteString(r.Context(), user.Active.Did, rkey) 420 421 - s.Pages.HxRedirect(w, "/strings/"+user.Active.Did) 422 } 423 424 func (s *Strings) comment(w http.ResponseWriter, r *http.Request) {
··· 82 } 83 84 s.Pages.StringsTimeline(w, pages.StringTimelineParams{ 85 + LoggedInUser: s.OAuth.GetUser(r), 86 Strings: strings, 87 }) 88 } ··· 153 if err != nil { 154 l.Error("failed to get star count", "err", err) 155 } 156 + user := s.OAuth.GetUser(r) 157 isStarred := false 158 if user != nil { 159 + isStarred = db.GetStarStatus(s.Db, user.Did, string.AtUri()) 160 } 161 162 s.Pages.SingleString(w, pages.SingleStringParams{ ··· 178 func (s *Strings) edit(w http.ResponseWriter, r *http.Request) { 179 l := s.Logger.With("handler", "edit") 180 181 + user := s.OAuth.GetUser(r) 182 183 id, ok := r.Context().Value("resolvedId").(identity.Identity) 184 if !ok { ··· 216 first := all[0] 217 218 // verify that the logged in user owns this string 219 + if user.Did != id.DID.String() { 220 + l.Error("unauthorized request", "expected", id.DID, "got", user.Did) 221 w.WriteHeader(http.StatusUnauthorized) 222 return 223 } ··· 226 case http.MethodGet: 227 // return the form with prefilled fields 228 s.Pages.PutString(w, pages.PutStringParams{ 229 + LoggedInUser: s.OAuth.GetUser(r), 230 Action: "edit", 231 String: first, 232 }) ··· 299 s.Notifier.EditString(r.Context(), &entry) 300 301 // if that went okay, redir to the string 302 + s.Pages.HxRedirect(w, "/strings/"+user.Did+"/"+entry.Rkey) 303 } 304 305 } 306 307 func (s *Strings) create(w http.ResponseWriter, r *http.Request) { 308 l := s.Logger.With("handler", "create") 309 + user := s.OAuth.GetUser(r) 310 311 switch r.Method { 312 case http.MethodGet: 313 s.Pages.PutString(w, pages.PutStringParams{ 314 + LoggedInUser: s.OAuth.GetUser(r), 315 Action: "new", 316 }) 317 case http.MethodPost: ··· 335 description := r.FormValue("description") 336 337 string := models.String{ 338 + Did: syntax.DID(user.Did), 339 Rkey: tid.TID(), 340 Filename: filename, 341 Description: description, ··· 353 354 resp, err := comatproto.RepoPutRecord(r.Context(), client, &atproto.RepoPutRecord_Input{ 355 Collection: tangled.StringNSID, 356 + Repo: user.Did, 357 Rkey: string.Rkey, 358 Record: &lexutil.LexiconTypeDecoder{ 359 Val: &record, ··· 375 s.Notifier.NewString(r.Context(), &string) 376 377 // successful 378 + s.Pages.HxRedirect(w, "/strings/"+user.Did+"/"+string.Rkey) 379 } 380 } 381 382 func (s *Strings) delete(w http.ResponseWriter, r *http.Request) { 383 l := s.Logger.With("handler", "create") 384 + user := s.OAuth.GetUser(r) 385 fail := func(msg string, err error) { 386 l.Error(msg, "err", err) 387 s.Pages.Notice(w, "error", msg) ··· 402 return 403 } 404 405 + if user.Did != id.DID.String() { 406 + fail("You cannot delete this string", fmt.Errorf("unauthorized deletion, %s != %s", user.Did, id.DID.String())) 407 return 408 } 409 410 if err := db.DeleteString( 411 s.Db, 412 + orm.FilterEq("did", user.Did), 413 orm.FilterEq("rkey", rkey), 414 ); err != nil { 415 fail("Failed to delete string.", err) 416 return 417 } 418 419 + s.Notifier.DeleteString(r.Context(), user.Did, rkey) 420 421 + s.Pages.HxRedirect(w, "/strings/"+user.Did) 422 } 423 424 func (s *Strings) comment(w http.ResponseWriter, r *http.Request) {
+10 -23
cmd/dolly/main.go
··· 2 3 import ( 4 "bytes" 5 - _ "embed" 6 "flag" 7 "fmt" 8 "image" ··· 17 "github.com/srwiley/oksvg" 18 "github.com/srwiley/rasterx" 19 "golang.org/x/image/draw" 20 "tangled.org/core/ico" 21 ) 22 23 func main() { 24 var ( 25 - size string 26 - fillColor string 27 - output string 28 - templatePath string 29 ) 30 31 - flag.StringVar(&templatePath, "template", "", "Path to dolly go-html template") 32 flag.StringVar(&size, "size", "512x512", "Output size in format WIDTHxHEIGHT (e.g., 512x512)") 33 flag.StringVar(&fillColor, "color", "#000000", "Fill color in hex format (e.g., #FF5733)") 34 flag.StringVar(&output, "output", "dolly.svg", "Output file path (format detected from extension: .svg, .png, or .ico)") 35 flag.Parse() 36 - 37 - if templatePath == "" { 38 - fmt.Fprintf(os.Stderr, "Empty template path") 39 - os.Exit(1) 40 - } 41 42 width, height, err := parseSize(size) 43 if err != nil { ··· 59 os.Exit(1) 60 } 61 62 - tpl, err := os.ReadFile(templatePath) 63 - if err != nil { 64 - fmt.Fprintf(os.Stderr, "Failed to read template from path %s: %v\n", templatePath, err) 65 - os.Exit(1) 66 - } 67 - 68 - svgData, err := dolly(string(tpl), fillColor) 69 if err != nil { 70 fmt.Fprintf(os.Stderr, "Error generating SVG: %v\n", err) 71 os.Exit(1) ··· 97 fmt.Printf("Successfully generated %s (%dx%d)\n", output, width, height) 98 } 99 100 - func dolly(tplString, hexColor string) ([]byte, error) { 101 - tpl, err := template.New("dolly").Parse(tplString) 102 if err != nil { 103 return nil, err 104 } 105 106 var svgData bytes.Buffer 107 - if err := tpl.ExecuteTemplate(&svgData, "fragments/dolly/logo", map[string]any{ 108 - "FillColor": hexColor, 109 - "Classes": "", 110 }); err != nil { 111 return nil, err 112 }
··· 2 3 import ( 4 "bytes" 5 "flag" 6 "fmt" 7 "image" ··· 16 "github.com/srwiley/oksvg" 17 "github.com/srwiley/rasterx" 18 "golang.org/x/image/draw" 19 + "tangled.org/core/appview/pages" 20 "tangled.org/core/ico" 21 ) 22 23 func main() { 24 var ( 25 + size string 26 + fillColor string 27 + output string 28 ) 29 30 flag.StringVar(&size, "size", "512x512", "Output size in format WIDTHxHEIGHT (e.g., 512x512)") 31 flag.StringVar(&fillColor, "color", "#000000", "Fill color in hex format (e.g., #FF5733)") 32 flag.StringVar(&output, "output", "dolly.svg", "Output file path (format detected from extension: .svg, .png, or .ico)") 33 flag.Parse() 34 35 width, height, err := parseSize(size) 36 if err != nil { ··· 52 os.Exit(1) 53 } 54 55 + svgData, err := dolly(fillColor) 56 if err != nil { 57 fmt.Fprintf(os.Stderr, "Error generating SVG: %v\n", err) 58 os.Exit(1) ··· 84 fmt.Printf("Successfully generated %s (%dx%d)\n", output, width, height) 85 } 86 87 + func dolly(hexColor string) ([]byte, error) { 88 + tpl, err := template.New("dolly"). 89 + ParseFS(pages.Files, "templates/fragments/dolly/logo.html") 90 if err != nil { 91 return nil, err 92 } 93 94 var svgData bytes.Buffer 95 + if err := tpl.ExecuteTemplate(&svgData, "fragments/dolly/logo", pages.DollyParams{ 96 + FillColor: hexColor, 97 }); err != nil { 98 return nil, err 99 }
-3
docs/template.html
··· 35 $endfor$ 36 37 <link rel="preload" href="/static/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin /> 38 - <link rel="icon" href="/static/logos/dolly.ico" sizes="48x48"/> 39 - <link rel="icon" href="/static/logos/dolly.svg" sizes="any" type="image/svg+xml"/> 40 - <link rel="apple-touch-icon" href="/static/logos/dolly.png"/> 41 42 </head> 43 <body class="bg-white dark:bg-gray-900 flex flex-col min-h-svh">
··· 35 $endfor$ 36 37 <link rel="preload" href="/static/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin /> 38 39 </head> 40 <body class="bg-white dark:bg-gray-900 flex flex-col min-h-svh">
-13
input.css
··· 124 dark:text-gray-100 dark:before:bg-gray-800 dark:before:border-gray-700; 125 } 126 127 - .btn-flat { 128 - @apply relative z-10 inline-flex min-h-[30px] cursor-pointer items-center justify-center 129 - bg-transparent px-2 pb-[0.2rem] text-sm text-gray-900 130 - before:absolute before:inset-0 before:-z-10 before:block before:rounded 131 - before:border before:border-gray-200 before:bg-white 132 - before:content-[''] before:transition-all before:duration-150 before:ease-in-out 133 - hover:before:bg-gray-50 134 - dark:hover:before:bg-gray-700 135 - focus:outline-none focus-visible:before:outline focus-visible:before:outline-2 focus-visible:before:outline-gray-400 136 - disabled:cursor-not-allowed disabled:opacity-50 137 - dark:text-gray-100 dark:before:bg-gray-800 dark:before:border-gray-700; 138 - } 139 - 140 .btn-create { 141 @apply btn text-white 142 before:bg-green-600 hover:before:bg-green-700
··· 124 dark:text-gray-100 dark:before:bg-gray-800 dark:before:border-gray-700; 125 } 126 127 .btn-create { 128 @apply btn text-white 129 before:bg-green-600 hover:before:bg-green-700
+8 -3
knotserver/git/diff.go
··· 64 65 for _, tf := range d.TextFragments { 66 ndiff.TextFragments = append(ndiff.TextFragments, *tf) 67 - nd.Stat.Insertions += tf.LinesAdded 68 - nd.Stat.Deletions += tf.LinesDeleted 69 } 70 71 nd.Diff = append(nd.Diff, ndiff) 72 } 73 74 - nd.Stat.FilesChanged += len(diffs) 75 nd.Commit.FromGoGitCommit(c) 76 77 return &nd, nil
··· 64 65 for _, tf := range d.TextFragments { 66 ndiff.TextFragments = append(ndiff.TextFragments, *tf) 67 + for _, l := range tf.Lines { 68 + switch l.Op { 69 + case gitdiff.OpAdd: 70 + nd.Stat.Insertions += 1 71 + case gitdiff.OpDelete: 72 + nd.Stat.Deletions += 1 73 + } 74 + } 75 } 76 77 nd.Diff = append(nd.Diff, ndiff) 78 } 79 80 nd.Commit.FromGoGitCommit(c) 81 82 return &nd, nil
-33
lexicons/pipeline/cancelPipeline.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "sh.tangled.pipeline.cancelPipeline", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Cancel a running pipeline", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": ["repo", "pipeline", "workflow"], 13 - "properties": { 14 - "repo": { 15 - "type": "string", 16 - "format": "at-uri", 17 - "description": "repo at-uri, spindle can't resolve repo from pipeline at-uri yet" 18 - }, 19 - "pipeline": { 20 - "type": "string", 21 - "format": "at-uri", 22 - "description": "pipeline at-uri" 23 - }, 24 - "workflow": { 25 - "type": "string", 26 - "description": "workflow name" 27 - } 28 - } 29 - } 30 - } 31 - } 32 - } 33 - }
···
-5
nix/pkgs/docs.nix
··· 52 cp -f ${inter-fonts-src}/InterVariable*.ttf $out/static/fonts/ 53 cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono*.woff2 $out/static/fonts/ 54 55 - # favicons 56 - ${dolly}/bin/dolly -output $out/static/logos/dolly.png -size 180x180 57 - ${dolly}/bin/dolly -output $out/static/logos/dolly.ico -size 48x48 58 - ${dolly}/bin/dolly -output $out/static/logos/dolly.svg -color currentColor 59 - 60 # styles 61 cd ${src} && ${tailwindcss}/bin/tailwindcss -i input.css -o $out/stylesheet.css 62 ''
··· 52 cp -f ${inter-fonts-src}/InterVariable*.ttf $out/static/fonts/ 53 cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono*.woff2 $out/static/fonts/ 54 55 # styles 56 cd ${src} && ${tailwindcss}/bin/tailwindcss -i input.css -o $out/stylesheet.css 57 ''
+18 -25
nix/pkgs/dolly.nix
··· 1 { 2 - lib, 3 buildGoApplication, 4 modules, 5 - writeShellScriptBin, 6 - }: let 7 - src = lib.fileset.toSource { 8 - root = ../..; 9 - fileset = lib.fileset.unions [ 10 - ../../go.mod 11 - ../../ico 12 - ../../cmd/dolly/main.go 13 - ../../appview/pages/templates/fragments/dolly/logo.html 14 - ]; 15 - }; 16 - dolly-unwrapped = buildGoApplication { 17 - pname = "dolly-unwrapped"; 18 - version = "0.1.0"; 19 - inherit src modules; 20 - doCheck = false; 21 - subPackages = ["cmd/dolly"]; 22 - }; 23 - in 24 - writeShellScriptBin "dolly" '' 25 - exec ${dolly-unwrapped}/bin/dolly \ 26 - -template ${src}/appview/pages/templates/fragments/dolly/logo.html \ 27 - "$@" 28 - ''
··· 1 { 2 buildGoApplication, 3 modules, 4 + src, 5 + }: 6 + buildGoApplication { 7 + pname = "dolly"; 8 + version = "0.1.0"; 9 + inherit src modules; 10 + 11 + # patch the static dir 12 + postUnpack = '' 13 + pushd source 14 + mkdir -p appview/pages/static 15 + touch appview/pages/static/x 16 + popd 17 + ''; 18 + 19 + doCheck = false; 20 + subPackages = ["cmd/dolly"]; 21 + }
+10 -66
patchutil/interdiff.go
··· 5 "strings" 6 7 "github.com/bluekeyes/go-gitdiff/gitdiff" 8 - "tangled.org/core/appview/filetree" 9 "tangled.org/core/types" 10 ) 11 ··· 13 Files []*InterdiffFile 14 } 15 16 - func (i *InterdiffResult) Stats() types.DiffStat { 17 - var ins, del int64 18 - for _, s := range i.ChangedFiles() { 19 - stat := s.Stats() 20 - ins += stat.Insertions 21 - del += stat.Deletions 22 - } 23 - return types.DiffStat{ 24 - Insertions: ins, 25 - Deletions: del, 26 - FilesChanged: len(i.Files), 27 - } 28 - } 29 - 30 - func (i *InterdiffResult) ChangedFiles() []types.DiffFileRenderer { 31 - drs := make([]types.DiffFileRenderer, len(i.Files)) 32 - for i, s := range i.Files { 33 - drs[i] = s 34 - } 35 - return drs 36 - } 37 - 38 - func (i *InterdiffResult) FileTree() *filetree.FileTreeNode { 39 - fs := make([]string, len(i.Files)) 40 - for i, s := range i.Files { 41 - fs[i] = s.Name 42 } 43 - return filetree.FileTree(fs) 44 } 45 46 func (i *InterdiffResult) String() string { ··· 59 Status InterdiffFileStatus 60 } 61 62 - func (s *InterdiffFile) Id() string { 63 - return s.Name 64 - } 65 - 66 - func (s *InterdiffFile) Split() types.SplitDiff { 67 fragments := make([]types.SplitFragment, len(s.TextFragments)) 68 69 for i, fragment := range s.TextFragments { ··· 76 } 77 } 78 79 - return types.SplitDiff{ 80 Name: s.Id(), 81 TextFragments: fragments, 82 } 83 } 84 85 - func (s *InterdiffFile) CanRender() string { 86 - if s.Status.IsUnchanged() { 87 - return "This file has not been changed." 88 - } else if s.Status.IsRebased() { 89 - return "This patch was likely rebased, as context lines do not match." 90 - } else if s.Status.IsError() { 91 - return "Failed to calculate interdiff for this file." 92 - } else { 93 - return "" 94 - } 95 - } 96 - 97 - func (s *InterdiffFile) Names() types.DiffFileName { 98 - var n types.DiffFileName 99 - n.New = s.Name 100 - return n 101 - } 102 - 103 - func (s *InterdiffFile) Stats() types.DiffFileStat { 104 - var ins, del int64 105 - 106 - if s.File != nil { 107 - for _, f := range s.TextFragments { 108 - ins += f.LinesAdded 109 - del += f.LinesDeleted 110 - } 111 - } 112 - 113 - return types.DiffFileStat{ 114 - Insertions: ins, 115 - Deletions: del, 116 - } 117 } 118 119 func (s *InterdiffFile) String() string {
··· 5 "strings" 6 7 "github.com/bluekeyes/go-gitdiff/gitdiff" 8 "tangled.org/core/types" 9 ) 10 ··· 12 Files []*InterdiffFile 13 } 14 15 + func (i *InterdiffResult) AffectedFiles() []string { 16 + files := make([]string, len(i.Files)) 17 + for _, f := range i.Files { 18 + files = append(files, f.Name) 19 } 20 + return files 21 } 22 23 func (i *InterdiffResult) String() string { ··· 36 Status InterdiffFileStatus 37 } 38 39 + func (s *InterdiffFile) Split() *types.SplitDiff { 40 fragments := make([]types.SplitFragment, len(s.TextFragments)) 41 42 for i, fragment := range s.TextFragments { ··· 49 } 50 } 51 52 + return &types.SplitDiff{ 53 Name: s.Id(), 54 TextFragments: fragments, 55 } 56 } 57 58 + // used by html elements as a unique ID for hrefs 59 + func (s *InterdiffFile) Id() string { 60 + return s.Name 61 } 62 63 func (s *InterdiffFile) String() string {
-9
patchutil/patchutil_test.go
··· 4 "errors" 5 "reflect" 6 "testing" 7 - 8 - "tangled.org/core/types" 9 ) 10 11 func TestIsPatchValid(t *testing.T) { ··· 325 }) 326 } 327 } 328 - 329 - func TestImplsInterfaces(t *testing.T) { 330 - id := &InterdiffResult{} 331 - _ = isDiffsRenderer(id) 332 - } 333 - 334 - func isDiffsRenderer[S types.DiffRenderer](S) bool { return true }
··· 4 "errors" 5 "reflect" 6 "testing" 7 ) 8 9 func TestIsPatchValid(t *testing.T) { ··· 323 }) 324 } 325 }
+18 -6
spindle/db/events.go
··· 18 EventJson string `json:"event"` 19 } 20 21 - func (d *DB) insertEvent(event Event, notifier *notifier.Notifier) error { 22 _, err := d.Exec( 23 `insert into events (rkey, nsid, event, created) values (?, ?, ?, ?)`, 24 event.Rkey, ··· 70 return evts, nil 71 } 72 73 func (d *DB) createStatusEvent( 74 workflowId models.WorkflowId, 75 statusKind models.StatusKind, ··· 100 EventJson: string(eventJson), 101 } 102 103 - return d.insertEvent(event, n) 104 105 } 106 ··· 148 149 func (d *DB) StatusFailed(workflowId models.WorkflowId, workflowError string, exitCode int64, n *notifier.Notifier) error { 150 return d.createStatusEvent(workflowId, models.StatusKindFailed, &workflowError, &exitCode, n) 151 - } 152 - 153 - func (d *DB) StatusCancelled(workflowId models.WorkflowId, workflowError string, exitCode int64, n *notifier.Notifier) error { 154 - return d.createStatusEvent(workflowId, models.StatusKindCancelled, &workflowError, &exitCode, n) 155 } 156 157 func (d *DB) StatusSuccess(workflowId models.WorkflowId, n *notifier.Notifier) error {
··· 18 EventJson string `json:"event"` 19 } 20 21 + func (d *DB) InsertEvent(event Event, notifier *notifier.Notifier) error { 22 _, err := d.Exec( 23 `insert into events (rkey, nsid, event, created) values (?, ?, ?, ?)`, 24 event.Rkey, ··· 70 return evts, nil 71 } 72 73 + func (d *DB) CreateStatusEvent(rkey string, s tangled.PipelineStatus, n *notifier.Notifier) error { 74 + eventJson, err := json.Marshal(s) 75 + if err != nil { 76 + return err 77 + } 78 + 79 + event := Event{ 80 + Rkey: rkey, 81 + Nsid: tangled.PipelineStatusNSID, 82 + Created: time.Now().UnixNano(), 83 + EventJson: string(eventJson), 84 + } 85 + 86 + return d.InsertEvent(event, n) 87 + } 88 + 89 func (d *DB) createStatusEvent( 90 workflowId models.WorkflowId, 91 statusKind models.StatusKind, ··· 116 EventJson: string(eventJson), 117 } 118 119 + return d.InsertEvent(event, n) 120 121 } 122 ··· 164 165 func (d *DB) StatusFailed(workflowId models.WorkflowId, workflowError string, exitCode int64, n *notifier.Notifier) error { 166 return d.createStatusEvent(workflowId, models.StatusKindFailed, &workflowError, &exitCode, n) 167 } 168 169 func (d *DB) StatusSuccess(workflowId models.WorkflowId, n *notifier.Notifier) error {
+13 -24
spindle/engines/nixery/engine.go
··· 179 return err 180 } 181 e.registerCleanup(wid, func(ctx context.Context) error { 182 - if err := e.docker.NetworkRemove(ctx, networkName(wid)); err != nil { 183 - return fmt.Errorf("removing network: %w", err) 184 - } 185 - return nil 186 }) 187 188 addl := wf.Data.(addlFields) ··· 232 return fmt.Errorf("creating container: %w", err) 233 } 234 e.registerCleanup(wid, func(ctx context.Context) error { 235 - if err := e.docker.ContainerStop(ctx, resp.ID, container.StopOptions{}); err != nil { 236 - return fmt.Errorf("stopping container: %w", err) 237 } 238 239 - err := e.docker.ContainerRemove(ctx, resp.ID, container.RemoveOptions{ 240 RemoveVolumes: true, 241 RemoveLinks: false, 242 Force: false, 243 }) 244 - if err != nil { 245 - return fmt.Errorf("removing container: %w", err) 246 - } 247 - return nil 248 }) 249 250 - if err := e.docker.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil { 251 return fmt.Errorf("starting container: %w", err) 252 } 253 ··· 399 } 400 401 func (e *Engine) DestroyWorkflow(ctx context.Context, wid models.WorkflowId) error { 402 - fns := e.drainCleanups(wid) 403 404 for _, fn := range fns { 405 if err := fn(ctx); err != nil { ··· 415 416 key := wid.String() 417 e.cleanup[key] = append(e.cleanup[key], fn) 418 - } 419 - 420 - func (e *Engine) drainCleanups(wid models.WorkflowId) []cleanupFunc { 421 - e.cleanupMu.Lock() 422 - key := wid.String() 423 - 424 - fns := e.cleanup[key] 425 - delete(e.cleanup, key) 426 - e.cleanupMu.Unlock() 427 - 428 - return fns 429 } 430 431 func networkName(wid models.WorkflowId) string {
··· 179 return err 180 } 181 e.registerCleanup(wid, func(ctx context.Context) error { 182 + return e.docker.NetworkRemove(ctx, networkName(wid)) 183 }) 184 185 addl := wf.Data.(addlFields) ··· 229 return fmt.Errorf("creating container: %w", err) 230 } 231 e.registerCleanup(wid, func(ctx context.Context) error { 232 + err = e.docker.ContainerStop(ctx, resp.ID, container.StopOptions{}) 233 + if err != nil { 234 + return err 235 } 236 237 + return e.docker.ContainerRemove(ctx, resp.ID, container.RemoveOptions{ 238 RemoveVolumes: true, 239 RemoveLinks: false, 240 Force: false, 241 }) 242 }) 243 244 + err = e.docker.ContainerStart(ctx, resp.ID, container.StartOptions{}) 245 + if err != nil { 246 return fmt.Errorf("starting container: %w", err) 247 } 248 ··· 394 } 395 396 func (e *Engine) DestroyWorkflow(ctx context.Context, wid models.WorkflowId) error { 397 + e.cleanupMu.Lock() 398 + key := wid.String() 399 + 400 + fns := e.cleanup[key] 401 + delete(e.cleanup, key) 402 + e.cleanupMu.Unlock() 403 404 for _, fn := range fns { 405 if err := fn(ctx); err != nil { ··· 415 416 key := wid.String() 417 e.cleanup[key] = append(e.cleanup[key], fn) 418 } 419 420 func networkName(wid models.WorkflowId) string {
+2 -2
spindle/models/models.go
··· 53 StatusKindRunning, 54 } 55 FinishStates [4]StatusKind = [4]StatusKind{ 56 - StatusKindFailed, 57 - StatusKindTimeout, 58 StatusKindCancelled, 59 StatusKindSuccess, 60 } 61 ) 62
··· 53 StatusKindRunning, 54 } 55 FinishStates [4]StatusKind = [4]StatusKind{ 56 StatusKindCancelled, 57 + StatusKindFailed, 58 StatusKindSuccess, 59 + StatusKindTimeout, 60 } 61 ) 62
+1 -1
spindle/models/pipeline_env.go
··· 20 // Standard CI environment variable 21 env["CI"] = "true" 22 23 - env["TANGLED_PIPELINE_ID"] = pipelineId.AtUri().String() 24 25 // Repo info 26 if tr.Repo != nil {
··· 20 // Standard CI environment variable 21 env["CI"] = "true" 22 23 + env["TANGLED_PIPELINE_ID"] = pipelineId.Rkey 24 25 // Repo info 26 if tr.Repo != nil {
+4 -5
spindle/server.go
··· 286 Config: s.cfg, 287 Resolver: s.res, 288 Vault: s.vault, 289 - Notifier: s.Notifier(), 290 ServiceAuth: serviceAuth, 291 } 292 ··· 321 tpl.TriggerMetadata.Repo.Repo, 322 ) 323 if err != nil { 324 - return fmt.Errorf("failed to get repo: %w", err) 325 } 326 327 pipelineId := models.PipelineId{ ··· 342 Name: w.Name, 343 }, fmt.Sprintf("unknown engine %#v", w.Engine), -1, s.n) 344 if err != nil { 345 - return fmt.Errorf("db.StatusFailed: %w", err) 346 } 347 348 continue ··· 356 357 ewf, err := s.engs[w.Engine].InitWorkflow(*w, tpl) 358 if err != nil { 359 - return fmt.Errorf("init workflow: %w", err) 360 } 361 362 // inject TANGLED_* env vars after InitWorkflow ··· 373 Name: w.Name, 374 }, s.n) 375 if err != nil { 376 - return fmt.Errorf("db.StatusPending: %w", err) 377 } 378 } 379 }
··· 286 Config: s.cfg, 287 Resolver: s.res, 288 Vault: s.vault, 289 ServiceAuth: serviceAuth, 290 } 291 ··· 320 tpl.TriggerMetadata.Repo.Repo, 321 ) 322 if err != nil { 323 + return err 324 } 325 326 pipelineId := models.PipelineId{ ··· 341 Name: w.Name, 342 }, fmt.Sprintf("unknown engine %#v", w.Engine), -1, s.n) 343 if err != nil { 344 + return err 345 } 346 347 continue ··· 355 356 ewf, err := s.engs[w.Engine].InitWorkflow(*w, tpl) 357 if err != nil { 358 + return err 359 } 360 361 // inject TANGLED_* env vars after InitWorkflow ··· 372 Name: w.Name, 373 }, s.n) 374 if err != nil { 375 + return err 376 } 377 } 378 }
-97
spindle/xrpc/pipeline_cancelPipeline.go
··· 1 - package xrpc 2 - 3 - import ( 4 - "encoding/json" 5 - "fmt" 6 - "net/http" 7 - "strings" 8 - 9 - "github.com/bluesky-social/indigo/api/atproto" 10 - "github.com/bluesky-social/indigo/atproto/syntax" 11 - "github.com/bluesky-social/indigo/xrpc" 12 - securejoin "github.com/cyphar/filepath-securejoin" 13 - "tangled.org/core/api/tangled" 14 - "tangled.org/core/rbac" 15 - "tangled.org/core/spindle/models" 16 - xrpcerr "tangled.org/core/xrpc/errors" 17 - ) 18 - 19 - func (x *Xrpc) CancelPipeline(w http.ResponseWriter, r *http.Request) { 20 - l := x.Logger 21 - fail := func(e xrpcerr.XrpcError) { 22 - l.Error("failed", "kind", e.Tag, "error", e.Message) 23 - writeError(w, e, http.StatusBadRequest) 24 - } 25 - l.Debug("cancel pipeline") 26 - 27 - actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 28 - if !ok { 29 - fail(xrpcerr.MissingActorDidError) 30 - return 31 - } 32 - 33 - var input tangled.PipelineCancelPipeline_Input 34 - if err := json.NewDecoder(r.Body).Decode(&input); err != nil { 35 - fail(xrpcerr.GenericError(err)) 36 - return 37 - } 38 - 39 - aturi := syntax.ATURI(input.Pipeline) 40 - wid := models.WorkflowId{ 41 - PipelineId: models.PipelineId{ 42 - Knot: strings.TrimPrefix(aturi.Authority().String(), "did:web:"), 43 - Rkey: aturi.RecordKey().String(), 44 - }, 45 - Name: input.Workflow, 46 - } 47 - l.Debug("cancel pipeline", "wid", wid) 48 - 49 - // unfortunately we have to resolve repo-at here 50 - repoAt, err := syntax.ParseATURI(input.Repo) 51 - if err != nil { 52 - fail(xrpcerr.InvalidRepoError(input.Repo)) 53 - return 54 - } 55 - 56 - ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String()) 57 - if err != nil || ident.Handle.IsInvalidHandle() { 58 - fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err))) 59 - return 60 - } 61 - 62 - xrpcc := xrpc.Client{Host: ident.PDSEndpoint()} 63 - resp, err := atproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 64 - if err != nil { 65 - fail(xrpcerr.GenericError(err)) 66 - return 67 - } 68 - 69 - repo := resp.Value.Val.(*tangled.Repo) 70 - didSlashRepo, err := securejoin.SecureJoin(ident.DID.String(), repo.Name) 71 - if err != nil { 72 - fail(xrpcerr.GenericError(err)) 73 - return 74 - } 75 - 76 - // TODO: fine-grained role based control 77 - isRepoOwner, err := x.Enforcer.IsRepoOwner(actorDid.String(), rbac.ThisServer, didSlashRepo) 78 - if err != nil || !isRepoOwner { 79 - fail(xrpcerr.AccessControlError(actorDid.String())) 80 - return 81 - } 82 - for _, engine := range x.Engines { 83 - l.Debug("destorying workflow", "wid", wid) 84 - err = engine.DestroyWorkflow(r.Context(), wid) 85 - if err != nil { 86 - fail(xrpcerr.GenericError(fmt.Errorf("failed to destroy workflow: %w", err))) 87 - return 88 - } 89 - err = x.Db.StatusCancelled(wid, "User canceled the workflow", -1, x.Notifier) 90 - if err != nil { 91 - fail(xrpcerr.GenericError(fmt.Errorf("failed to emit status failed: %w", err))) 92 - return 93 - } 94 - } 95 - 96 - w.WriteHeader(http.StatusOK) 97 - }
···
-3
spindle/xrpc/xrpc.go
··· 10 11 "tangled.org/core/api/tangled" 12 "tangled.org/core/idresolver" 13 - "tangled.org/core/notifier" 14 "tangled.org/core/rbac" 15 "tangled.org/core/spindle/config" 16 "tangled.org/core/spindle/db" ··· 30 Config *config.Config 31 Resolver *idresolver.Resolver 32 Vault secrets.Manager 33 - Notifier *notifier.Notifier 34 ServiceAuth *serviceauth.ServiceAuth 35 } 36 ··· 43 r.Post("/"+tangled.RepoAddSecretNSID, x.AddSecret) 44 r.Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret) 45 r.Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets) 46 - r.Post("/"+tangled.PipelineCancelPipelineNSID, x.CancelPipeline) 47 }) 48 49 // service query endpoints (no auth required)
··· 10 11 "tangled.org/core/api/tangled" 12 "tangled.org/core/idresolver" 13 "tangled.org/core/rbac" 14 "tangled.org/core/spindle/config" 15 "tangled.org/core/spindle/db" ··· 29 Config *config.Config 30 Resolver *idresolver.Resolver 31 Vault secrets.Manager 32 ServiceAuth *serviceauth.ServiceAuth 33 } 34 ··· 41 r.Post("/"+tangled.RepoAddSecretNSID, x.AddSecret) 42 r.Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret) 43 r.Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets) 44 }) 45 46 // service query endpoints (no auth required)
+30 -78
types/diff.go
··· 1 package types 2 3 import ( 4 - "net/url" 5 - 6 "github.com/bluekeyes/go-gitdiff/gitdiff" 7 - "tangled.org/core/appview/filetree" 8 ) 9 10 type DiffOpts struct { 11 Split bool `json:"split"` 12 } 13 14 - func (d DiffOpts) Encode() string { 15 - values := make(url.Values) 16 - if d.Split { 17 - values.Set("diff", "split") 18 - } else { 19 - values.Set("diff", "unified") 20 - } 21 - return values.Encode() 22 - } 23 - 24 - // A nicer git diff representation. 25 - type NiceDiff struct { 26 - Commit Commit `json:"commit"` 27 - Stat DiffStat `json:"stat"` 28 - Diff []Diff `json:"diff"` 29 } 30 31 type Diff struct { ··· 41 IsRename bool `json:"is_rename"` 42 } 43 44 - func (d Diff) Stats() DiffFileStat { 45 - var stats DiffFileStat 46 for _, f := range d.TextFragments { 47 stats.Insertions += f.LinesAdded 48 stats.Deletions += f.LinesDeleted ··· 50 return stats 51 } 52 53 - type DiffStat struct { 54 - Insertions int64 `json:"insertions"` 55 - Deletions int64 `json:"deletions"` 56 - FilesChanged int `json:"files_changed"` 57 - } 58 - 59 - type DiffFileStat struct { 60 - Insertions int64 61 - Deletions int64 62 } 63 64 type DiffTree struct { ··· 68 Diff []*gitdiff.File `json:"diff"` 69 } 70 71 - type DiffFileName struct { 72 - Old string 73 - New string 74 - } 75 - 76 - func (d NiceDiff) ChangedFiles() []DiffFileRenderer { 77 - drs := make([]DiffFileRenderer, len(d.Diff)) 78 - for i, s := range d.Diff { 79 - drs[i] = s 80 - } 81 - return drs 82 - } 83 84 - func (d NiceDiff) FileTree() *filetree.FileTreeNode { 85 - fs := make([]string, len(d.Diff)) 86 - for i, s := range d.Diff { 87 - n := s.Names() 88 - if n.New == "" { 89 - fs[i] = n.Old 90 } else { 91 - fs[i] = n.New 92 } 93 } 94 - return filetree.FileTree(fs) 95 - } 96 97 - func (d NiceDiff) Stats() DiffStat { 98 - return d.Stat 99 } 100 101 - func (d Diff) Id() string { 102 if d.IsDelete { 103 return d.Name.Old 104 } 105 return d.Name.New 106 } 107 108 - func (d Diff) Names() DiffFileName { 109 - var n DiffFileName 110 - if d.IsDelete { 111 - n.Old = d.Name.Old 112 - return n 113 - } else if d.IsCopy || d.IsRename { 114 - n.Old = d.Name.Old 115 - n.New = d.Name.New 116 - return n 117 - } else { 118 - n.New = d.Name.New 119 - return n 120 - } 121 - } 122 - 123 - func (d Diff) CanRender() string { 124 - if d.IsBinary { 125 - return "This is a binary file and will not be displayed." 126 - } 127 - 128 - return "" 129 - } 130 - 131 - func (d Diff) Split() SplitDiff { 132 fragments := make([]SplitFragment, len(d.TextFragments)) 133 for i, fragment := range d.TextFragments { 134 leftLines, rightLines := SeparateLines(&fragment) ··· 139 } 140 } 141 142 - return SplitDiff{ 143 Name: d.Id(), 144 TextFragments: fragments, 145 }
··· 1 package types 2 3 import ( 4 "github.com/bluekeyes/go-gitdiff/gitdiff" 5 ) 6 7 type DiffOpts struct { 8 Split bool `json:"split"` 9 } 10 11 + type TextFragment struct { 12 + Header string `json:"comment"` 13 + Lines []gitdiff.Line `json:"lines"` 14 } 15 16 type Diff struct { ··· 26 IsRename bool `json:"is_rename"` 27 } 28 29 + type DiffStat struct { 30 + Insertions int64 31 + Deletions int64 32 + } 33 + 34 + func (d *Diff) Stats() DiffStat { 35 + var stats DiffStat 36 for _, f := range d.TextFragments { 37 stats.Insertions += f.LinesAdded 38 stats.Deletions += f.LinesDeleted ··· 40 return stats 41 } 42 43 + // A nicer git diff representation. 44 + type NiceDiff struct { 45 + Commit Commit `json:"commit"` 46 + Stat struct { 47 + FilesChanged int `json:"files_changed"` 48 + Insertions int `json:"insertions"` 49 + Deletions int `json:"deletions"` 50 + } `json:"stat"` 51 + Diff []Diff `json:"diff"` 52 } 53 54 type DiffTree struct { ··· 58 Diff []*gitdiff.File `json:"diff"` 59 } 60 61 + func (d *NiceDiff) ChangedFiles() []string { 62 + files := make([]string, len(d.Diff)) 63 64 + for i, f := range d.Diff { 65 + if f.IsDelete { 66 + files[i] = f.Name.Old 67 } else { 68 + files[i] = f.Name.New 69 } 70 } 71 72 + return files 73 } 74 75 + // used by html elements as a unique ID for hrefs 76 + func (d *Diff) Id() string { 77 if d.IsDelete { 78 return d.Name.Old 79 } 80 return d.Name.New 81 } 82 83 + func (d *Diff) Split() *SplitDiff { 84 fragments := make([]SplitFragment, len(d.TextFragments)) 85 for i, fragment := range d.TextFragments { 86 leftLines, rightLines := SeparateLines(&fragment) ··· 91 } 92 } 93 94 + return &SplitDiff{ 95 Name: d.Id(), 96 TextFragments: fragments, 97 }
-31
types/diff_renderer.go
··· 1 - package types 2 - 3 - import "tangled.org/core/appview/filetree" 4 - 5 - type DiffRenderer interface { 6 - // list of file affected by these diffs 7 - ChangedFiles() []DiffFileRenderer 8 - 9 - // filetree 10 - FileTree() *filetree.FileTreeNode 11 - 12 - Stats() DiffStat 13 - } 14 - 15 - type DiffFileRenderer interface { 16 - // html ID for each file in the diff 17 - Id() string 18 - 19 - // produce a splitdiff 20 - Split() SplitDiff 21 - 22 - // stats for this single file 23 - Stats() DiffFileStat 24 - 25 - // old and new name of file 26 - Names() DiffFileName 27 - 28 - // whether this diff can be displayed, 29 - // returns a reason if not, and the empty string if it can 30 - CanRender() string 31 - }
···
+2 -11
types/diff_test.go
··· 1 package types 2 3 - import ( 4 - "testing" 5 - ) 6 7 func TestDiffId(t *testing.T) { 8 tests := []struct { ··· 107 } 108 109 for i, diff := range nd.Diff { 110 - if changedFiles[i].Id() != diff.Id() { 111 t.Errorf("ChangedFiles()[%d] = %q, but Diff.Id() = %q", i, changedFiles[i], diff.Id()) 112 } 113 } 114 } 115 - 116 - func TestImplsInterfaces(t *testing.T) { 117 - nd := NiceDiff{} 118 - _ = isDiffsRenderer(nd) 119 - } 120 - 121 - func isDiffsRenderer[S DiffRenderer](S) bool { return true }
··· 1 package types 2 3 + import "testing" 4 5 func TestDiffId(t *testing.T) { 6 tests := []struct { ··· 105 } 106 107 for i, diff := range nd.Diff { 108 + if changedFiles[i] != diff.Id() { 109 t.Errorf("ChangedFiles()[%d] = %q, but Diff.Id() = %q", i, changedFiles[i], diff.Id()) 110 } 111 } 112 }
+2 -1
types/split.go
··· 22 TextFragments []SplitFragment `json:"fragments"` 23 } 24 25 - func (d SplitDiff) Id() string { 26 return d.Name 27 } 28
··· 22 TextFragments []SplitFragment `json:"fragments"` 23 } 24 25 + // used by html elements as a unique ID for hrefs 26 + func (d *SplitDiff) Id() string { 27 return d.Name 28 } 29