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

Compare changes

Choose any two refs to compare.

Changed files
+712 -243
api
appview
db
knots
middleware
models
pages
templates
knots
layouts
fragments
repo
spindles
pulls
repo
spindles
state
docs
knotserver
lexicons
nix
spindle
types
+79 -20
api/tangled/cbor_gen.go
··· 7934 7934 } 7935 7935 7936 7936 cw := cbg.NewCborWriter(w) 7937 - fieldCount := 9 7937 + fieldCount := 10 7938 7938 7939 7939 if t.Body == nil { 7940 7940 fieldCount-- 7941 7941 } 7942 7942 7943 7943 if t.Mentions == nil { 7944 + fieldCount-- 7945 + } 7946 + 7947 + if t.Patch == nil { 7944 7948 fieldCount-- 7945 7949 } 7946 7950 ··· 8008 8012 } 8009 8013 8010 8014 // t.Patch (string) (string) 8011 - if len("patch") > 1000000 { 8012 - return xerrors.Errorf("Value in field \"patch\" was too long") 8013 - } 8015 + if t.Patch != nil { 8014 8016 8015 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patch"))); err != nil { 8016 - return err 8017 - } 8018 - if _, err := cw.WriteString(string("patch")); err != nil { 8019 - return err 8020 - } 8017 + if len("patch") > 1000000 { 8018 + return xerrors.Errorf("Value in field \"patch\" was too long") 8019 + } 8021 8020 8022 - if len(t.Patch) > 1000000 { 8023 - return xerrors.Errorf("Value in field t.Patch was too long") 8024 - } 8021 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patch"))); err != nil { 8022 + return err 8023 + } 8024 + if _, err := cw.WriteString(string("patch")); err != nil { 8025 + return err 8026 + } 8027 + 8028 + if t.Patch == nil { 8029 + if _, err := cw.Write(cbg.CborNull); err != nil { 8030 + return err 8031 + } 8032 + } else { 8033 + if len(*t.Patch) > 1000000 { 8034 + return xerrors.Errorf("Value in field t.Patch was too long") 8035 + } 8025 8036 8026 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Patch))); err != nil { 8027 - return err 8028 - } 8029 - if _, err := cw.WriteString(string(t.Patch)); err != nil { 8030 - return err 8037 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Patch))); err != nil { 8038 + return err 8039 + } 8040 + if _, err := cw.WriteString(string(*t.Patch)); err != nil { 8041 + return err 8042 + } 8043 + } 8031 8044 } 8032 8045 8033 8046 // t.Title (string) (string) ··· 8147 8160 return err 8148 8161 } 8149 8162 8163 + // t.PatchBlob (util.LexBlob) (struct) 8164 + if len("patchBlob") > 1000000 { 8165 + return xerrors.Errorf("Value in field \"patchBlob\" was too long") 8166 + } 8167 + 8168 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patchBlob"))); err != nil { 8169 + return err 8170 + } 8171 + if _, err := cw.WriteString(string("patchBlob")); err != nil { 8172 + return err 8173 + } 8174 + 8175 + if err := t.PatchBlob.MarshalCBOR(cw); err != nil { 8176 + return err 8177 + } 8178 + 8150 8179 // t.References ([]string) (slice) 8151 8180 if t.References != nil { 8152 8181 ··· 8262 8291 case "patch": 8263 8292 8264 8293 { 8265 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 8294 + b, err := cr.ReadByte() 8266 8295 if err != nil { 8267 8296 return err 8268 8297 } 8298 + if b != cbg.CborNull[0] { 8299 + if err := cr.UnreadByte(); err != nil { 8300 + return err 8301 + } 8269 8302 8270 - t.Patch = string(sval) 8303 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 8304 + if err != nil { 8305 + return err 8306 + } 8307 + 8308 + t.Patch = (*string)(&sval) 8309 + } 8271 8310 } 8272 8311 // t.Title (string) (string) 8273 8312 case "title": ··· 8370 8409 } 8371 8410 8372 8411 t.CreatedAt = string(sval) 8412 + } 8413 + // t.PatchBlob (util.LexBlob) (struct) 8414 + case "patchBlob": 8415 + 8416 + { 8417 + 8418 + b, err := cr.ReadByte() 8419 + if err != nil { 8420 + return err 8421 + } 8422 + if b != cbg.CborNull[0] { 8423 + if err := cr.UnreadByte(); err != nil { 8424 + return err 8425 + } 8426 + t.PatchBlob = new(util.LexBlob) 8427 + if err := t.PatchBlob.UnmarshalCBOR(cr); err != nil { 8428 + return xerrors.Errorf("unmarshaling t.PatchBlob pointer: %w", err) 8429 + } 8430 + } 8431 + 8373 8432 } 8374 8433 // t.References ([]string) (slice) 8375 8434 case "references":
+51
api/tangled/repolistRepos.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package tangled 4 + 5 + // schema: sh.tangled.repo.listRepos 6 + 7 + import ( 8 + "context" 9 + 10 + "github.com/bluesky-social/indigo/lex/util" 11 + ) 12 + 13 + const ( 14 + RepoListReposNSID = "sh.tangled.repo.listRepos" 15 + ) 16 + 17 + // RepoListRepos_Output is the output of a sh.tangled.repo.listRepos call. 18 + type RepoListRepos_Output struct { 19 + Users []*RepoListRepos_User `json:"users" cborgen:"users"` 20 + } 21 + 22 + // RepoListRepos_RepoEntry is a "repoEntry" in the sh.tangled.repo.listRepos schema. 23 + type RepoListRepos_RepoEntry struct { 24 + // defaultBranch: Default branch of the repository 25 + DefaultBranch *string `json:"defaultBranch,omitempty" cborgen:"defaultBranch,omitempty"` 26 + // did: DID of the repository owner 27 + Did string `json:"did" cborgen:"did"` 28 + // fullPath: Full path to the repository 29 + FullPath string `json:"fullPath" cborgen:"fullPath"` 30 + // name: Repository name 31 + Name string `json:"name" cborgen:"name"` 32 + } 33 + 34 + // RepoListRepos_User is a "user" in the sh.tangled.repo.listRepos schema. 35 + type RepoListRepos_User struct { 36 + // did: DID of the user 37 + Did string `json:"did" cborgen:"did"` 38 + Repos []*RepoListRepos_RepoEntry `json:"repos" cborgen:"repos"` 39 + } 40 + 41 + // RepoListRepos calls the XRPC method "sh.tangled.repo.listRepos". 42 + func RepoListRepos(ctx context.Context, c util.LexClient) (*RepoListRepos_Output, error) { 43 + var out RepoListRepos_Output 44 + 45 + params := map[string]interface{}{} 46 + if err := c.LexDo(ctx, util.Query, "", "sh.tangled.repo.listRepos", params, nil, &out); err != nil { 47 + return nil, err 48 + } 49 + 50 + return &out, nil 51 + }
+12 -9
api/tangled/repopull.go
··· 17 17 } // 18 18 // RECORDTYPE: RepoPull 19 19 type RepoPull struct { 20 - LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"` 21 - Body *string `json:"body,omitempty" cborgen:"body,omitempty"` 22 - CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 - Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"` 24 - Patch string `json:"patch" cborgen:"patch"` 25 - References []string `json:"references,omitempty" cborgen:"references,omitempty"` 26 - Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"` 27 - Target *RepoPull_Target `json:"target" cborgen:"target"` 28 - Title string `json:"title" cborgen:"title"` 20 + LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"` 21 + Body *string `json:"body,omitempty" cborgen:"body,omitempty"` 22 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 + Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"` 24 + // patch: (deprecated) use patchBlob instead 25 + Patch *string `json:"patch,omitempty" cborgen:"patch,omitempty"` 26 + // patchBlob: patch content 27 + PatchBlob *util.LexBlob `json:"patchBlob" cborgen:"patchBlob"` 28 + References []string `json:"references,omitempty" cborgen:"references,omitempty"` 29 + Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"` 30 + Target *RepoPull_Target `json:"target" cborgen:"target"` 31 + Title string `json:"title" cborgen:"title"` 29 32 } 30 33 31 34 // RepoPull_Source is a "source" in the sh.tangled.repo.pull schema.
+18 -11
appview/db/profile.go
··· 20 20 timeline := models.ProfileTimeline{ 21 21 ByMonth: make([]models.ByMonth, TimeframeMonths), 22 22 } 23 - currentMonth := time.Now().Month() 23 + now := time.Now() 24 24 timeframe := fmt.Sprintf("-%d months", TimeframeMonths) 25 25 26 26 pulls, err := GetPullsByOwnerDid(e, forDid, timeframe) ··· 30 30 31 31 // group pulls by month 32 32 for _, pull := range pulls { 33 - pullMonth := pull.Created.Month() 33 + monthsAgo := monthsBetween(pull.Created, now) 34 34 35 - if currentMonth-pullMonth >= TimeframeMonths { 35 + if monthsAgo >= TimeframeMonths { 36 36 // shouldn't happen; but times are weird 37 37 continue 38 38 } 39 39 40 - idx := currentMonth - pullMonth 40 + idx := monthsAgo 41 41 items := &timeline.ByMonth[idx].PullEvents.Items 42 42 43 43 *items = append(*items, &pull) ··· 53 53 } 54 54 55 55 for _, issue := range issues { 56 - issueMonth := issue.Created.Month() 56 + monthsAgo := monthsBetween(issue.Created, now) 57 57 58 - if currentMonth-issueMonth >= TimeframeMonths { 58 + if monthsAgo >= TimeframeMonths { 59 59 // shouldn't happen; but times are weird 60 60 continue 61 61 } 62 62 63 - idx := currentMonth - issueMonth 63 + idx := monthsAgo 64 64 items := &timeline.ByMonth[idx].IssueEvents.Items 65 65 66 66 *items = append(*items, &issue) ··· 77 77 if repo.Source != "" { 78 78 sourceRepo, err = GetRepoByAtUri(e, repo.Source) 79 79 if err != nil { 80 - return nil, err 80 + // the source repo was not found, skip this bit 81 + log.Println("profile", "err", err) 81 82 } 82 83 } 83 84 84 - repoMonth := repo.Created.Month() 85 + monthsAgo := monthsBetween(repo.Created, now) 85 86 86 - if currentMonth-repoMonth >= TimeframeMonths { 87 + if monthsAgo >= TimeframeMonths { 87 88 // shouldn't happen; but times are weird 88 89 continue 89 90 } 90 91 91 - idx := currentMonth - repoMonth 92 + idx := monthsAgo 92 93 93 94 items := &timeline.ByMonth[idx].RepoEvents 94 95 *items = append(*items, models.RepoEvent{ ··· 98 99 } 99 100 100 101 return &timeline, nil 102 + } 103 + 104 + func monthsBetween(from, to time.Time) int { 105 + years := to.Year() - from.Year() 106 + months := int(to.Month() - from.Month()) 107 + return years*12 + months 101 108 } 102 109 103 110 func UpsertProfile(tx *sql.Tx, profile *models.Profile) error {
+1 -1
appview/db/punchcard.go
··· 78 78 punch.Count = int(count.Int64) 79 79 } 80 80 81 - punchcard.Punches[punch.Date.YearDay()] = punch 81 + punchcard.Punches[punch.Date.YearDay()-1] = punch 82 82 punchcard.Total += punch.Count 83 83 } 84 84
-5
appview/knots/knots.go
··· 666 666 k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.") 667 667 return 668 668 } 669 - if memberId.Handle.IsInvalidHandle() { 670 - l.Error("failed to resolve member identity to handle") 671 - k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.") 672 - return 673 - } 674 669 675 670 // remove from enforcer 676 671 err = k.Enforcer.RemoveKnotMember(domain, memberId.DID.String())
+4
appview/middleware/middleware.go
··· 223 223 ) 224 224 if err != nil { 225 225 log.Println("failed to resolve repo", "err", err) 226 + w.WriteHeader(http.StatusNotFound) 226 227 mw.pages.ErrorKnot404(w) 227 228 return 228 229 } ··· 240 241 f, err := mw.repoResolver.Resolve(r) 241 242 if err != nil { 242 243 log.Println("failed to fully resolve repo", err) 244 + w.WriteHeader(http.StatusNotFound) 243 245 mw.pages.ErrorKnot404(w) 244 246 return 245 247 } ··· 288 290 f, err := mw.repoResolver.Resolve(r) 289 291 if err != nil { 290 292 log.Println("failed to fully resolve repo", err) 293 + w.WriteHeader(http.StatusNotFound) 291 294 mw.pages.ErrorKnot404(w) 292 295 return 293 296 } ··· 324 327 f, err := mw.repoResolver.Resolve(r) 325 328 if err != nil { 326 329 log.Println("failed to fully resolve repo", err) 330 + w.WriteHeader(http.StatusNotFound) 327 331 mw.pages.ErrorKnot404(w) 328 332 return 329 333 }
+1 -1
appview/models/pull.go
··· 83 83 Repo *Repo 84 84 } 85 85 86 + // NOTE: This method does not include patch blob in returned atproto record 86 87 func (p Pull) AsRecord() tangled.RepoPull { 87 88 var source *tangled.RepoPull_Source 88 89 if p.PullSource != nil { ··· 113 114 Repo: p.RepoAt.String(), 114 115 Branch: p.TargetBranch, 115 116 }, 116 - Patch: p.LatestPatch(), 117 117 Source: source, 118 118 } 119 119 return record
+1 -1
appview/pages/templates/banner.html
··· 30 30 <div class="mx-6"> 31 31 These services may not be fully accessible until upgraded. 32 32 <a class="underline text-red-800 dark:text-red-200" 33 - href="https://tangled.org/@tangled.org/core/tree/master/docs/migrations.md"> 33 + href="https://docs.tangled.org/migrating-knots-spindles.html#migrating-knots-spindles"> 34 34 Click to read the upgrade guide</a>. 35 35 </div> 36 36 </details>
+1 -1
appview/pages/templates/knots/index.html
··· 105 105 {{ define "docsButton" }} 106 106 <a 107 107 class="btn flex items-center gap-2" 108 - href="https://tangled.org/@tangled.org/core/blob/master/docs/knot-hosting.md"> 108 + href="https://docs.tangled.org/knot-self-hosting-guide.html#knot-self-hosting-guide"> 109 109 {{ i "book" "size-4" }} 110 110 docs 111 111 </a>
+2 -2
appview/pages/templates/layouts/fragments/footer.html
··· 26 26 <div class="flex flex-col gap-1"> 27 27 <div class="{{ $headerStyle }}">resources</div> 28 28 <a href="https://blog.tangled.org" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ i "book-open" $iconStyle }} blog</a> 29 - <a href="https://tangled.org/@tangled.org/core/tree/master/docs" class="{{ $linkStyle }}">{{ i "book" $iconStyle }} docs</a> 29 + <a href="https://docs.tangled.org" class="{{ $linkStyle }}">{{ i "book" $iconStyle }} docs</a> 30 30 <a href="https://tangled.org/@tangled.org/core" class="{{ $linkStyle }}">{{ i "code" $iconStyle }} source</a> 31 31 <a href="https://tangled.org/brand" class="{{ $linkStyle }}">{{ i "paintbrush" $iconStyle }} brand</a> 32 32 </div> ··· 73 73 <div class="flex flex-col gap-1"> 74 74 <div class="{{ $headerStyle }}">resources</div> 75 75 <a href="https://blog.tangled.org" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ i "book-open" $iconStyle }} blog</a> 76 - <a href="https://tangled.org/@tangled.org/core/tree/master/docs" class="{{ $linkStyle }}">{{ i "book" $iconStyle }} docs</a> 76 + <a href="https://docs.tangled.org" class="{{ $linkStyle }}">{{ i "book" $iconStyle }} docs</a> 77 77 <a href="https://tangled.org/@tangled.org/core" class="{{ $linkStyle }}">{{ i "code" $iconStyle }} source</a> 78 78 <a href="https://tangled.org/brand" class="{{ $linkStyle }}">{{ i "paintbrush" $iconStyle }} brand</a> 79 79 </div>
+1 -1
appview/pages/templates/repo/fragments/diff.html
··· 17 17 {{ else }} 18 18 {{ range $idx, $hunk := $diff }} 19 19 {{ with $hunk }} 20 - <details open id="file-{{ .Name.New }}" 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 }}"> 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 21 <summary class="list-none cursor-pointer sticky top-0"> 22 22 <div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between"> 23 23 <div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto">
+35 -35
appview/pages/templates/repo/fragments/splitDiff.html
··· 3 3 {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800" -}} 4 4 {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 5 5 {{- $lineNrSepStyle := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 6 - {{- $containerStyle := "flex min-w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}} 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 7 {{- $emptyStyle := "bg-gray-200/30 dark:bg-gray-700/30" -}} 8 8 {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400" -}} 9 9 {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}} 10 10 {{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}} 11 11 {{- $opStyle := "w-5 flex-shrink-0 select-none text-center" -}} 12 12 <div class="grid grid-cols-2 divide-x divide-gray-200 dark:divide-gray-700"> 13 - <pre class="overflow-x-auto col-span-1"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</div> 13 + <div class="overflow-x-auto col-span-1 font-mono leading-normal"><div class="overflow-x-auto"><div class="inline-flex flex-col min-w-full">{{- range .TextFragments -}}<span class="block bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</span> 14 14 {{- range .LeftLines -}} 15 15 {{- if .IsEmpty -}} 16 - <div class="{{ $emptyStyle }} {{ $containerStyle }}"> 17 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></div> 18 - <div class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></div> 19 - <div class="px-2 invisible" aria-hidden="true">{{ .Content }}</div> 20 - </div> 16 + <span class="{{ $emptyStyle }} {{ $containerStyle }}"> 17 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></span> 18 + <span class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></span> 19 + <span class="px-2 invisible" aria-hidden="true">{{ .Content }}</span> 20 + </span> 21 21 {{- else if eq .Op.String "-" -}} 22 - <div class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}"> 23 - <div class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></div> 24 - <div class="{{ $opStyle }}">{{ .Op.String }}</div> 25 - <div class="px-2">{{ .Content }}</div> 26 - </div> 22 + <span class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}"> 23 + <span class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></span> 24 + <span class="{{ $opStyle }}">{{ .Op.String }}</span> 25 + <span class="px-2 whitespace-pre">{{ .Content }}</span> 26 + </span> 27 27 {{- else if eq .Op.String " " -}} 28 - <div class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}"> 29 - <div class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></div> 30 - <div class="{{ $opStyle }}">{{ .Op.String }}</div> 31 - <div class="px-2">{{ .Content }}</div> 32 - </div> 28 + <span class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}"> 29 + <span class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></span> 30 + <span class="{{ $opStyle }}">{{ .Op.String }}</span> 31 + <span class="px-2 whitespace-pre">{{ .Content }}</span> 32 + </span> 33 33 {{- end -}} 34 34 {{- end -}} 35 - {{- end -}}</div></div></pre> 35 + {{- end -}}</div></div></div> 36 36 37 - <pre class="overflow-x-auto col-span-1"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</div> 37 + <div class="overflow-x-auto col-span-1 font-mono leading-normal"><div class="overflow-x-auto"><div class="inline-flex flex-col min-w-full">{{- range .TextFragments -}}<span class="block bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</span> 38 38 {{- range .RightLines -}} 39 39 {{- if .IsEmpty -}} 40 - <div class="{{ $emptyStyle }} {{ $containerStyle }}"> 41 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></div> 42 - <div class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></div> 43 - <div class="px-2 invisible" aria-hidden="true">{{ .Content }}</div> 44 - </div> 40 + <span class="{{ $emptyStyle }} {{ $containerStyle }}"> 41 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></span> 42 + <span class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></span> 43 + <span class="px-2 invisible" aria-hidden="true">{{ .Content }}</span> 44 + </span> 45 45 {{- else if eq .Op.String "+" -}} 46 - <div class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}"> 47 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a></div> 48 - <div class="{{ $opStyle }}">{{ .Op.String }}</div> 49 - <div class="px-2" >{{ .Content }}</div> 50 - </div> 46 + <span class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}"> 47 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a></span> 48 + <span class="{{ $opStyle }}">{{ .Op.String }}</span> 49 + <span class="px-2 whitespace-pre">{{ .Content }}</span> 50 + </span> 51 51 {{- else if eq .Op.String " " -}} 52 - <div class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}"> 53 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a></div> 54 - <div class="{{ $opStyle }}">{{ .Op.String }}</div> 55 - <div class="px-2">{{ .Content }}</div> 56 - </div> 52 + <span class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}"> 53 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a> </span> 54 + <span class="{{ $opStyle }}">{{ .Op.String }}</span> 55 + <span class="px-2 whitespace-pre">{{ .Content }}</span> 56 + </span> 57 57 {{- end -}} 58 58 {{- end -}} 59 - {{- end -}}</div></div></pre> 59 + {{- end -}}</div></div></div> 60 60 </div> 61 61 {{ end }}
+21 -22
appview/pages/templates/repo/fragments/unifiedDiff.html
··· 1 1 {{ define "repo/fragments/unifiedDiff" }} 2 2 {{ $name := .Id }} 3 - <pre class="overflow-x-auto"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</div> 3 + <div class="overflow-x-auto font-mono leading-normal"><div class="overflow-x-auto"><div class="inline-flex flex-col min-w-full">{{- range .TextFragments -}}<span class="block bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</span> 4 4 {{- $oldStart := .OldPosition -}} 5 5 {{- $newStart := .NewPosition -}} 6 6 {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800 target:bg-yellow-200 target:dark:bg-yellow-600" -}} 7 7 {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 8 8 {{- $lineNrSepStyle1 := "" -}} 9 9 {{- $lineNrSepStyle2 := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 10 - {{- $containerStyle := "flex min-w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}} 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 11 {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 " -}} 12 12 {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}} 13 13 {{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}} 14 14 {{- $opStyle := "w-5 flex-shrink-0 select-none text-center" -}} 15 15 {{- range .Lines -}} 16 16 {{- if eq .Op.String "+" -}} 17 - <div class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{$newStart}}"> 18 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><span aria-hidden="true" class="invisible">{{$newStart}}</span></div> 19 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{$newStart}}">{{ $newStart }}</a></div> 20 - <div class="{{ $opStyle }}">{{ .Op.String }}</div> 21 - <div class="px-2">{{ .Line }}</div> 22 - </div> 17 + <span class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{$newStart}}"> 18 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><span aria-hidden="true" class="invisible">{{$newStart}}</span></span> 19 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{$newStart}}">{{ $newStart }}</a></span> 20 + <span class="{{ $opStyle }}">{{ .Op.String }}</span> 21 + <span class="px-2 whitespace-pre">{{ .Line }}</span> 22 + </span> 23 23 {{- $newStart = add64 $newStart 1 -}} 24 24 {{- end -}} 25 25 {{- if eq .Op.String "-" -}} 26 - <div class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}"> 27 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}">{{ $oldStart }}</a></div> 28 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><span aria-hidden="true" class="invisible">{{$oldStart}}</span></div> 29 - <div class="{{ $opStyle }}">{{ .Op.String }}</div> 30 - <div class="px-2">{{ .Line }}</div> 31 - </div> 26 + <span class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}"> 27 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}">{{ $oldStart }}</a></span> 28 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><span aria-hidden="true" class="invisible">{{$oldStart}}</span></span> 29 + <span class="{{ $opStyle }}">{{ .Op.String }}</span> 30 + <span class="px-2 whitespace-pre">{{ .Line }}</span> 31 + </span> 32 32 {{- $oldStart = add64 $oldStart 1 -}} 33 33 {{- end -}} 34 34 {{- if eq .Op.String " " -}} 35 - <div class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}-N{{$newStart}}"> 36 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $oldStart }}</a></div> 37 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $newStart }}</a></div> 38 - <div class="{{ $opStyle }}">{{ .Op.String }}</div> 39 - <div class="px-2">{{ .Line }}</div> 40 - </div> 35 + <span class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}-N{{$newStart}}"> 36 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $oldStart }}</a></span> 37 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $newStart }}</a></span> 38 + <span class="{{ $opStyle }}">{{ .Op.String }}</span> 39 + <span class="px-2 whitespace-pre">{{ .Line }}</span> 40 + </span> 41 41 {{- $newStart = add64 $newStart 1 -}} 42 42 {{- $oldStart = add64 $oldStart 1 -}} 43 43 {{- end -}} 44 44 {{- end -}} 45 - {{- end -}}</div></div></pre> 45 + {{- end -}}</div></div></div> 46 46 {{ end }} 47 -
+1 -1
appview/pages/templates/repo/pipelines/pipelines.html
··· 23 23 </p> 24 24 <p> 25 25 <span class="{{ $bullet }}">2</span>Configure your CI/CD 26 - <a href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/pipeline.md" class="underline">pipeline</a>. 26 + <a href="https://docs.tangled.org/spindles.html#pipelines" class="underline">pipeline</a>. 27 27 </p> 28 28 <p><span class="{{ $bullet }}">3</span>Trigger a workflow with a push or a pull-request!</p> 29 29 </div>
+1 -1
appview/pages/templates/repo/settings/pipelines.html
··· 22 22 <p class="text-gray-500 dark:text-gray-400"> 23 23 Choose a spindle to execute your workflows on. Only repository owners 24 24 can configure spindles. Spindles can be selfhosted, 25 - <a class="text-gray-500 dark:text-gray-400 underline" href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md"> 25 + <a class="text-gray-500 dark:text-gray-400 underline" href="https://docs.tangled.org/spindles.html#self-hosting-guide"> 26 26 click to learn more. 27 27 </a> 28 28 </p>
+1 -1
appview/pages/templates/spindles/index.html
··· 102 102 {{ define "docsButton" }} 103 103 <a 104 104 class="btn flex items-center gap-2" 105 - href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md"> 105 + href="https://docs.tangled.org/spindles.html#self-hosting-guide"> 106 106 {{ i "book" "size-4" }} 107 107 docs 108 108 </a>
+48 -36
appview/pulls/pulls.go
··· 1241 1241 return 1242 1242 } 1243 1243 1244 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 1245 + if err != nil { 1246 + log.Println("failed to upload patch", err) 1247 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1248 + return 1249 + } 1250 + 1244 1251 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1245 1252 Collection: tangled.RepoPullNSID, 1246 1253 Repo: user.Did, ··· 1252 1259 Repo: string(repo.RepoAt()), 1253 1260 Branch: targetBranch, 1254 1261 }, 1255 - Patch: patch, 1262 + PatchBlob: blob.Blob, 1256 1263 Source: recordPullSource, 1257 1264 CreatedAt: time.Now().Format(time.RFC3339), 1258 1265 }, ··· 1328 1335 // apply all record creations at once 1329 1336 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1330 1337 for _, p := range stack { 1338 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(p.LatestPatch())) 1339 + if err != nil { 1340 + log.Println("failed to upload patch blob", err) 1341 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1342 + return 1343 + } 1344 + 1331 1345 record := p.AsRecord() 1332 - write := comatproto.RepoApplyWrites_Input_Writes_Elem{ 1346 + record.PatchBlob = blob.Blob 1347 + writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1333 1348 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 1334 1349 Collection: tangled.RepoPullNSID, 1335 1350 Rkey: &p.Rkey, ··· 1337 1352 Val: &record, 1338 1353 }, 1339 1354 }, 1340 - } 1341 - writes = append(writes, &write) 1355 + }) 1342 1356 } 1343 1357 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 1344 1358 Repo: user.Did, ··· 1871 1885 return 1872 1886 } 1873 1887 1874 - var recordPullSource *tangled.RepoPull_Source 1875 - if pull.IsBranchBased() { 1876 - recordPullSource = &tangled.RepoPull_Source{ 1877 - Branch: pull.PullSource.Branch, 1878 - Sha: sourceRev, 1879 - } 1888 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 1889 + if err != nil { 1890 + log.Println("failed to upload patch blob", err) 1891 + s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1892 + return 1880 1893 } 1881 - if pull.IsForkBased() { 1882 - repoAt := pull.PullSource.RepoAt.String() 1883 - recordPullSource = &tangled.RepoPull_Source{ 1884 - Branch: pull.PullSource.Branch, 1885 - Repo: &repoAt, 1886 - Sha: sourceRev, 1887 - } 1888 - } 1894 + record := pull.AsRecord() 1895 + record.PatchBlob = blob.Blob 1896 + record.CreatedAt = time.Now().Format(time.RFC3339) 1889 1897 1890 1898 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1891 1899 Collection: tangled.RepoPullNSID, ··· 1893 1901 Rkey: pull.Rkey, 1894 1902 SwapRecord: ex.Cid, 1895 1903 Record: &lexutil.LexiconTypeDecoder{ 1896 - Val: &tangled.RepoPull{ 1897 - Title: pull.Title, 1898 - Target: &tangled.RepoPull_Target{ 1899 - Repo: string(repo.RepoAt()), 1900 - Branch: pull.TargetBranch, 1901 - }, 1902 - Patch: patch, // new patch 1903 - Source: recordPullSource, 1904 - CreatedAt: time.Now().Format(time.RFC3339), 1905 - }, 1904 + Val: &record, 1906 1905 }, 1907 1906 }) 1908 1907 if err != nil { ··· 1988 1987 } 1989 1988 defer tx.Rollback() 1990 1989 1990 + client, err := s.oauth.AuthorizedClient(r) 1991 + if err != nil { 1992 + log.Println("failed to authorize client") 1993 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1994 + return 1995 + } 1996 + 1991 1997 // pds updates to make 1992 1998 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1993 1999 ··· 2021 2027 return 2022 2028 } 2023 2029 2030 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 2031 + if err != nil { 2032 + log.Println("failed to upload patch blob", err) 2033 + s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 2034 + return 2035 + } 2024 2036 record := p.AsRecord() 2037 + record.PatchBlob = blob.Blob 2025 2038 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 2026 2039 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 2027 2040 Collection: tangled.RepoPullNSID, ··· 2056 2069 return 2057 2070 } 2058 2071 2072 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 2073 + if err != nil { 2074 + log.Println("failed to upload patch blob", err) 2075 + s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 2076 + return 2077 + } 2059 2078 record := np.AsRecord() 2060 - 2079 + record.PatchBlob = blob.Blob 2061 2080 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 2062 2081 RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{ 2063 2082 Collection: tangled.RepoPullNSID, ··· 2091 2110 if err != nil { 2092 2111 log.Println("failed to resubmit pull", err) 2093 2112 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 2094 - return 2095 - } 2096 - 2097 - client, err := s.oauth.AuthorizedClient(r) 2098 - if err != nil { 2099 - log.Println("failed to authorize client") 2100 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 2101 2113 return 2102 2114 } 2103 2115
+1
appview/repo/archive.go
··· 18 18 l := rp.logger.With("handler", "DownloadArchive") 19 19 ref := chi.URLParam(r, "ref") 20 20 ref, _ = url.PathUnescape(ref) 21 + ref = strings.TrimSuffix(ref, ".tar.gz") 21 22 f, err := rp.repoResolver.Resolve(r) 22 23 if err != nil { 23 24 l.Error("failed to get repo and knot", "err", err)
-5
appview/spindles/spindles.go
··· 653 653 s.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.") 654 654 return 655 655 } 656 - if memberId.Handle.IsInvalidHandle() { 657 - l.Error("failed to resolve member identity to handle") 658 - s.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.") 659 - return 660 - } 661 656 662 657 tx, err := s.Db.Begin() 663 658 if err != nil {
+6 -4
appview/state/profile.go
··· 163 163 } 164 164 165 165 // populate commit counts in the timeline, using the punchcard 166 - currentMonth := time.Now().Month() 166 + now := time.Now() 167 167 for _, p := range profile.Punchcard.Punches { 168 - idx := currentMonth - p.Date.Month() 169 - if int(idx) < len(timeline.ByMonth) { 170 - timeline.ByMonth[idx].Commits += p.Count 168 + years := now.Year() - p.Date.Year() 169 + months := int(now.Month() - p.Date.Month()) 170 + monthsAgo := years*12 + months 171 + if monthsAgo >= 0 && monthsAgo < len(timeline.ByMonth) { 172 + timeline.ByMonth[monthsAgo].Commits += p.Count 171 173 } 172 174 } 173 175
+2
appview/state/router.go
··· 109 109 }) 110 110 111 111 r.NotFound(func(w http.ResponseWriter, r *http.Request) { 112 + w.WriteHeader(http.StatusNotFound) 112 113 s.pages.Error404(w) 113 114 }) 114 115 ··· 182 183 r.Get("/brand", s.Brand) 183 184 184 185 r.NotFound(func(w http.ResponseWriter, r *http.Request) { 186 + w.WriteHeader(http.StatusNotFound) 185 187 s.pages.Error404(w) 186 188 }) 187 189 return r
+66 -67
docs/DOCS.md
··· 1 1 --- 2 - title: Tangled Documentation 2 + title: Tangled docs 3 3 author: The Tangled Contributors 4 4 date: 21 Sun, Dec 2025 5 5 --- ··· 8 8 9 9 Tangled is a decentralized code hosting and collaboration 10 10 platform. Every component of Tangled is open-source and 11 - selfhostable. [tangled.org](https://tangled.org) also 11 + self-hostable. [tangled.org](https://tangled.org) also 12 12 provides hosting and CI services that are free to use. 13 13 14 14 There are several models for decentralized code 15 15 collaboration platforms, ranging from ActivityPub’s 16 16 (Forgejo) federated model, to Radicle’s entirely P2P model. 17 17 Our approach attempts to be the best of both worlds by 18 - adopting atproto—a protocol for building decentralized 18 + adopting the AT Protocol—a protocol for building decentralized 19 19 social applications with a central identity 20 20 21 21 Our approach to this is the idea of “knots”. Knots are ··· 26 26 default, Tangled provides managed knots where you can host 27 27 your repositories for free. 28 28 29 - The "appview" at tangled.org acts as a consolidated “view” 29 + The appview at tangled.org acts as a consolidated "view" 30 30 into the whole network, allowing users to access, clone and 31 31 contribute to repositories hosted across different knots 32 32 seamlessly. 33 33 34 - # Quick Start Guide 34 + # Quick start guide 35 35 36 - ## Login or Sign up 36 + ## Login or sign up 37 37 38 - You can [login](https://tangled.org) by using your AT 38 + You can [login](https://tangled.org) by using your AT Protocol 39 39 account. If you are unclear on what that means, simply head 40 40 to the [signup](https://tangled.org/signup) page and create 41 41 an account. By doing so, you will be choosing Tangled as 42 42 your account provider (you will be granted a handle of the 43 43 form `user.tngl.sh`). 44 44 45 - In the AT network, users are free to choose their account 45 + In the AT Protocol network, users are free to choose their account 46 46 provider (known as a "Personal Data Service", or PDS), and 47 47 login to applications that support AT accounts. 48 48 49 - You can think of it as "one account for all of the 50 - atmosphere"! 49 + You can think of it as "one account for all of the atmosphere"! 51 50 52 51 If you already have an AT account (you may have one if you 53 52 signed up to Bluesky, for example), you can login with the 54 53 same handle on Tangled (so just use `user.bsky.social` on 55 54 the login page). 56 55 57 - ## Add an SSH Key 56 + ## Add an SSH key 58 57 59 58 Once you are logged in, you can start creating repositories 60 59 and pushing code. Tangled supports pushing git repositories ··· 87 86 paste your public key, give it a descriptive name, and hit 88 87 save. 89 88 90 - ## Create a Repository 89 + ## Create a repository 91 90 92 91 Once your SSH key is added, create your first repository: 93 92 ··· 98 97 4. Choose a knotserver to host this repository on 99 98 5. Hit create 100 99 101 - "Knots" are selfhostable, lightweight git servers that can 100 + Knots are self-hostable, lightweight Git servers that can 102 101 host your repository. Unlike traditional code forges, your 103 102 code can live on any server. Read the [Knots](TODO) section 104 103 for more. ··· 125 124 are hosted by tangled.org. If you use a custom knot, refer 126 125 to the [Knots](TODO) section. 127 126 128 - ## Push Your First Repository 127 + ## Push your first repository 129 128 130 - Initialize a new git repository: 129 + Initialize a new Git repository: 131 130 132 131 ```bash 133 132 mkdir my-project ··· 165 164 cd /path/to/your/existing/repo 166 165 ``` 167 166 168 - You can inspect your existing git remote like so: 167 + You can inspect your existing Git remote like so: 169 168 170 169 ```bash 171 170 git remote -v ··· 197 196 origin git@tangled.org:user.tngl.sh/my-project (push) 198 197 ``` 199 198 200 - Push all your branches and tags to tangled: 199 + Push all your branches and tags to Tangled: 201 200 202 201 ```bash 203 202 git push -u origin --all ··· 232 231 ``` 233 232 234 233 You also need to re-add the original URL as a push 235 - destination (git replaces the push URL when you use `--add` 234 + destination (Git replaces the push URL when you use `--add` 236 235 the first time): 237 236 238 237 ```bash ··· 249 248 ``` 250 249 251 250 Notice that there's one fetch URL (the primary remote) and 252 - two push URLs. Now, whenever you push, git will 251 + two push URLs. Now, whenever you push, Git will 253 252 automatically push to both remotes: 254 253 255 254 ```bash ··· 301 300 ## Docker 302 301 303 302 Refer to 304 - [@tangled.org/knot-docker](https://tangled.sh/@tangled.sh/knot-docker). 303 + [@tangled.org/knot-docker](https://tangled.org/@tangled.org/knot-docker). 305 304 Note that this is community maintained. 306 305 307 306 ## Manual setup ··· 372 371 ``` 373 372 KNOT_REPO_SCAN_PATH=/home/git 374 373 KNOT_SERVER_HOSTNAME=knot.example.com 375 - APPVIEW_ENDPOINT=https://tangled.sh 374 + APPVIEW_ENDPOINT=https://tangled.org 376 375 KNOT_SERVER_OWNER=did:plc:foobar 377 376 KNOT_SERVER_INTERNAL_LISTEN_ADDR=127.0.0.1:5444 378 377 KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555 ··· 603 602 - `nixery`: This uses an instance of 604 603 [Nixery](https://nixery.dev) to run steps, which allows 605 604 you to add [dependencies](#dependencies) from 606 - [Nixpkgs](https://github.com/NixOS/nixpkgs). You can 605 + Nixpkgs (https://github.com/NixOS/nixpkgs). You can 607 606 search for packages on https://search.nixos.org, and 608 607 there's a pretty good chance the package(s) you're looking 609 608 for will be there. ··· 630 629 default, the depth is set to 1, meaning only the most 631 630 recent commit will be fetched, which is the commit that 632 631 triggered the workflow. 633 - - `submodules`: If you use [git 634 - submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules) 632 + - `submodules`: If you use Git submodules 633 + (https://git-scm.com/book/en/v2/Git-Tools-Submodules) 635 634 in your repository, setting this field to `true` will 636 635 recursively fetch all submodules. This is `false` by 637 636 default. ··· 657 656 Say you want to fetch Node.js and Go from `nixpkgs`, and a 658 657 package called `my_pkg` you've made from your own registry 659 658 at your repository at 660 - `https://tangled.sh/@example.com/my_pkg`. You can define 659 + `https://tangled.org/@example.com/my_pkg`. You can define 661 660 those dependencies like so: 662 661 663 662 ```yaml ··· 779 778 780 779 If you want another example of a workflow, you can look at 781 780 the one [Tangled uses to build the 782 - project](https://tangled.sh/@tangled.sh/core/blob/master/.tangled/workflows/build.yml). 781 + project](https://tangled.org/@tangled.org/core/blob/master/.tangled/workflows/build.yml). 783 782 784 783 ## Self-hosting guide 785 784 ··· 836 835 837 836 ## Architecture 838 837 839 - Spindle is a small CI runner service. Here's a high level overview of how it operates: 838 + Spindle is a small CI runner service. Here's a high-level overview of how it operates: 840 839 841 - * listens for [`sh.tangled.spindle.member`](/lexicons/spindle/member.json) and 840 + * Listens for [`sh.tangled.spindle.member`](/lexicons/spindle/member.json) and 842 841 [`sh.tangled.repo`](/lexicons/repo.json) records on the Jetstream. 843 - * when a new repo record comes through (typically when you add a spindle to a 842 + * When a new repo record comes through (typically when you add a spindle to a 844 843 repo from the settings), spindle then resolves the underlying knot and 845 844 subscribes to repo events (see: 846 845 [`sh.tangled.pipeline`](/lexicons/pipeline.json)). 847 - * the spindle engine then handles execution of the pipeline, with results and 848 - logs beamed on the spindle event stream over wss 846 + * The spindle engine then handles execution of the pipeline, with results and 847 + logs beamed on the spindle event stream over WebSocket 849 848 850 849 ### The engine 851 850 852 851 At present, the only supported backend is Docker (and Podman, if Docker 853 - compatibility is enabled, so that `/run/docker.sock` is created). Spindle 852 + compatibility is enabled, so that `/run/docker.sock` is created). spindle 854 853 executes each step in the pipeline in a fresh container, with state persisted 855 854 across steps within the `/tangled/workspace` directory. 856 855 ··· 858 857 [Nixery](https://nixery.dev), which is handy for caching layers for frequently 859 858 used packages. 860 859 861 - The pipeline manifest is [specified here](/docs/spindle/pipeline.md). 860 + The pipeline manifest is [specified here](https://docs.tangled.org/spindles.html#pipelines). 862 861 863 862 ## Secrets with openbao 864 863 865 - This document covers setting up Spindle to use OpenBao for secrets 864 + This document covers setting up spindle to use OpenBao for secrets 866 865 management via OpenBao Proxy instead of the default SQLite backend. 867 866 868 867 ### Overview 869 868 870 869 Spindle now uses OpenBao Proxy for secrets management. The proxy handles 871 - authentication automatically using AppRole credentials, while Spindle 870 + authentication automatically using AppRole credentials, while spindle 872 871 connects to the local proxy instead of directly to the OpenBao server. 873 872 874 873 This approach provides better security, automatic token renewal, and ··· 876 875 877 876 ### Installation 878 877 879 - Install OpenBao from nixpkgs: 878 + Install OpenBao from Nixpkgs: 880 879 881 880 ```bash 882 881 nix shell nixpkgs#openbao # for a local server ··· 1029 1028 } 1030 1029 } 1031 1030 1032 - # Proxy listener for Spindle 1031 + # Proxy listener for spindle 1033 1032 listener "tcp" { 1034 1033 address = "127.0.0.1:8201" 1035 1034 tls_disable = true ··· 1062 1061 1063 1062 #### Configure spindle 1064 1063 1065 - Set these environment variables for Spindle: 1064 + Set these environment variables for spindle: 1066 1065 1067 1066 ```bash 1068 1067 export SPINDLE_SERVER_SECRETS_PROVIDER=openbao ··· 1070 1069 export SPINDLE_SERVER_SECRETS_OPENBAO_MOUNT=spindle 1071 1070 ``` 1072 1071 1073 - On startup, the spindle will now connect to the local proxy, 1072 + On startup, spindle will now connect to the local proxy, 1074 1073 which handles all authentication automatically. 1075 1074 1076 1075 ### Production setup for proxy ··· 1099 1098 # List all secrets 1100 1099 bao kv list spindle/ 1101 1100 1102 - # Add a test secret via Spindle API, then check it exists 1101 + # Add a test secret via the spindle API, then check it exists 1103 1102 bao kv list spindle/repos/ 1104 1103 1105 1104 # Get a specific secret ··· 1112 1111 port 8200 or 8201) 1113 1112 - The proxy authenticates with OpenBao using AppRole 1114 1113 credentials 1115 - - All Spindle requests go through the proxy, which injects 1114 + - All spindle requests go through the proxy, which injects 1116 1115 authentication tokens 1117 1116 - Secrets are stored at 1118 1117 `spindle/repos/{sanitized_repo_path}/{secret_key}` ··· 1131 1130 and the policy has the necessary permissions. 1132 1131 1133 1132 **404 route errors**: The spindle KV mount probably doesn't 1134 - exist - run the mount creation step again. 1133 + exist—run the mount creation step again. 1135 1134 1136 1135 **Proxy authentication failures**: Check the proxy logs and 1137 1136 verify the role-id and secret-id files are readable and ··· 1159 1158 secret_id="$(cat /tmp/openbao/secret-id)" 1160 1159 ``` 1161 1160 1162 - # Migrating knots & spindles 1161 + # Migrating knots and spindles 1163 1162 1164 1163 Sometimes, non-backwards compatible changes are made to the 1165 1164 knot/spindle XRPC APIs. If you host a knot or a spindle, you ··· 1172 1171 1173 1172 ## Upgrading from v1.8.x 1174 1173 1175 - After v1.8.2, the HTTP API for knot and spindles have been 1174 + After v1.8.2, the HTTP API for knots and spindles has been 1176 1175 deprecated and replaced with XRPC. Repositories on outdated 1177 1176 knots will not be viewable from the appview. Upgrading is 1178 1177 straightforward however. 1179 1178 1180 1179 For knots: 1181 1180 1182 - - Upgrade to latest tag (v1.9.0 or above) 1181 + - Upgrade to the latest tag (v1.9.0 or above) 1183 1182 - Head to the [knot dashboard](https://tangled.org/settings/knots) and 1184 1183 hit the "retry" button to verify your knot 1185 1184 1186 1185 For spindles: 1187 1186 1188 - - Upgrade to latest tag (v1.9.0 or above) 1187 + - Upgrade to the latest tag (v1.9.0 or above) 1189 1188 - Head to the [spindle 1190 1189 dashboard](https://tangled.org/settings/spindles) and hit the 1191 1190 "retry" button to verify your spindle ··· 1227 1226 # Hacking on Tangled 1228 1227 1229 1228 We highly recommend [installing 1230 - nix](https://nixos.org/download/) (the package manager) 1231 - before working on the codebase. The nix flake provides a lot 1229 + Nix](https://nixos.org/download/) (the package manager) 1230 + before working on the codebase. The Nix flake provides a lot 1232 1231 of helpers to get started and most importantly, builds and 1233 1232 dev shells are entirely deterministic. 1234 1233 ··· 1238 1237 nix develop 1239 1238 ``` 1240 1239 1241 - Non-nix users can look at the `devShell` attribute in the 1240 + Non-Nix users can look at the `devShell` attribute in the 1242 1241 `flake.nix` file to determine necessary dependencies. 1243 1242 1244 1243 ## Running the appview 1245 1244 1246 - The nix flake also exposes a few `app` attributes (run `nix 1245 + The Nix flake also exposes a few `app` attributes (run `nix 1247 1246 flake show` to see a full list of what the flake provides), 1248 1247 one of the apps runs the appview with the `air` 1249 1248 live-reloader: ··· 1258 1257 nix run .#watch-tailwind 1259 1258 ``` 1260 1259 1261 - To authenticate with the appview, you will need redis and 1262 - OAUTH JWKs to be setup: 1260 + To authenticate with the appview, you will need Redis and 1261 + OAuth JWKs to be set up: 1263 1262 1264 1263 ``` 1265 - # oauth jwks should already be setup by the nix devshell: 1264 + # OAuth JWKs should already be set up by the Nix devshell: 1266 1265 echo $TANGLED_OAUTH_CLIENT_SECRET 1267 1266 z42ty4RT1ovnTopY8B8ekz9NuziF2CuMkZ7rbRFpAR9jBqMc 1268 1267 ··· 1280 1279 # the secret key from above 1281 1280 export TANGLED_OAUTH_CLIENT_SECRET="z42tuP..." 1282 1281 1283 - # run redis in at a new shell to store oauth sessions 1282 + # Run Redis in a new shell to store OAuth sessions 1284 1283 redis-server 1285 1284 ``` 1286 1285 1287 1286 ## Running knots and spindles 1288 1287 1289 1288 An end-to-end knot setup requires setting up a machine with 1290 - `sshd`, `AuthorizedKeysCommand`, and git user, which is 1291 - quite cumbersome. So the nix flake provides a 1289 + `sshd`, `AuthorizedKeysCommand`, and a Git user, which is 1290 + quite cumbersome. So the Nix flake provides a 1292 1291 `nixosConfiguration` to do so. 1293 1292 1294 1293 <details> 1295 - <summary><strong>MacOS users will have to setup a Nix Builder first</strong></summary> 1294 + <summary><strong>macOS users will have to set up a Nix Builder first</strong></summary> 1296 1295 1297 1296 In order to build Tangled's dev VM on macOS, you will 1298 1297 first need to set up a Linux Nix builder. The recommended ··· 1303 1302 you are using Apple Silicon). 1304 1303 1305 1304 > IMPORTANT: You must build `darwin.linux-builder` somewhere other than inside 1306 - > the tangled repo so that it doesn't conflict with the other VM. For example, 1305 + > the Tangled repo so that it doesn't conflict with the other VM. For example, 1307 1306 > you can do 1308 1307 > 1309 1308 > ```shell ··· 1316 1315 > avoid subtle problems. 1317 1316 1318 1317 Alternatively, you can use any other method to set up a 1319 - Linux machine with `nix` installed that you can `sudo ssh` 1318 + Linux machine with Nix installed that you can `sudo ssh` 1320 1319 into (in other words, root user on your Mac has to be able 1321 1320 to ssh into the Linux machine without entering a password) 1322 1321 and that has the same architecture as your Mac. See ··· 1347 1346 with `ssh` exposed on port 2222. 1348 1347 1349 1348 Once the services are running, head to 1350 - http://localhost:3000/settings/knots and hit verify. It should 1349 + http://localhost:3000/settings/knots and hit "Verify". It should 1351 1350 verify the ownership of the services instantly if everything 1352 1351 went smoothly. 1353 1352 ··· 1371 1370 1372 1371 The above VM should already be running a spindle on 1373 1372 `localhost:6555`. Head to http://localhost:3000/settings/spindles and 1374 - hit verify. You can then configure each repository to use 1373 + hit "Verify". You can then configure each repository to use 1375 1374 this spindle and run CI jobs. 1376 1375 1377 1376 Of interest when debugging spindles: 1378 1377 1379 1378 ``` 1380 - # service logs from journald: 1379 + # Service logs from journald: 1381 1380 journalctl -xeu spindle 1382 1381 1383 1382 # CI job logs from disk: 1384 1383 ls /var/log/spindle 1385 1384 1386 - # debugging spindle db: 1385 + # Debugging spindle database: 1387 1386 sqlite3 /var/lib/spindle/spindle.db 1388 1387 1389 1388 # litecli has a nicer REPL interface: ··· 1432 1431 1433 1432 ### General notes 1434 1433 1435 - - PRs get merged "as-is" (fast-forward) -- like applying a patch-series 1436 - using `git am`. At present, there is no squashing -- so please author 1434 + - PRs get merged "as-is" (fast-forward)—like applying a patch-series 1435 + using `git am`. At present, there is no squashing—so please author 1437 1436 your commits as they would appear on `master`, following the above 1438 1437 guidelines. 1439 1438 - If there is a lot of nesting, for example "appview: ··· 1454 1453 ## Code formatting 1455 1454 1456 1455 We use a variety of tools to format our code, and multiplex them with 1457 - [`treefmt`](https://treefmt.com): all you need to do to format your changes 1456 + [`treefmt`](https://treefmt.com). All you need to do to format your changes 1458 1457 is run `nix run .#fmt` (or just `treefmt` if you're in the devshell). 1459 1458 1460 1459 ## Proposals for bigger changes ··· 1482 1481 We'll use the issue thread to discuss and refine the idea before moving 1483 1482 forward. 1484 1483 1485 - ## Developer certificate of origin (DCO) 1484 + ## Developer Certificate of Origin (DCO) 1486 1485 1487 1486 We require all contributors to certify that they have the right to 1488 1487 submit the code they're contributing. To do this, we follow the
+33 -8
docs/template.html
··· 20 20 <meta name="description" content="$description-meta$" /> 21 21 $endif$ 22 22 23 - <title>$pagetitle$ - Tangled docs</title> 23 + <title>$pagetitle$</title> 24 24 25 25 <style> 26 26 $styles.css()$ ··· 43 43 $endfor$ 44 44 45 45 $if(toc)$ 46 - <!-- mobile topbar toc --> 47 - <details id="mobile-$idprefix$TOC" role="doc-toc" class="md:hidden bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 z-50 space-y-4 group px-6 py-4"> 48 - <summary class="cursor-pointer list-none text-sm font-semibold select-none flex gap-2 justify-between items-center dark:text-white"> 46 + <!-- mobile TOC trigger --> 47 + <div class="md:hidden px-6 py-4 border-b border-gray-200 dark:border-gray-700"> 48 + <button 49 + type="button" 50 + popovertarget="mobile-toc-popover" 51 + popovertargetaction="toggle" 52 + class="w-full flex gap-2 items-center text-sm font-semibold dark:text-white" 53 + > 54 + ${ menu.svg() } 55 + $if(toc-title)$$toc-title$$else$Table of Contents$endif$ 56 + </button> 57 + </div> 58 + 59 + <div 60 + id="mobile-toc-popover" 61 + popover 62 + class="mobile-toc-popover 63 + bg-white dark:bg-gray-800 64 + border-b border-gray-200 dark:border-gray-700 65 + h-full overflow-y-auto 66 + px-6 py-4 fixed inset-x-0 top-0 w-fit max-w-4/5 m-0" 67 + > 68 + <button 69 + type="button" 70 + popovertarget="mobile-toc-popover" 71 + popovertargetaction="toggle" 72 + class="w-full flex gap-2 items-center text-sm font-semibold dark:text-white mb-4"> 73 + ${ x.svg() } 49 74 $if(toc-title)$$toc-title$$else$Table of Contents$endif$ 50 - <span class="group-open:hidden inline">${ menu.svg() }</span> 51 - <span class="hidden group-open:inline">${ x.svg() }</span> 52 - </summary> 75 + </button> 53 76 ${ table-of-contents:toc.html() } 54 - </details> 77 + </div> 78 + 79 + 55 80 <!-- desktop sidebar toc --> 56 81 <nav id="$idprefix$TOC" role="doc-toc" class="hidden md:block fixed left-0 top-0 w-80 h-screen bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 overflow-y-auto p-4 z-50"> 57 82 $if(toc-title)$
+1 -1
flake.nix
··· 76 76 }; 77 77 buildGoApplication = 78 78 (self.callPackage "${gomod2nix}/builder" { 79 - gomod2nix = gomod2nix.legacyPackages.${pkgs.system}.gomod2nix; 79 + gomod2nix = gomod2nix.legacyPackages.${pkgs.stdenv.hostPlatform.system}.gomod2nix; 80 80 }).buildGoApplication; 81 81 modules = ./nix/gomod2nix.toml; 82 82 sqlite-lib = self.callPackage ./nix/pkgs/sqlite-lib.nix {
+1
input.css
··· 255 255 @apply py-1 text-gray-900 dark:text-gray-100; 256 256 } 257 257 } 258 + 258 259 } 259 260 260 261 /* Background */
+103
knotserver/xrpc/list_repos.go
··· 1 + package xrpc 2 + 3 + import ( 4 + "net/http" 5 + "os" 6 + "path/filepath" 7 + "strings" 8 + 9 + securejoin "github.com/cyphar/filepath-securejoin" 10 + "tangled.org/core/api/tangled" 11 + "tangled.org/core/knotserver/git" 12 + xrpcerr "tangled.org/core/xrpc/errors" 13 + ) 14 + 15 + // ListRepos lists all users (DIDs) and their repositories by scanning the repository directory 16 + func (x *Xrpc) ListRepos(w http.ResponseWriter, r *http.Request) { 17 + scanPath := x.Config.Repo.ScanPath 18 + 19 + didEntries, err := os.ReadDir(scanPath) 20 + if err != nil { 21 + x.Logger.Error("failed to read scan path", "error", err, "path", scanPath) 22 + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 23 + return 24 + } 25 + 26 + var users []*tangled.RepoListRepos_User 27 + 28 + for _, didEntry := range didEntries { 29 + if !didEntry.IsDir() { 30 + continue 31 + } 32 + 33 + did := didEntry.Name() 34 + 35 + // Validate DID format (basic check) 36 + if !strings.HasPrefix(did, "did:") { 37 + continue 38 + } 39 + 40 + didPath, err := securejoin.SecureJoin(scanPath, did) 41 + if err != nil { 42 + x.Logger.Warn("failed to join path for did", "did", did, "error", err) 43 + continue 44 + } 45 + 46 + // Read repositories for this DID 47 + repoEntries, err := os.ReadDir(didPath) 48 + if err != nil { 49 + x.Logger.Warn("failed to read did directory", "did", did, "error", err) 50 + continue 51 + } 52 + 53 + var repos []*tangled.RepoListRepos_RepoEntry 54 + 55 + for _, repoEntry := range repoEntries { 56 + if !repoEntry.IsDir() { 57 + continue 58 + } 59 + 60 + repoName := repoEntry.Name() 61 + 62 + // Check if it's a valid git repository 63 + repoPath, err := securejoin.SecureJoin(didPath, repoName) 64 + if err != nil { 65 + continue 66 + } 67 + 68 + repo, err := git.PlainOpen(repoPath) 69 + if err != nil { 70 + // Not a valid git repository, skip 71 + continue 72 + } 73 + 74 + // Get default branch 75 + defaultBranch := "master" 76 + branch, err := repo.FindMainBranch() 77 + if err == nil { 78 + defaultBranch = branch 79 + } 80 + 81 + repos = append(repos, &tangled.RepoListRepos_RepoEntry{ 82 + Name: repoName, 83 + Did: did, 84 + FullPath: filepath.Join(did, repoName), 85 + DefaultBranch: &defaultBranch, 86 + }) 87 + } 88 + 89 + // Only add user if they have repositories 90 + if len(repos) > 0 { 91 + users = append(users, &tangled.RepoListRepos_User{ 92 + Did: did, 93 + Repos: repos, 94 + }) 95 + } 96 + } 97 + 98 + response := tangled.RepoListRepos_Output{ 99 + Users: users, 100 + } 101 + 102 + writeJson(w, response) 103 + }
+1
knotserver/xrpc/xrpc.go
··· 73 73 74 74 // service query endpoints (no auth required) 75 75 r.Get("/"+tangled.OwnerNSID, x.Owner) 76 + r.Get("/"+tangled.RepoListReposNSID, x.ListRepos) 76 77 77 78 return r 78 79 }
+8 -2
lexicons/pulls/pull.json
··· 12 12 "required": [ 13 13 "target", 14 14 "title", 15 - "patch", 15 + "patchBlob", 16 16 "createdAt" 17 17 ], 18 18 "properties": { ··· 27 27 "type": "string" 28 28 }, 29 29 "patch": { 30 - "type": "string" 30 + "type": "string", 31 + "description": "(deprecated) use patchBlob instead" 32 + }, 33 + "patchBlob": { 34 + "type": "blob", 35 + "accept": "text/x-patch", 36 + "description": "patch content" 31 37 }, 32 38 "source": { 33 39 "type": "ref",
+71
lexicons/repo/listRepos.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.repo.listRepos", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Lists all users (DIDs) and their repositories", 8 + "parameters": { 9 + "type": "params", 10 + "properties": {} 11 + }, 12 + "output": { 13 + "encoding": "application/json", 14 + "schema": { 15 + "type": "object", 16 + "required": ["users"], 17 + "properties": { 18 + "users": { 19 + "type": "array", 20 + "items": { 21 + "type": "ref", 22 + "ref": "#user" 23 + } 24 + } 25 + } 26 + } 27 + } 28 + }, 29 + "user": { 30 + "type": "object", 31 + "required": ["did", "repos"], 32 + "properties": { 33 + "did": { 34 + "type": "string", 35 + "format": "did", 36 + "description": "DID of the user" 37 + }, 38 + "repos": { 39 + "type": "array", 40 + "items": { 41 + "type": "ref", 42 + "ref": "#repoEntry" 43 + } 44 + } 45 + } 46 + }, 47 + "repoEntry": { 48 + "type": "object", 49 + "required": ["name", "did", "fullPath"], 50 + "properties": { 51 + "name": { 52 + "type": "string", 53 + "description": "Repository name" 54 + }, 55 + "did": { 56 + "type": "string", 57 + "format": "did", 58 + "description": "DID of the repository owner" 59 + }, 60 + "fullPath": { 61 + "type": "string", 62 + "description": "Full path to the repository" 63 + }, 64 + "defaultBranch": { 65 + "type": "string", 66 + "description": "Default branch of the repository" 67 + } 68 + } 69 + } 70 + } 71 + }
+1 -1
nix/vm.nix
··· 8 8 var = builtins.getEnv name; 9 9 in 10 10 if var == "" 11 - then throw "\$${name} must be defined, see docs/hacking.md for more details" 11 + then throw "\$${name} must be defined, see https://docs.tangled.org/hacking-on-tangled.html#hacking-on-tangled for more details" 12 12 else var; 13 13 envVarOr = name: default: let 14 14 var = builtins.getEnv name;
+3 -3
readme.md
··· 10 10 11 11 ## docs 12 12 13 - * [knot hosting guide](/docs/knot-hosting.md) 14 - * [contributing guide](/docs/contributing.md) **please read before opening a PR!** 15 - * [hacking on tangled](/docs/hacking.md) 13 + - [knot hosting guide](https://docs.tangled.org/knot-self-hosting-guide.html#knot-self-hosting-guide) 14 + - [contributing guide](https://docs.tangled.org/contribution-guide.html#contribution-guide) **please read before opening a PR!** 15 + - [hacking on tangled](https://docs.tangled.org/hacking-on-tangled.html#hacking-on-tangled) 16 16 17 17 ## security 18 18
+1 -1
spindle/motd
··· 20 20 ** 21 21 ******** 22 22 23 - This is a spindle server. More info at https://tangled.sh/@tangled.sh/core/tree/master/docs/spindle 23 + This is a spindle server. More info at https://docs.tangled.org/spindles.html#spindles 24 24 25 25 Most API routes are under /xrpc/
+21 -3
spindle/server.go
··· 8 8 "log/slog" 9 9 "maps" 10 10 "net/http" 11 + "sync" 11 12 12 13 "github.com/go-chi/chi/v5" 13 14 "tangled.org/core/api/tangled" ··· 30 31 ) 31 32 32 33 //go:embed motd 33 - var motd []byte 34 + var defaultMotd []byte 34 35 35 36 const ( 36 37 rbacDomain = "thisserver" ··· 47 48 cfg *config.Config 48 49 ks *eventconsumer.Consumer 49 50 res *idresolver.Resolver 50 - vault secrets.Manager 51 + vault secrets.Manager 52 + motd []byte 53 + motdMu sync.RWMutex 51 54 } 52 55 53 56 // New creates a new Spindle server with the provided configuration and engines. ··· 128 131 cfg: cfg, 129 132 res: resolver, 130 133 vault: vault, 134 + motd: defaultMotd, 131 135 } 132 136 133 137 err = e.AddSpindle(rbacDomain) ··· 201 205 return s.e 202 206 } 203 207 208 + // SetMotdContent sets custom MOTD content, replacing the embedded default. 209 + func (s *Spindle) SetMotdContent(content []byte) { 210 + s.motdMu.Lock() 211 + defer s.motdMu.Unlock() 212 + s.motd = content 213 + } 214 + 215 + // GetMotdContent returns the current MOTD content. 216 + func (s *Spindle) GetMotdContent() []byte { 217 + s.motdMu.RLock() 218 + defer s.motdMu.RUnlock() 219 + return s.motd 220 + } 221 + 204 222 // Start starts the Spindle server (blocking). 205 223 func (s *Spindle) Start(ctx context.Context) error { 206 224 // starts a job queue runner in the background ··· 246 264 mux := chi.NewRouter() 247 265 248 266 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 249 - w.Write(motd) 267 + w.Write(s.GetMotdContent()) 250 268 }) 251 269 mux.HandleFunc("/events", s.Events) 252 270 mux.HandleFunc("/logs/{knot}/{rkey}/{name}", s.Logs)
+3
types/diff.go
··· 74 74 75 75 // used by html elements as a unique ID for hrefs 76 76 func (d *Diff) Id() string { 77 + if d.IsDelete { 78 + return d.Name.Old 79 + } 77 80 return d.Name.New 78 81 } 79 82
+112
types/diff_test.go
··· 1 + package types 2 + 3 + import "testing" 4 + 5 + func TestDiffId(t *testing.T) { 6 + tests := []struct { 7 + name string 8 + diff Diff 9 + expected string 10 + }{ 11 + { 12 + name: "regular file uses new name", 13 + diff: Diff{ 14 + Name: struct { 15 + Old string `json:"old"` 16 + New string `json:"new"` 17 + }{Old: "", New: "src/main.go"}, 18 + }, 19 + expected: "src/main.go", 20 + }, 21 + { 22 + name: "new file uses new name", 23 + diff: Diff{ 24 + Name: struct { 25 + Old string `json:"old"` 26 + New string `json:"new"` 27 + }{Old: "", New: "src/new.go"}, 28 + IsNew: true, 29 + }, 30 + expected: "src/new.go", 31 + }, 32 + { 33 + name: "deleted file uses old name", 34 + diff: Diff{ 35 + Name: struct { 36 + Old string `json:"old"` 37 + New string `json:"new"` 38 + }{Old: "src/deleted.go", New: ""}, 39 + IsDelete: true, 40 + }, 41 + expected: "src/deleted.go", 42 + }, 43 + { 44 + name: "renamed file uses new name", 45 + diff: Diff{ 46 + Name: struct { 47 + Old string `json:"old"` 48 + New string `json:"new"` 49 + }{Old: "src/old.go", New: "src/renamed.go"}, 50 + IsRename: true, 51 + }, 52 + expected: "src/renamed.go", 53 + }, 54 + } 55 + 56 + for _, tt := range tests { 57 + t.Run(tt.name, func(t *testing.T) { 58 + if got := tt.diff.Id(); got != tt.expected { 59 + t.Errorf("Diff.Id() = %q, want %q", got, tt.expected) 60 + } 61 + }) 62 + } 63 + } 64 + 65 + func TestChangedFilesMatchesDiffId(t *testing.T) { 66 + // ChangedFiles() must return values matching each Diff's Id() 67 + // so that sidebar links point to the correct anchors. 68 + // Tests existing, deleted, new, and renamed files. 69 + nd := NiceDiff{ 70 + Diff: []Diff{ 71 + { 72 + Name: struct { 73 + Old string `json:"old"` 74 + New string `json:"new"` 75 + }{Old: "", New: "src/modified.go"}, 76 + }, 77 + { 78 + Name: struct { 79 + Old string `json:"old"` 80 + New string `json:"new"` 81 + }{Old: "src/deleted.go", New: ""}, 82 + IsDelete: true, 83 + }, 84 + { 85 + Name: struct { 86 + Old string `json:"old"` 87 + New string `json:"new"` 88 + }{Old: "", New: "src/new.go"}, 89 + IsNew: true, 90 + }, 91 + { 92 + Name: struct { 93 + Old string `json:"old"` 94 + New string `json:"new"` 95 + }{Old: "src/old.go", New: "src/renamed.go"}, 96 + IsRename: true, 97 + }, 98 + }, 99 + } 100 + 101 + changedFiles := nd.ChangedFiles() 102 + 103 + if len(changedFiles) != len(nd.Diff) { 104 + t.Fatalf("ChangedFiles() returned %d items, want %d", len(changedFiles), len(nd.Diff)) 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 + }