···1818// RECORDTYPE: ActorProfile
1919type ActorProfile struct {
2020 LexiconTypeID string `json:"$type,const=sh.tangled.actor.profile" cborgen:"$type,const=sh.tangled.actor.profile"`
2121+ // avatar: Small image to be displayed next to posts from account. AKA, 'profile picture'
2222+ Avatar *util.LexBlob `json:"avatar,omitempty" cborgen:"avatar,omitempty"`
2123 // bluesky: Include link to this account on Bluesky.
2224 Bluesky bool `json:"bluesky" cborgen:"bluesky"`
2325 // description: Free-form profile description text.
···6868// - this handler should calculate the diff in order to create the labelop record
6969// - we need the diff in order to maintain a "history" of operations performed by users
7070func (l *Labels) PerformLabelOp(w http.ResponseWriter, r *http.Request) {
7171- user := l.oauth.GetUser(r)
7171+ user := l.oauth.GetMultiAccountUser(r)
72727373 noticeId := "add-label-error"
7474···8282 return
8383 }
84848585- did := user.Did
8585+ did := user.Active.Did
8686 rkey := tid.TID()
8787 performedAt := time.Now()
8888 indexedAt := time.Now()
+6-8
appview/middleware/middleware.go
···115115 return func(next http.Handler) http.Handler {
116116 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
117117 // requires auth also
118118- actor := mw.oauth.GetUser(r)
118118+ actor := mw.oauth.GetMultiAccountUser(r)
119119 if actor == nil {
120120 // we need a logged in user
121121 log.Printf("not logged in, redirecting")
···128128 return
129129 }
130130131131- ok, err := mw.enforcer.E.HasGroupingPolicy(actor.Did, group, domain)
131131+ ok, err := mw.enforcer.E.HasGroupingPolicy(actor.Active.Did, group, domain)
132132 if err != nil || !ok {
133133- // we need a logged in user
134134- log.Printf("%s does not have perms of a %s in domain %s", actor.Did, group, domain)
133133+ log.Printf("%s does not have perms of a %s in domain %s", actor.Active.Did, group, domain)
135134 http.Error(w, "Forbiden", http.StatusUnauthorized)
136135 return
137136 }
···149148 return func(next http.Handler) http.Handler {
150149 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
151150 // requires auth also
152152- actor := mw.oauth.GetUser(r)
151151+ actor := mw.oauth.GetMultiAccountUser(r)
153152 if actor == nil {
154153 // we need a logged in user
155154 log.Printf("not logged in, redirecting")
···162161 return
163162 }
164163165165- ok, err := mw.enforcer.E.Enforce(actor.Did, f.Knot, f.DidSlashRepo(), requiredPerm)
164164+ ok, err := mw.enforcer.E.Enforce(actor.Active.Did, f.Knot, f.DidSlashRepo(), requiredPerm)
166165 if err != nil || !ok {
167167- // we need a logged in user
168168- log.Printf("%s does not have perms of a %s in repo %s", actor.Did, requiredPerm, f.DidSlashRepo())
166166+ log.Printf("%s does not have perms of a %s in repo %s", actor.Active.Did, requiredPerm, f.DidSlashRepo())
169167 http.Error(w, "Forbiden", http.StatusUnauthorized)
170168 return
171169 }
+38
appview/models/pipeline.go
···33import (
44 "fmt"
55 "slices"
66+ "strings"
67 "time"
7889 "github.com/bluesky-social/indigo/atproto/syntax"
···5657 }
57585859 return 0
6060+}
6161+6262+// produces short summary of successes:
6363+// - "0/4" when zero successes of 4 workflows
6464+// - "4/4" when all successes of 4 workflows
6565+// - "0/0" when no workflows run in this pipeline
6666+func (p Pipeline) ShortStatusSummary() string {
6767+ counts := make(map[spindle.StatusKind]int)
6868+ for _, w := range p.Statuses {
6969+ counts[w.Latest().Status] += 1
7070+ }
7171+7272+ total := len(p.Statuses)
7373+ successes := counts[spindle.StatusKindSuccess]
7474+7575+ return fmt.Sprintf("%d/%d", successes, total)
7676+}
7777+7878+// produces a string of the form "3/4 success, 2/4 failed, 1/4 pending"
7979+func (p Pipeline) LongStatusSummary() string {
8080+ counts := make(map[spindle.StatusKind]int)
8181+ for _, w := range p.Statuses {
8282+ counts[w.Latest().Status] += 1
8383+ }
8484+8585+ total := len(p.Statuses)
8686+8787+ var result []string
8888+ // finish states first, followed by start states
8989+ states := append(spindle.FinishStates[:], spindle.StartStates[:]...)
9090+ for _, state := range states {
9191+ if count, ok := counts[state]; ok {
9292+ result = append(result, fmt.Sprintf("%d/%d %s", count, total, state.String()))
9393+ }
9494+ }
9595+9696+ return strings.Join(result, ", ")
5997}
60986199func (p Pipeline) Counts() map[string]int {
+1
appview/models/profile.go
···1313 Did string
14141515 // data
1616+ Avatar string // CID of the avatar blob
1617 Description string
1718 IncludeBluesky bool
1819 Location string
+7-17
appview/models/pull.go
···171171 return syntax.ATURI(p.CommentAt)
172172}
173173174174-// func (p *PullComment) AsRecord() tangled.RepoPullComment {
175175-// mentions := make([]string, len(p.Mentions))
176176-// for i, did := range p.Mentions {
177177-// mentions[i] = string(did)
178178-// }
179179-// references := make([]string, len(p.References))
180180-// for i, uri := range p.References {
181181-// references[i] = string(uri)
182182-// }
183183-// return tangled.RepoPullComment{
184184-// Pull: p.PullAt,
185185-// Body: p.Body,
186186-// Mentions: mentions,
187187-// References: references,
188188-// CreatedAt: p.Created.Format(time.RFC3339),
189189-// }
190190-// }
174174+func (p *Pull) TotalComments() int {
175175+ total := 0
176176+ for _, s := range p.Submissions {
177177+ total += len(s.Comments)
178178+ }
179179+ return total
180180+}
191181192182func (p *Pull) LastRoundNumber() int {
193183 return len(p.Submissions) - 1
+4-1
appview/models/repo.go
···130130131131 // current display mode
132132 ShowingRendered bool // currently in rendered mode
133133- ShowingText bool // currently in text/code mode
134133135134 // content type flags
136135 ContentType BlobContentType
···151150 // no view available, only raw
152151 return !(b.HasRenderedView || b.HasTextView)
153152}
153153+154154+func (b BlobView) ShowingText() bool {
155155+ return !b.ShowingRendered
156156+}
+6-6
appview/notifications/notifications.go
···48484949func (n *Notifications) notificationsPage(w http.ResponseWriter, r *http.Request) {
5050 l := n.logger.With("handler", "notificationsPage")
5151- user := n.oauth.GetUser(r)
5151+ user := n.oauth.GetMultiAccountUser(r)
52525353 page := pagination.FromContext(r.Context())
54545555 total, err := db.CountNotifications(
5656 n.db,
5757- orm.FilterEq("recipient_did", user.Did),
5757+ orm.FilterEq("recipient_did", user.Active.Did),
5858 )
5959 if err != nil {
6060 l.Error("failed to get total notifications", "err", err)
···6565 notifications, err := db.GetNotificationsWithEntities(
6666 n.db,
6767 page,
6868- orm.FilterEq("recipient_did", user.Did),
6868+ orm.FilterEq("recipient_did", user.Active.Did),
6969 )
7070 if err != nil {
7171 l.Error("failed to get notifications", "err", err)
···7373 return
7474 }
75757676- err = db.MarkAllNotificationsRead(n.db, user.Did)
7676+ err = db.MarkAllNotificationsRead(n.db, user.Active.Did)
7777 if err != nil {
7878 l.Error("failed to mark notifications as read", "err", err)
7979 }
···9090}
91919292func (n *Notifications) getUnreadCount(w http.ResponseWriter, r *http.Request) {
9393- user := n.oauth.GetUser(r)
9393+ user := n.oauth.GetMultiAccountUser(r)
9494 if user == nil {
9595 http.Error(w, "Forbidden", http.StatusUnauthorized)
9696 return
···98989999 count, err := db.CountNotifications(
100100 n.db,
101101- orm.FilterEq("recipient_did", user.Did),
101101+ orm.FilterEq("recipient_did", user.Active.Did),
102102 orm.FilterEq("read", 0),
103103 )
104104 if err != nil {
···3030 <div class="mx-6">
3131 These services may not be fully accessible until upgraded.
3232 <a class="underline text-red-800 dark:text-red-200"
3333- href="https://docs.tangled.org/migrating-knots-spindles.html#migrating-knots-spindles">
3333+ href="https://docs.tangled.org/migrating-knots-and-spindles.html">
3434 Click to read the upgrade guide</a>.
3535 </div>
3636 </details>
···2121 <div class="col-span-1 md:col-span-2">
2222 <h2 class="text-sm pb-2 uppercase font-bold">SSH Keys</h2>
2323 <p class="text-gray-500 dark:text-gray-400">
2424- SSH public keys added here will be broadcasted to knots that you are a member of,
2424+ SSH public keys added here will be broadcasted to knots that you are a member of,
2525 allowing you to push to repositories there.
2626 </p>
2727 </div>
···6363 hx-swap="none"
6464 class="flex flex-col gap-2"
6565>
6666- <p class="uppercase p-0">ADD SSH KEY</p>
6666+ <label for="key-name" class="uppercase p-0">
6767+ add ssh key
6868+ </label>
6769 <p class="text-sm text-gray-500 dark:text-gray-400">SSH keys allow you to push to repositories in knots you're a member of.</p>
6870 <input
6971 type="text"
+2-2
appview/pipelines/pipelines.go
···7777}
78787979func (p *Pipelines) Index(w http.ResponseWriter, r *http.Request) {
8080- user := p.oauth.GetUser(r)
8080+ user := p.oauth.GetMultiAccountUser(r)
8181 l := p.logger.With("handler", "Index")
82828383 f, err := p.repoResolver.Resolve(r)
···106106}
107107108108func (p *Pipelines) Workflow(w http.ResponseWriter, r *http.Request) {
109109- user := p.oauth.GetUser(r)
109109+ user := p.oauth.GetMultiAccountUser(r)
110110 l := p.logger.With("handler", "Workflow")
111111112112 f, err := p.repoResolver.Resolve(r)
···375375KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555
376376```
377377378378-If you run a Linux distribution that uses systemd, you can use the provided
379379-service file to run the server. Copy
380380-[`knotserver.service`](/systemd/knotserver.service)
378378+If you run a Linux distribution that uses systemd, you can
379379+use the provided service file to run the server. Copy
380380+[`knotserver.service`](https://tangled.org/tangled.org/core/blob/master/systemd/knotserver.service)
381381to `/etc/systemd/system/`. Then, run:
382382383383```
···692692 NODE_ENV: "production"
693693 MY_ENV_VAR: "MY_ENV_VALUE"
694694```
695695+696696+By default, the following environment variables set:
697697+698698+- `CI` - Always set to `true` to indicate a CI environment
699699+- `TANGLED_PIPELINE_ID` - The AT URI of the current pipeline
700700+- `TANGLED_REPO_KNOT` - The repository's knot hostname
701701+- `TANGLED_REPO_DID` - The DID of the repository owner
702702+- `TANGLED_REPO_NAME` - The name of the repository
703703+- `TANGLED_REPO_DEFAULT_BRANCH` - The default branch of the
704704+ repository
705705+- `TANGLED_REPO_URL` - The full URL to the repository
706706+707707+These variables are only available when the pipeline is
708708+triggered by a push:
709709+710710+- `TANGLED_REF` - The full git reference (e.g.,
711711+ `refs/heads/main` or `refs/tags/v1.0.0`)
712712+- `TANGLED_REF_NAME` - The short name of the reference
713713+ (e.g., `main` or `v1.0.0`)
714714+- `TANGLED_REF_TYPE` - The type of reference, either
715715+ `branch` or `tag`
716716+- `TANGLED_SHA` - The commit SHA that triggered the pipeline
717717+- `TANGLED_COMMIT_SHA` - Alias for `TANGLED_SHA`
718718+719719+These variables are only available when the pipeline is
720720+triggered by a pull request:
721721+722722+- `TANGLED_PR_SOURCE_BRANCH` - The source branch of the pull
723723+ request
724724+- `TANGLED_PR_TARGET_BRANCH` - The target branch of the pull
725725+ request
726726+- `TANGLED_PR_SOURCE_SHA` - The commit SHA of the source
727727+ branch
695728696729### Steps
697730
+3
flake.nix
···284284 rm -f api/tangled/*
285285 lexgen --build-file lexicon-build-config.json lexicons
286286 sed -i.bak 's/\tutil/\/\/\tutil/' api/tangled/*
287287+ # lexgen generates incomplete Marshaler/Unmarshaler for union types
288288+ find api/tangled/*.go -not -name "cbor_gen.go" -exec \
289289+ sed -i '/^func.*\(MarshalCBOR\|UnmarshalCBOR\)/,/^}/ s/^/\/\/ /' {} +
287290 ${pkgs.gotools}/bin/goimports -w api/tangled/*
288291 go run ./cmd/cborgen/
289292 lexgen --build-file lexicon-build-config.json lexicons
···11package types
2233import (
44+ "net/url"
55+46 "github.com/bluekeyes/go-gitdiff/gitdiff"
77+ "tangled.org/core/appview/filetree"
58)
69710type DiffOpts struct {
811 Split bool `json:"split"`
912}
10131111-type TextFragment struct {
1212- Header string `json:"comment"`
1313- Lines []gitdiff.Line `json:"lines"`
1414+func (d DiffOpts) Encode() string {
1515+ values := make(url.Values)
1616+ if d.Split {
1717+ values.Set("diff", "split")
1818+ } else {
1919+ values.Set("diff", "unified")
2020+ }
2121+ return values.Encode()
2222+}
2323+2424+// A nicer git diff representation.
2525+type NiceDiff struct {
2626+ Commit Commit `json:"commit"`
2727+ Stat DiffStat `json:"stat"`
2828+ Diff []Diff `json:"diff"`
1429}
15301631type Diff struct {
···2641 IsRename bool `json:"is_rename"`
2742}
28432929-type DiffStat struct {
3030- Insertions int64
3131- Deletions int64
3232-}
3333-3434-func (d *Diff) Stats() DiffStat {
3535- var stats DiffStat
4444+func (d Diff) Stats() DiffFileStat {
4545+ var stats DiffFileStat
3646 for _, f := range d.TextFragments {
3747 stats.Insertions += f.LinesAdded
3848 stats.Deletions += f.LinesDeleted
···4050 return stats
4151}
42524343-// A nicer git diff representation.
4444-type NiceDiff struct {
4545- Commit Commit `json:"commit"`
4646- Stat struct {
4747- FilesChanged int `json:"files_changed"`
4848- Insertions int `json:"insertions"`
4949- Deletions int `json:"deletions"`
5050- } `json:"stat"`
5151- Diff []Diff `json:"diff"`
5353+type DiffStat struct {
5454+ Insertions int64 `json:"insertions"`
5555+ Deletions int64 `json:"deletions"`
5656+ FilesChanged int `json:"files_changed"`
5757+}
5858+5959+type DiffFileStat struct {
6060+ Insertions int64
6161+ Deletions int64
5262}
53635464type DiffTree struct {
···5868 Diff []*gitdiff.File `json:"diff"`
5969}
60706161-func (d *NiceDiff) ChangedFiles() []string {
6262- files := make([]string, len(d.Diff))
7171+type DiffFileName struct {
7272+ Old string
7373+ New string
7474+}
63756464- for i, f := range d.Diff {
6565- if f.IsDelete {
6666- files[i] = f.Name.Old
7676+func (d NiceDiff) ChangedFiles() []DiffFileRenderer {
7777+ drs := make([]DiffFileRenderer, len(d.Diff))
7878+ for i, s := range d.Diff {
7979+ drs[i] = s
8080+ }
8181+ return drs
8282+}
8383+8484+func (d NiceDiff) FileTree() *filetree.FileTreeNode {
8585+ fs := make([]string, len(d.Diff))
8686+ for i, s := range d.Diff {
8787+ n := s.Names()
8888+ if n.New == "" {
8989+ fs[i] = n.Old
6790 } else {
6868- files[i] = f.Name.New
9191+ fs[i] = n.New
6992 }
7093 }
9494+ return filetree.FileTree(fs)
9595+}
71967272- return files
9797+func (d NiceDiff) Stats() DiffStat {
9898+ return d.Stat
7399}
741007575-// used by html elements as a unique ID for hrefs
7676-func (d *Diff) Id() string {
101101+func (d Diff) Id() string {
77102 if d.IsDelete {
78103 return d.Name.Old
79104 }
80105 return d.Name.New
81106}
821078383-func (d *Diff) Split() *SplitDiff {
108108+func (d Diff) Names() DiffFileName {
109109+ var n DiffFileName
110110+ if d.IsDelete {
111111+ n.Old = d.Name.Old
112112+ return n
113113+ } else if d.IsCopy || d.IsRename {
114114+ n.Old = d.Name.Old
115115+ n.New = d.Name.New
116116+ return n
117117+ } else {
118118+ n.New = d.Name.New
119119+ return n
120120+ }
121121+}
122122+123123+func (d Diff) CanRender() string {
124124+ if d.IsBinary {
125125+ return "This is a binary file and will not be displayed."
126126+ }
127127+128128+ return ""
129129+}
130130+131131+func (d Diff) Split() SplitDiff {
84132 fragments := make([]SplitFragment, len(d.TextFragments))
85133 for i, fragment := range d.TextFragments {
86134 leftLines, rightLines := SeparateLines(&fragment)
···91139 }
92140 }
931419494- return &SplitDiff{
142142+ return SplitDiff{
95143 Name: d.Id(),
96144 TextFragments: fragments,
97145 }
+31
types/diff_renderer.go
···11+package types
22+33+import "tangled.org/core/appview/filetree"
44+55+type DiffRenderer interface {
66+ // list of file affected by these diffs
77+ ChangedFiles() []DiffFileRenderer
88+99+ // filetree
1010+ FileTree() *filetree.FileTreeNode
1111+1212+ Stats() DiffStat
1313+}
1414+1515+type DiffFileRenderer interface {
1616+ // html ID for each file in the diff
1717+ Id() string
1818+1919+ // produce a splitdiff
2020+ Split() SplitDiff
2121+2222+ // stats for this single file
2323+ Stats() DiffFileStat
2424+2525+ // old and new name of file
2626+ Names() DiffFileName
2727+2828+ // whether this diff can be displayed,
2929+ // returns a reason if not, and the empty string if it can
3030+ CanRender() string
3131+}
···2222 TextFragments []SplitFragment `json:"fragments"`
2323}
24242525-// used by html elements as a unique ID for hrefs
2626-func (d *SplitDiff) Id() string {
2525+func (d SplitDiff) Id() string {
2726 return d.Name
2827}
2928