···11+---
22+atroot: true
33+template:
44+slug: 6-months
55+title: 6 months of Tangled
66+subtitle: a quick recap, and notes on the future
77+date: 2025-10-21
88+image: https://assets.tangled.network/blog/6-months.png
99+authors:
1010+ - name: Anirudh
1111+ email: anirudh@tangled.org
1212+ handle: anirudh.fi
1313+ - name: Akshay
1414+ email: akshay@tangled.org
1515+ handle: oppi.li
1616+draft: false
1717+---
1818+1919+Hello Tanglers! It's been over 6 months since we first announced
2020+Tangled, so we figured we'd do a quick retrospective of what we built so
2121+far and what's next.
2222+2323+If you're new here, here's a quick overview: Tangled is a git hosting
2424+and collaboration platform built on top of the [AT
2525+Protocol](https://atproto.com). You can read a bit more about our
2626+architecture [here](/intro).
2727+2828+## new logo and mascot: dolly!
2929+3030+Tangled finally has a logo! Designed by Akshay himself, Dolly is in
3131+reference to the first ever *cloned* mammal. For a full set of brand assets and guidelines, see our new [branding page](https://tangled.org/brand).
3232+3333+
3434+3535+With that, let's recap the major platform improvements so far!
3636+3737+## pull requests: doubling down on jujutsu
3838+3939+One of the first major features we built was our [pull requests
4040+system](/pulls), which follows a unique round-based submission & review
4141+approach. This was really fun to innovate on -- it remains one of
4242+Tangled's core differentiators, and one we plan to keep improving.
4343+4444+In the same vein, we're the first ever code forge to support [stacking
4545+pull requests](/stacking) using Jujutsu! We're big fans of the tool and
4646+we use it everyday as we hack on
4747+[tangled.org/core](https://tangled.org/@tangled.org/core).
4848+4949+Ultimately, we think PR-based collaboration should evolve beyond the
5050+traditional model, and we're excited to keep experimenting with new
5151+ideas that make code review and contribution easier!
5252+5353+## spindle
5454+5555+CI was our most requested feature, and we spent a *lot* of time debating
5656+how to approach it. We considered integrating with existing platforms,
5757+but none were good fits. So we gave in to NIH and [built spindle
5858+ourselves](/ci)! This allowed us to go in on Nix using Nixery to build
5959+CI images on the fly and cache them.
6060+6161+Spindle is still early but designed to be extensible and is AT-native.
6262+The current Docker/Nixery-based engine is limiting -- we plan to switch
6363+to micro VMs down the line to run full-fledged NixOS (and other base
6464+images). Meanwhile, if you've got ideas for other spindle backends
6565+(Kubernetes?!), we'd love to [hear from you](https://chat.tangled.org).
6666+6767+## XRPC APIs
6868+6969+We introduced a complete migration of the knotserver to an
7070+[XRPC](https://atproto.com/specs/xrpc) API. Alongside this, we also
7171+decoupled the knot from the appview by getting rid of the registration
7272+secret, which was centralizing. Knots (and spindles) simply declare
7373+their owner, and any appview can verify ownership. Once we stabilize the
7474+[lexicon definitions](lexicons) for these XRPC calls, building clients
7575+for knots, or alternate implementations should become much simpler.
7676+7777+[lexicons]: https://tangled.sh/@tangled.sh/core/tree/master/lexicons
7878+7979+## issues rework
8080+8181+Issues got a major rework (and facelift) too! They are now threaded:
8282+top-level comments with replies. This makes Q/A style discussions much
8383+easier to follow!
8484+8585+
8686+8787+## hosted PDS
8888+8989+A complaint we often recieved was the need for a Bluesky account to use
9090+Tangled; and besides, we realised that the overlap between Bluesky users
9191+and possible Tangled users only goes so far -- we aim to be a generic
9292+code forge after all, AT just happens to be an implementation
9393+detail.
9494+9595+To address this, we spun up the tngl.sh PDS hosted right here in
9696+Finland. The only way to get an account on this PDS is by [signing
9797+up](https://tangled.sh/signup). There's a lot we can do to improve this
9898+experience as a generic PDS host, but we're still working out details
9999+around that.
100100+101101+## labels
102102+103103+You can easily categorize issues and pulls via labels! There is plenty
104104+of customization available:
105105+106106+- labels can be basic, or they can have a key and value set, for example:
107107+`wontfix` or `priority/high`
108108+- labels can be constrained to a set of values: `priority: [high medium low]`
109109+- there can be multiple labels of a given type: `reviewed-by: @oppi.li`,
110110+`reviewed-by: @anirudh.fi`
111111+112112+The options are endless! You can access them via your repo's settings page.
113113+114114+<div class="flex justify-center items-center gap-2">
115115+ <figure class="w-full m-0 flex flex-col items-center">
116116+ <a href="https://assets.tangled.network/blog/labels_vignette.webp">
117117+ <img class="my-1 w-full h-auto cursor-pointer" src="https://assets.tangled.network/blog/labels_vignette.webp" alt="A set of labels applied to an issue.">
118118+ </a>
119119+ <figcaption class="text-center">A set of labels applied to an issue.</figcaption>
120120+ </figure>
121121+122122+ <figure class="w-1/3 m-0 flex flex-col items-center">
123123+ <a href="https://assets.tangled.network/blog/new_label_modal.png">
124124+ <img class="my-1 w-full h-auto cursor-pointer" src="https://assets.tangled.network/blog/new_label_modal.png" alt="Create custom key-value type labels.">
125125+ </a>
126126+ <figcaption class="text-center">Create custom key-value type labels.</figcaption>
127127+ </figure>
128128+</div>
129129+130130+131131+## notifications
132132+133133+In-app notifications now exist! You get notifications for a variety of events now:
134134+135135+* new issues/pulls on your repos (also for collaborators)
136136+* comments on your issues/pulls (also for collaborators)
137137+* close/reopen (or merge) of issues/pulls
138138+* new stars
139139+* new follows
140140+141141+All of this can be fine-tuned in [/settings/notifications](https://tangled.org/settings/notifications).
142142+143143+
144144+145145+146146+## the future
147147+148148+We're working on a *lot* of exciting new things and possibly some big
149149+announcements to come. Be on the lookout for:
150150+151151+* email notifications
152152+* preliminary support for issue and PR search
153153+* total "atprotation" [^1] -- the last two holdouts here are repo and pull records
154154+* total federation -- i.e. supporting third-party appviews by making it
155155+ reproducible
156156+* achieve complete independence from Bluesky PBC by hosting our own relay
157157+158158+That's all for now; we'll see you in the atmosphere! Meanwhile, if you'd like to contribute to projects on Tangled, make sure to check out the [good first issues page](https://tangled.org/goodfirstissues) to get started!
159159+160160+[^1]: atprotation implies a two-way sync between the PDS and appview. Currently, pull requests and repositories are not ingested -- so writing/updating either records on your PDS will not show up on the appview.
+214
blog/posts/ci.md
···11+---
22+atroot: true
33+template:
44+slug: ci
55+title: introducing spindle
66+subtitle: tangled's new CI runner is now generally available
77+date: 2025-08-06
88+authors:
99+ - name: Anirudh
1010+ email: anirudh@tangled.sh
1111+ handle: anirudh.fi
1212+ - name: Akshay
1313+ email: akshay@tangled.sh
1414+ handle: oppi.li
1515+---
1616+1717+Since launching Tangled, continuous integration has
1818+consistently topped our feature request list. Today, CI is
1919+no longer a wishlist item, but a fully-featured reality.
2020+2121+Meet **spindle**: Tangled's new CI runner built atop Nix and
2222+AT Protocol. In typical Tangled fashion we've been
2323+dogfooding spindle for a while now; this very blog post
2424+you're reading was [built and published using
2525+spindle](https://tangled.sh/@tangled.sh/site/pipelines/452/workflow/deploy.yaml).
2626+2727+Tangled is a new social-enabled Git collaboration platform,
2828+[read our intro](/intro) for more about the project.
2929+3030+
3131+3232+## how spindle works
3333+3434+Spindle is designed around simplicity and the decentralized
3535+nature of the AT Protocol. In ingests "pipeline" records and
3636+emits job status updates.
3737+3838+When you push code or open a pull request, the knot hosting
3939+your repository emits a pipeline event
4040+(`sh.tangled.pipeline`). Running as a dedicated service,
4141+spindle subscribes to these events via websocket connections
4242+to your knot.
4343+4444+Once triggered, spindle reads your pipeline manifest, spins
4545+up the necessary execution environment (covered below), and
4646+runs your defined workflow steps. Throughout execution, it
4747+streams real-time logs and status updates
4848+(`sh.tangled.pipeline.status`) back through websockets,
4949+which the Tangled appview subscribes to for live updates.
5050+5151+Over at the appview, these updates are ingested and stored,
5252+and logs are streamed live.
5353+5454+## spindle pipelines
5555+5656+The pipeline manifest is defined in YAML, and should be
5757+relatively familiar to those that have used other CI
5858+solutions. Here's a minimal example:
5959+6060+```yaml
6161+# test.yaml
6262+6363+when:
6464+ - event: ["push", "pull_request"]
6565+ branch: ["master"]
6666+6767+dependencies:
6868+ nixpkgs:
6969+ - go
7070+7171+steps:
7272+ - name: run all tests
7373+ environment:
7474+ CGO_ENABLED: 1
7575+ command: |
7676+ go test -v ./...
7777+```
7878+7979+You can read the [full manifest spec
8080+here](https://tangled.sh/@tangled.sh/core/blob/master/docs/spindle/pipeline.md),
8181+but the `dependencies` block is the real interesting bit.
8282+Dependencies for your workflow, like Go, Node.js, Python
8383+etc. can be pulled in from nixpkgs.
8484+[Nixpkgs](https://github.com/nixos/nixpkgs/) -- for the
8585+uninitiated -- is a vast collection of packages for the Nix
8686+package manager. Fortunately, you needn't know nor care
8787+about Nix to use it! Just head to https://search.nixos.org
8888+to find your package of choice (I'll bet 1€ that it's
8989+there[^1]), toss it in the list and run your build. The
9090+Nix-savvy of you lot will be happy to know that you can use
9191+custom registries too.
9292+9393+[^1]: I mean, if it isn't there, it's nowhere.
9494+9595+Workflow manifests are intentionally simple. We do not want
9696+to include a "marketplace" of workflows or complex job
9797+orchestration. The bulk of the work should be offloaded to a
9898+build system, and CI should be used simply for finishing
9999+touches. That being said, this is still the first revision
100100+for CI, there is a lot more on the roadmap!
101101+102102+Let's take a look at how spindle executes workflow steps.
103103+104104+## workflow execution
105105+106106+At present, the spindle "engine" supports just the Docker
107107+backend[^2]. Podman is known to work with the Docker socket
108108+feature enabled. Each step is run in a separate container,
109109+with the `/tangled/workspace` and `/nix` volumes persisted
110110+across steps.
111111+112112+[^2]: Support for additional backends like Firecracker are
113113+ planned. Contributions welcome!
114114+115115+The container image is built using
116116+[Nixery](https://nixery.dev). Nixery is a nifty little tool
117117+that takes a path-separated set of Nix packages and returns
118118+an OCI image with each package in a separate layer. Try this
119119+in your terminal if you've got Docker installed:
120120+121121+```
122122+docker run nixery.dev/bash/hello-go hello-go
123123+```
124124+125125+This should output `Hello, world!`. This is running the
126126+[hello-go](https://search.nixos.org/packages?channel=25.05&show=hello-go)
127127+package from nixpkgs.
128128+129129+Nixery is super handy since we can construct these images
130130+for CI environments on the fly, with all dependencies baked
131131+in, and the best part: caching for commonly used packages is
132132+free thanks to Docker (pre-existing layers get reused). We
133133+run a Nixery instance of our own at
134134+https://nixery.tangled.sh but you may override that if you
135135+choose to.
136136+137137+## debugging CI
138138+139139+We understand that debugging CI can be the worst. There are
140140+two parts to this problem:
141141+142142+- CI services often bring their own workflow definition
143143+ formats and it can sometimes be difficult to know why the
144144+ workflow won't run or why the workflow definition is
145145+ incorrect
146146+- The CI job itself fails, but this has more to do with the
147147+ build system of choice
148148+149149+To mend the first problem: we are making use of git
150150+[push-options](https://git-scm.com/docs/git-push#Documentation/git-push.txt--ooption).
151151+When you push to a repository with an option like so:
152152+153153+```
154154+git push origin master -o verbose-ci
155155+```
156156+157157+The server runs a basic set of analysis rules on your
158158+workflow file, and reports any errors:
159159+160160+```
161161+λ git push origin main -o verbose-ci
162162+ .
163163+ .
164164+ .
165165+ .
166166+remote: error: failed to parse workflow(s):
167167+remote: - at .tangled/workflows/fmt.yml: yaml: line 14: did not find expected key
168168+remote:
169169+remote: warning(s) on pipeline:
170170+remote: - at build.yml: workflow skipped: did not match trigger push
171171+```
172172+173173+The analysis performed at the moment is quite basic (expect
174174+it to get better over time), but it is already quite useful
175175+to help debug workflows that don't trigger!
176176+177177+## pipeline secrets
178178+179179+Secrets are a bit tricky since atproto has no notion of
180180+private data. Secrets are instead written directly from the
181181+appview to the spindle instance using [service
182182+auth](https://docs.bsky.app/docs/api/com-atproto-server-get-service-auth).
183183+In essence, the appview makes a signed request using the
184184+logged-in user's DID key; spindle verifies this signature by
185185+fetching the public key from the DID document.
186186+187187+
188188+189189+The secrets themselves are stored in a secret manager. By
190190+default, this is the same sqlite database that spindle uses.
191191+This is *fine* for self-hosters. The hosted, flagship
192192+instance at https://spindle.tangled.sh however uses
193193+[OpenBao](https://openbao.org), an OSS fork of HashiCorp
194194+Vault.
195195+196196+## get started now
197197+198198+You can run your own spindle instance pretty easily: the
199199+[spindle self-hosting
200200+guide](https://tangled.sh/@tangled.sh/core/blob/master/docs/spindle/hosting.md)
201201+should have you covered. Once done, head to your
202202+repository's settings tab and set it up! Doesn't work? Feel
203203+free to pop into [Discord](https://chat.tangled.sh) to get
204204+help -- we have a nice little crew that's always around to
205205+help.
206206+207207+All Tangled users have access to our hosted spindle
208208+instance, free of charge[^3]. You don't have any more
209209+excuses to not migrate to Tangled now -- [get
210210+started](https://tangled.sh/login) with your AT Protocol
211211+account today.
212212+213213+[^3]: We can't promise we won't charge for it at some point
214214+ but there will always be a free tier.
+241
blog/posts/docs.md
···11+---
22+atroot: true
33+template:
44+slug: docs
55+title: we rolled our own documentation site
66+subtitle: you don't need mintlify
77+date: 2026-01-12
88+authors:
99+ - name: Akshay
1010+ email: akshay@tangled.org
1111+ handle: oppi.li
1212+draft: false
1313+---
1414+1515+We recently organized our documentation and put it up on
1616+https://docs.tangled.org, using just pandoc. For several
1717+reasons, using pandoc to roll your own static sites is more
1818+than sufficient for small projects.
1919+2020+
2121+2222+## requirements
2323+2424+- Lives in [our
2525+ monorepo](https://tangled.org/tangled.org/core).
2626+- No JS: a collection of pages containing just text
2727+ should not require JS to view!
2828+- Searchability: in practice, documentation engines that
2929+ come bundled with a search-engine have always been lack
3030+ lustre. I tend to Ctrl+F or use an actual search engine in
3131+ most scenarios.
3232+- Low complexity: building, testing, deploying should be
3333+ easy.
3434+- Easy to style
3535+3636+## evaluating the ecosystem
3737+3838+I took the time to evaluate several documentation engine
3939+solutions:
4040+4141+- [Mintlify](https://www.mintlify.com/): It is quite obvious
4242+ from their homepage that mintlify is performing an AI
4343+ pivot for the sake of doing so.
4444+- [Docusaurus](https://docusaurus.io/): The generated
4545+ documentation site is quite nice, but the value of pages
4646+ being served as a full-blown React SPA is questionable.
4747+- [MkDocs](https://www.mkdocs.org/): Works great with JS
4848+ disabled, however the table of contents needs to be
4949+ maintained via `mkdocs.yml`, which can be quite tedious.
5050+- [MdBook](https://rust-lang.github.io/mdBook/index.html):
5151+ As above, you need a `SUMMARY.md` file to control the
5252+ table-of-contents.
5353+5454+MkDocs and MdBook are still on my radar however, in case we
5555+need a bigger feature set.
5656+5757+## using pandoc
5858+5959+[pandoc](https://pandoc.org/) is a wonderfully customizable
6060+markup converter. It provides a "chunkedhtml" output format,
6161+which is perfect for generating documentation sites. Without
6262+any customization,
6363+[this](https://pandoc.org/demo/example33/) is the generated
6464+output, for this [markdown file
6565+input](https://pandoc.org/demo/MANUAL.txt).
6666+6767+- You get an autogenerated TOC based on the document layout
6868+- Each section is turned into a page of its own
6969+7070+Massaging pandoc to work for us was quite straightforward:
7171+7272+- I first combined all our individual markdown files into
7373+ [one big
7474+ `DOCS.md`](https://tangled.org/tangled.org/core/blob/master/docs/DOCS.md)
7575+ file.
7676+- Modified the [default
7777+ template](https://github.com/jgm/pandoc-templates/blob/master/default.chunkedhtml)
7878+ to put the TOC on every page, to form a "sidebar", see
7979+ [`docs/template.html`](https://tangled.org/tangled.org/core/blob/master/docs/template.html)
8080+- Inserted tailwind `prose` classes where necessary, such
8181+ that markdown content is rendered the same way between
8282+ `tangled.org` and `docs.tangled.org`
8383+8484+Generating the docs is done with one pandoc command:
8585+8686+```bash
8787+pandoc docs/DOCS.md \
8888+ -o out/ \
8989+ -t chunkedhtml \
9090+ --variable toc \
9191+ --toc-depth=2 \
9292+ --css=docs/stylesheet.css \
9393+ --chunk-template="%i.html" \
9494+ --highlight-style=docs/highlight.theme \
9595+ --template=docs/template.html
9696+```
9797+9898+## avoiding javascript
9999+100100+The "sidebar" style table-of-contents needs to be collapsed
101101+on mobile displays. Most of the engines I evaluated seem to
102102+require JS to collapse and expand the sidebar, with MkDocs
103103+being the outlier, it uses a checkbox with the
104104+[`:checked`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:checked)
105105+pseudo-class trick to avoid JS.
106106+107107+The other ways to do this are:
108108+109109+- Use `<details` and `<summary>`: this is definitely a
110110+ "hack", clicking outside the sidebar does not collapse it.
111111+ Using Ctrl+F or "Find in page" still works through the
112112+ details tag though.
113113+- Use the new `popover` API: this seems like the perfect fit
114114+ for a "sidebar" component.
115115+116116+The bar at the top includes a button to trigger the popover:
117117+118118+```html
119119+<button popovertarget="toc-popover">Table of Contents</button>
120120+```
121121+122122+And a `fixed` position div includes the TOC itself:
123123+124124+```html
125125+<div id="toc-popover" popover class="fixed top-0">
126126+ <ul>
127127+ Quick Start
128128+ <li>...</li>
129129+ <li>...</li>
130130+ <li>...</li>
131131+ </ul>
132132+</div>
133133+```
134134+135135+The TOC is scrollable independently and can be collapsed by
136136+clicking anywhere on the screen outside the sidebar.
137137+Searching for content in the page via "Find in page" does
138138+not show any results that are present in the popover
139139+however. The collapsible TOC is only available on smaller
140140+viewports, the TOC is not hidden on larger viewports.
141141+142142+## search
143143+144144+There is no native search on the site for now. Taking
145145+inspiration from [https://htmx.org](https://htmx.org)'s search bar, our search
146146+bar also simply redirects to Google:
147147+148148+```html
149149+<form action="https://google.com/search">
150150+ <input type="hidden" name="q" value="+[inurl:https://docs.tangled.org]">
151151+ ...
152152+</form>
153153+```
154154+155155+I mentioned earlier that Ctrl+F has typically worked better
156156+for me than, say, the search engine provided by Docusaurus.
157157+To that end, the same docs have been exported to a ["single
158158+page" format](https://docs.tangled.org/single-page.html), by
159159+just removing the `chunkedhtml` related options:
160160+161161+```diff
162162+ pandoc docs/DOCS.md \
163163+ -o out/ \
164164+- -t chunkedhtml \
165165+ --variable toc \
166166+ --toc-depth=2 \
167167+ --css=docs/stylesheet.css \
168168+- --chunk-template="%i.html" \
169169+ --highlight-style=docs/highlight.theme \
170170+ --template=docs/template.html
171171+```
172172+173173+With all the content on a single page, it is trivial to
174174+search through the entire site with the browser. If the docs
175175+do outgrow this, I will consider other options!
176176+177177+## building and deploying
178178+179179+We use [nix](https://nixos.org) and
180180+[colmena](https://colmena.cli.rs/) to build and deploy all
181181+Tangled services. A nix derivation to [build the
182182+documentation](https://tangled.org/tangled.org/core/blob/master/nix/pkgs/docs.nix)
183183+site is written very easily with the `runCommandLocal`
184184+helper:
185185+186186+```nix
187187+runCommandLocal "docs" {} ''
188188+ .
189189+ .
190190+ .
191191+ ${pandoc}/bin/pandoc ${src}/docs/DOCS.md ...
192192+ .
193193+ .
194194+ .
195195+''
196196+```
197197+198198+The NixOS machine is configured to serve the site [via
199199+nginx](https://tangled.org/tangled.org/infra/blob/master/hosts/nixery/services/nginx.nix#L7):
200200+201201+```nix
202202+services.nginx = {
203203+ enable = true;
204204+ virtualHosts = {
205205+ "docs.tangled.org" = {
206206+ root = "${tangled-pkgs.docs}";
207207+ locations."/" = {
208208+ tryFiles = "$uri $uri/ =404";
209209+ index = "index.html";
210210+ };
211211+ };
212212+ };
213213+};
214214+```
215215+216216+And deployed using `colmena`:
217217+218218+```bash
219219+nix run nixpkgs#colmena -- apply
220220+```
221221+222222+To update the site, I first run:
223223+224224+```bash
225225+nix flake update tangled
226226+```
227227+228228+Which bumps the `tangled` flake input, and thus
229229+`tangled-pkgs.docs`. The above `colmena` invocation applies
230230+the changes to the machine serving the site.
231231+232232+## notes
233233+234234+Going homegrown has made it a lot easier to style the
235235+documentation site to match the main site. Unfortunately
236236+there are still a few discrepancies between pandoc's
237237+markdown rendering and
238238+[goldmark's](https://pkg.go.dev/github.com/yuin/goldmark/)
239239+markdown rendering (which is what we use in Tangled). We may
240240+yet roll our own SSG,
241241+[TigerStyle](https://tigerbeetle.com/blog/2025-02-27-why-we-designed-tigerbeetles-docs-from-scratch/)!
+64
blog/posts/intro.md
···11+---
22+atroot: true
33+template:
44+slug: intro
55+title: introducing tangled
66+subtitle: a git collaboration platform, built on atproto
77+date: 2025-03-02
88+authors:
99+ - name: Anirudh
1010+ email: anirudh@tangled.sh
1111+ handle: anirudh.fi
1212+---
1313+1414+1515+[Tangled](https://tangled.sh) is a new social-enabled Git collaboration
1616+platform, built on top of the [AT Protocol](https://atproto.com). We
1717+envision a place where developers have complete ownership of their code,
1818+open source communities can freely self-govern and most importantly,
1919+coding can be social and fun again.
2020+2121+There are several models for decentralized code collaboration platforms,
2222+ranging from ActivityPub's (Forgejo) federated model, to Radicle's
2323+entirely P2P model. Our approach attempts to be the best of both worlds
2424+by adopting atproto -- a protocol for building decentralized social
2525+applications with a central identity.
2626+2727+
2828+2929+Our approach to this is the idea of "knots". Knots are lightweight,
3030+headless servers that enable users to host Git repositories with ease.
3131+Knots are designed for either single or multi-tenant use which is
3232+perfect for self-hosting on a Raspberry Pi at home, or larger
3333+"community" servers. By default, Tangled provides managed knots where
3434+you can host your repositories for free.
3535+3636+The [App View][appview] at [tangled.sh](https://tangled.sh) acts as a
3737+consolidated "view" into the whole network, allowing users to access,
3838+clone and contribute to repositories hosted across different knots --
3939+completely seamlessly.
4040+4141+Tangled is still in its infancy, and we're building out several of its
4242+core features as we [dogfood it ourselves][dogfood]. We developed these
4343+three tenets to guide our decisions:
4444+4545+1. Ownership of data
4646+2. Low barrier to entry
4747+3. No compromise on user-experience
4848+4949+Collaborating on code isn't easy, and the tools and workflows we use
5050+should feel natural and stay out of the way. Tangled's architecture
5151+enables common workflows to work as you'd expect, all while remaining
5252+decentralized.
5353+5454+We believe that atproto has greatly simplfied one of the hardest parts
5555+of social media: having your friends on it. Today, we're rolling out
5656+invite-only access to Tangled -- join us on IRC at `#tangled` on
5757+[libera.chat](https://libera.chat) and we'll get you set up.
5858+5959+**Update**: Tangled is open to public, simply login at
6060+[tangled.sh/login](https://tangled.sh/login)! Have fun!
6161+6262+[pds]: https://atproto.com/guides/glossary#pds-personal-data-server
6363+[appview]: https://docs.bsky.app/docs/advanced-guides/federation-architecture#app-views
6464+[dogfood]: https://tangled.sh/@tangled.sh/core
+195
blog/posts/pulls.md
···11+---
22+atroot: true
33+template:
44+slug: pulls
55+title: the lifecycle of a pull request
66+subtitle: we shipped a bunch of PR features recently; here's how we built it
77+date: 2025-04-16
88+image: https://assets.tangled.network/blog/hidden-ref.png
99+authors:
1010+ - name: Anirudh
1111+ email: anirudh@tangled.sh
1212+ handle: anirudh.fi
1313+ - name: Akshay
1414+ email: akshay@tangled.sh
1515+ handle: oppi.li
1616+draft: false
1717+---
1818+1919+We've spent the last couple of weeks building out a pull
2020+request system for Tangled, and today we want to lift the
2121+hood and show you how it works.
2222+2323+If you're new to Tangled, [read our intro](/intro) for the
2424+full story!
2525+2626+You have three options to contribute to a repository:
2727+2828+- Paste a patch on the web UI
2929+- Compare two local branches (you'll see this only if you're a
3030+collaborator on the repo)
3131+- Compare across forks
3232+3333+Whatever you choose, at the core of every PR is the patch.
3434+First, you write some code. Then, you run `git diff` to
3535+produce a patch and make everyone's lives easier, or push to
3636+a branch, and we generate it ourselves by comparing against
3737+the target.
3838+3939+## patch generation
4040+4141+When you create a PR from a branch, we create a "patch" by
4242+calculating the difference between your branch and the
4343+target branch. Consider this scenario:
4444+4545+<figure class="max-w-[550px] m-auto flex flex-col items-center justify-center">
4646+ <img class="h-auto max-w-full" src="https://assets.tangled.network/blog/merge-base.png">
4747+ <figcaption class="text-center"><code>A</code> is the merge-base for
4848+<code>feature</code> and <code>main</code>.</figcaption>
4949+</figure>
5050+5151+Your `feature` branch has advanced 2 commits since you first
5252+branched out, but in the meanwhile, `main` has also advanced
5353+2 commits. Doing a trivial `git diff feature main` will
5454+produce a confusing patch:
5555+5656+- the patch will apply the changes from `X` and `Y`
5757+- the patch will **revert** the changes from `B` and `C`
5858+5959+We obviously do not want the second part! To only show the
6060+changes added by `feature`, we have to identify the
6161+"merge-base": the nearest common ancestor of `feature` and
6262+`main`.
6363+6464+6565+In this case, `A` is the nearest common ancestor, and
6666+subsequently, the patch calculated will contain just `X` and
6767+`Y`.
6868+6969+### ref comparisons across forks
7070+7171+The plumbing described above is easy to do across two
7272+branches, but what about forks? And what if they live on
7373+different servers altogether (as they can in Tangled!)?
7474+7575+Here's the concept: since we already have all the necessary
7676+components to compare two local refs, why not simply
7777+"localize" the remote ref?
7878+7979+In simpler terms, we instruct Git to fetch the target branch
8080+from the original repository and store it in your fork under
8181+a special name. This approach allows us to compare your
8282+changes against the most current version of the branch
8383+you're trying to contribute to, all while remaining within
8484+your fork.
8585+8686+<figure class="max-w-[550px] m-auto flex flex-col items-center justify-center">
8787+ <img class="h-auto max-w-full" src="https://assets.tangled.network/blog/hidden-ref.png">
8888+ <figcaption class="text-center">Hidden tracking ref.</figcaption>
8989+</figure>
9090+9191+We call this a "hidden tracking ref." When you create a pull
9292+request from a fork, we establish a refspec that tracks the
9393+remote branch, which we then use to generate a diff. A
9494+refspec is essentially a rule that tells Git how to map
9595+references between a remote and your local repository during
9696+fetch or push operations.
9797+9898+For example, if your fork has a feature branch called
9999+`feature-1`, and you want to make a pull request to the
100100+`main` branch of the original repository, we fetch the
101101+remote `main` into a local hidden ref using a refspec like
102102+this:
103103+104104+```
105105++refs/heads/main:refs/hidden/feature-1/main
106106+```
107107+108108+Since we already have a remote (`origin`, by default) to the
109109+original repository (remember, we cloned it earlier), we can
110110+use `fetch` with this refspec to bring the remote `main`
111111+branch into our local hidden ref. Each pull request gets its
112112+own hidden ref, hence the `refs/hidden/:localRef/:remoteRef`
113113+format. We keep this ref updated whenever you push new
114114+commits to your feature branch, ensuring that comparisons --
115115+and any potential merge conflicts -- are always based on the
116116+latest state of the target branch.
117117+118118+And just like earlier, we produce the patch by diffing your
119119+feature branch with the hidden tracking ref. Also, the entire pull
120120+request is stored as [an atproto record][atproto-record] and updated
121121+each time the patch changes.
122122+123123+[atproto-record]: https://pdsls.dev/at://did:plc:qfpnj4og54vl56wngdriaxug/sh.tangled.repo.pull/3lmwniim2i722
124124+125125+Neat, now that we have a patch; we can move on the hard
126126+part: code review.
127127+128128+129129+## your patch does the rounds
130130+131131+Tangled uses a "round-based" review format. Your initial
132132+submission starts "round 0". Once your submission receives
133133+scrutiny, you can address reviews and resubmit your patch.
134134+This resubmission starts "round 1". You keep whittling on
135135+your patch till it is good enough, and eventually merged (or
136136+closed if you are unlucky).
137137+138138+<figure class="max-w-[700px] m-auto flex flex-col items-center justify-center">
139139+ <img class="h-auto max-w-full" src="https://assets.tangled.network/blog/patch-pr-main.png">
140140+ <figcaption class="text-center">A new pull request with a couple
141141+rounds of reviews.</figcaption>
142142+</figure>
143143+144144+Rounds are a far superior to standard branch-based
145145+approaches:
146146+147147+- Submissions are immutable: how many times have your
148148+ reviews gone out-of-date because the author pushed commits
149149+ _during_ your review?
150150+- Reviews are attached to submissions: at a glance, it is
151151+ easy to tell which comment applies to which "version" of
152152+ the pull-request
153153+- The author can choose when to resubmit! They can commit as
154154+ much as they want to their branch, but a new round begins
155155+ when they choose to hit "resubmit"
156156+- It is possible to "interdiff" and observe changes made
157157+ across submissions (this is coming very soon to Tangled!)
158158+159159+This [post by Mitchell
160160+Hashimoto](https://mitchellh.com/writing/github-changesets)
161161+goes into further detail on what can be achieved with
162162+round-based reviews.
163163+164164+## future plans
165165+166166+To close off this post, we wanted to share some of our
167167+future plans for pull requests:
168168+169169+* `format-patch` support: both for pasting in the UI and
170170+ internally. This allows us to show commits in the PR page,
171171+ and offer different merge strategies to choose from
172172+ (squash, rebase, ...).
173173+ **Update 2025-08-12**: We have format-patch support!
174174+175175+* Gerrit-style `refs/for/main`: we're still hashing out the
176176+ details but being able to push commits to a ref to
177177+ "auto-create" a PR would be super handy!
178178+179179+* Change ID support: This will allow us to group changes
180180+ together and track them across multiple commits, and to
181181+ provide "history" for each change. This works great with [Jujutsu][jj].
182182+ **Update 2025-08-12**: This has now landed: https://blog.tangled.org/stacking
183183+184184+Join us on [Discord](https://chat.tangled.sh) or
185185+`#tangled` on libera.chat (the two are bridged, so we will
186186+never miss a message!). We are always available to help
187187+setup knots, listen to feedback on features, or even
188188+shepherd contributions!
189189+190190+**Update 2025-08-12**: We move fast, and we now have jujutsu support, and an
191191+early in-house CI: https://blog.tangled.org/ci. You no longer need a Bluesky
192192+account to sign-up; head to https://tangled.sh/signup and sign up with your
193193+email!
194194+195195+[jj]: https://jj-vcs.github.io/jj/latest/
+74
blog/posts/seed.md
···11+---
22+atroot: true
33+template:
44+slug: seed
55+title: announcing our €3,8M seed round
66+subtitle: and more on what's next
77+date: 2026-03-02
88+image: https://assets.tangled.network/blog/seed.png
99+authors:
1010+ - name: Anirudh
1111+ email: anirudh@tangled.org
1212+ handle: anirudh.fi
1313+---
1414+1515+
1616+1717+Today, we're announcing our €3,8M ($4.5M) financing round led by
1818+[byFounders](https://byfounders.vc), with participation from [Bain
1919+Capital Crypto](https://baincapitalcrypto.com/),
2020+[Antler](https://antler.co), Thomas Dohmke (former GitHub CEO), Avery
2121+Pennarun (CEO of Tailscale), among other incredible angels.
2222+2323+For the past year, we've been building Tangled from the ground up --
2424+starting from first principles and asking ourselves what code
2525+collaboration should really look like. We made deliberate,
2626+[future-facing technology](https://anirudh.fi/future) choices. We chose
2727+to build on top of the AT Protocol as it helped us realize a federated,
2828+open network where users can own their code and social data. We shipped
2929+stacked PRs to enable more efficient contribution and review workflows.
3030+What started off as a side project, grew to over 7k+ users, who've
3131+created over 5k+ repositories.
3232+3333+Our vision for Tangled has always been big: we want to build the best
3434+code forge ever, and become foundational infrastructure for the next
3535+generation of open source. Whatever that looks like: hundreds of devs
3636+building artisanal libraries, or one dev and a hundred agents building a
3737+micro-SaaS.
3838+3939+And finding the right investors to help us acheive this vision wasn't
4040+something we took lightly. We spent months getting to know potential
4141+partners -- among which, byFounders stood out immediately. Like us,
4242+they're community-driven at their core, and their commitment to
4343+transparency runs deep -- you can see the very term sheet we signed on
4444+their website! With these shared fundamental values, we knew byFounders
4545+were the right people to have in our corner and we're incredibly excited
4646+to work with them.
4747+4848+## what's next
4949+5050+We're heads down building. For 2026, expect to see:
5151+5252+* a fully revamped CI (spindle v2!) built on micro VMs to allow for
5353+ faster builds and more choice of build environments. Oh, and a proper
5454+ Nix CI -- we know you want it.
5555+* protocol-level improvements across the board that'll unlock nifty
5656+ things like repo migrations across knots, organizations, and more!
5757+* a customizable "mission control" dashboard for your active PRs,
5858+ issues, and anything else you might want to track.
5959+* a migration tool to help you move off GitHub
6060+* all things search: code search, repo search, etc.
6161+* platform and infrastructure performance improvements & more global
6262+presence
6363+* Tangled CLI!
6464+6565+If all this sounds exciting to you: we're growing our team! Shoot us
6666+[an email](mailto:team@tangled.org) telling us a bit about yourself and
6767+any past work that might be relevant, and what part of the roadmap
6868+interests you most. We can hire from (almost) anywhere.
6969+7070+New to Tangled? [Get started here](https://docs.tangled.org/). Oh, and
7171+come hang on [Discord](https://chat.tangled.org)!
7272+7373+A sincere thank you to everyone that helped us get here -- we're giddy
7474+about what's to come.
+351
blog/posts/stacking.md
···11+---
22+atroot: true
33+template:
44+slug: stacking
55+title: jujutsu on tangled
66+subtitle: tangled now supports jujutsu change-ids!
77+date: 2025-06-02
88+image: https://assets.tangled.network/blog/interdiff_difference.jpeg
99+authors:
1010+ - name: Akshay
1111+ email: akshay@tangled.sh
1212+ handle: oppi.li
1313+draft: false
1414+---
1515+1616+Jujutsu is built around structuring your work into
1717+meaningful commits. Naturally, during code-review, you'd
1818+expect reviewers to be able to comment on individual
1919+commits, and also see the evolution of a commit over time,
2020+as reviews are addressed. We set out to natively support
2121+this model of code-review on Tangled.
2222+2323+Tangled is a new social-enabled Git collaboration platform,
2424+[read our intro](/intro) for more about the project.
2525+2626+For starters, I would like to contrast the two schools of
2727+code-review, the "diff-soup" model and the interdiff model.
2828+2929+## the diff-soup model
3030+3131+When you create a PR on traditional code forges (GitHub
3232+specifically), the UX implicitly encourages you to address
3333+your code review by *adding commits* on top of your original
3434+set of changes:
3535+3636+- GitHub's "Apply Suggestion" button directly commits the
3737+ suggestion into your PR
3838+- GitHub only shows you the diff of all files at once by
3939+ default
4040+- It is difficult to know what changed across force pushes
4141+4242+Consider a hypothetical PR that adds 3 commits:
4343+4444+```
4545+[c] implement new feature across the board (HEAD)
4646+ |
4747+[b] introduce new feature
4848+ |
4949+[a] some small refactor
5050+```
5151+5252+And when only newly added commits are easy to review, this
5353+is what ends up happening:
5454+5555+```
5656+[f] formatting & linting (HEAD)
5757+ |
5858+[e] update name of new feature
5959+ |
6060+[d] fix bug in refactor
6161+ |
6262+[c] implement new feature across the board
6363+ |
6464+[b] introduce new feature
6565+ |
6666+[a] some small refactor
6767+```
6868+6969+It is impossible to tell what addresses what at a glance,
7070+there is an implicit relation between each change:
7171+7272+```
7373+[f] formatting & linting
7474+ |
7575+[e] update name of new feature -------------.
7676+ | |
7777+[d] fix bug in refactor -----------. |
7878+ | | |
7979+[c] implement new feature across the board |
8080+ | | |
8181+[b] introduce new feature <-----------------'
8282+ | |
8383+[a] some small refactor <----------'
8484+```
8585+8686+This has the downside of clobbering the output of `git
8787+blame` (if there is a bug in the new feature, you will first
8888+land on `e`, and upon digging further, you will land on
8989+`b`). This becomes incredibly tricky to navigate if reviews
9090+go on through multiple cycles.
9191+9292+9393+## the interdiff model
9494+9595+With jujutsu however, you have the tools at hand to
9696+fearlessly edit, split, squash and rework old commits (you
9797+can absolutely achieve this with git and interactive
9898+rebasing, but it is certainly not trivial).
9999+100100+Let's try that again:
101101+102102+```
103103+[c] implement new feature across the board (HEAD)
104104+ |
105105+[b] introduce new feature
106106+ |
107107+[a] some small refactor
108108+```
109109+110110+To fix the bug in the refactor:
111111+112112+```
113113+$ jj edit a
114114+Working copy (@) now at: [a] some small refactor
115115+116116+$ # hack hack hack
117117+118118+$ jj log -r a::
119119+Rebased 2 descendant commits onto updated working copy
120120+[c] implement new feature across the board (HEAD)
121121+ |
122122+[b] introduce new feature
123123+ |
124124+[a] some small refactor
125125+```
126126+127127+Jujutsu automatically rebases the descendants without having
128128+to lift a finger. Brilliant! You can repeat the same
129129+exercise for all review comments, and effectively, your
130130+PR will have evolved like so:
131131+132132+```
133133+ a -> b -> c initial attempt
134134+ | | |
135135+ v v v
136136+ a' -> b' -> c' after first cycle of reviews
137137+```
138138+139139+## the catch
140140+141141+If you use `git rebase`, you will know that it modifies
142142+history and therefore changes the commit SHA. How then,
143143+should one tell the difference between the "old" and "new"
144144+state of affairs?
145145+146146+Tools like `git-range-diff` make use of a variety of
147147+text-based heuristics to roughly match `a` to `a'` and `b`
148148+to `b'` etc.
149149+150150+Jujutsu however, works around this by assigning stable
151151+"change id"s to each change (which internally point to a git
152152+commit, if you use the git backing). If you edit a commit,
153153+its SHA changes, but its change-id remains the same.
154154+155155+And this is the essence of our new stacked PRs feature!
156156+157157+## interdiff code review on tangled
158158+159159+To really explain how this works, let's start with a [new
160160+codebase](https://tangled.sh/@oppi.li/stacking-demo/):
161161+162162+```
163163+$ jj git init --colocate
164164+165165+# -- initialize codebase --
166166+167167+$ jj log
168168+@ n set: introduce Set type main HEAD 1h
169169+```
170170+171171+I have kicked things off by creating a new go module that
172172+adds a `HashSet` data structure. My first changeset
173173+introduces some basic set operations:
174174+175175+```
176176+$ jj log
177177+@ so set: introduce set difference HEAD
178178+├ sq set: introduce set intersection
179179+├ mk set: introduce set union
180180+├ my set: introduce basic set operations
181181+~
182182+183183+$ jj git push -c @
184184+Changes to push to origin:
185185+ Add bookmark push-soqmukrvport to fc06362295bd
186186+```
187187+188188+When submitting a pull request, select "Submit as stacked PRs":
189189+190190+<figure class="max-w-[550px] m-auto flex flex-col items-center justify-center">
191191+ <a href="https://assets.tangled.network/blog/submit_stacked.jpeg">
192192+ <img class="my-1 h-auto max-w-full" src="https://assets.tangled.network/blog/submit_stacked.jpeg">
193193+ </a>
194194+ <figcaption class="text-center">Submitting Stacked PRs</figcaption>
195195+</figure>
196196+197197+This submits each change as an individual pull request:
198198+199199+<figure class="max-w-[550px] m-auto flex flex-col items-center justify-center">
200200+ <a href="https://assets.tangled.network/blog/top_of_stack.jpeg">
201201+ <img class="my-1 h-auto max-w-full" src="https://assets.tangled.network/blog/top_of_stack.jpeg">
202202+ </a>
203203+ <figcaption class="text-center">The "stack" is similar to Gerrit's relation chain</figcaption>
204204+</figure>
205205+206206+After a while, I receive a couple of review comments, not on
207207+my entire submission, but rather, on each *individual
208208+change*. Additionally, the reviewer is happy with my first
209209+change, and has gone ahead and merged that:
210210+211211+<div class="flex justify-center items-start gap-2">
212212+ <figure class="w-1/3 m-0 flex flex-col items-center">
213213+ <a href="https://assets.tangled.network/blog/basic_merged.jpeg">
214214+ <img class="my-1 w-full h-auto cursor-pointer" src="https://assets.tangled.network/blog/basic_merged.jpeg" alt="The first change has been merged">
215215+ </a>
216216+ <figcaption class="text-center">The first change has been merged</figcaption>
217217+ </figure>
218218+219219+ <figure class="w-1/3 m-0 flex flex-col items-center">
220220+ <a href="https://assets.tangled.network/blog/review_union.jpeg">
221221+ <img class="my-1 w-full h-auto cursor-pointer" src="https://assets.tangled.network/blog/review_union.jpeg" alt="A review on the set union implementation">
222222+ </a>
223223+ <figcaption class="text-center">A review on the set union implementation</figcaption>
224224+ </figure>
225225+226226+ <figure class="w-1/3 m-0 flex flex-col items-center">
227227+ <a href="https://assets.tangled.network/blog/review_difference.jpeg">
228228+ <img class="my-1 w-full h-auto cursor-pointer" src="https://assets.tangled.network/blog/review_difference.jpeg" alt="A review on the set difference implementation">
229229+ </a>
230230+ <figcaption class="text-center">A review on the set difference implementation</figcaption>
231231+ </figure>
232232+</div>
233233+234234+Let us address the first review:
235235+236236+> can you use the new `maps.Copy` api here?
237237+238238+```
239239+$ jj log
240240+@ so set: introduce set difference push-soqmukrvport
241241+├ sq set: introduce set intersection
242242+├ mk set: introduce set union
243243+├ my set: introduce basic set operations
244244+~
245245+246246+# let's edit the implementation of `Union`
247247+$ jj edit mk
248248+249249+# hack, hack, hack
250250+251251+$ jj log
252252+Rebased 2 descendant commits onto updated working copy
253253+├ so set: introduce set difference push-soqmukrvport*
254254+├ sq set: introduce set intersection
255255+@ mk set: introduce set union
256256+├ my set: introduce basic set operations
257257+~
258258+```
259259+260260+Next, let us address the bug:
261261+262262+> there is a logic bug here, the condition should be negated.
263263+264264+```
265265+# let's edit the implementation of `Difference`
266266+$ jj edit so
267267+268268+# hack, hack, hack
269269+```
270270+271271+We are done addressing reviews:
272272+```
273273+$ jj git push
274274+Changes to push to origin:
275275+ Move sideways bookmark push-soqmukrvport from fc06362295bd to dfe2750f6d40
276276+```
277277+278278+Upon resubmitting the PR for review, Tangled is able to
279279+accurately trace the commit across rewrites, using jujutsu
280280+change-ids, and map it to the corresponding PR:
281281+282282+<div class="flex justify-center items-start gap-2">
283283+ <figure class="w-1/2 m-0 flex flex-col items-center">
284284+ <a href="https://assets.tangled.network/blog/round_2_union.jpeg">
285285+ <img class="my-1 w-full h-auto" src="https://assets.tangled.network/blog/round_2_union.jpeg" alt="PR #2 advances to the next round">
286286+ </a>
287287+ <figcaption class="text-center">PR #2 advances to the next round</figcaption>
288288+ </figure>
289289+290290+ <figure class="w-1/2 m-0 flex flex-col items-center">
291291+ <a href="https://assets.tangled.network/blog/round_2_difference.jpeg">
292292+ <img class="my-1 w-full h-auto" src="https://assets.tangled.network/blog/round_2_difference.jpeg" alt="PR #4 advances to the next round">
293293+ </a>
294294+ <figcaption class="text-center">PR #4 advances to the next round</figcaption>
295295+ </figure>
296296+</div>
297297+298298+Of note here are a few things:
299299+300300+- The initial submission is still visible under `round #0`
301301+- By resubmitting, the round has simply advanced to `round
302302+ #1`
303303+- There is a helpful "interdiff" button to look at the
304304+ difference between the two submissions
305305+306306+The individual diffs are still available, but most
307307+importantly, the reviewer can view the *evolution* of a
308308+change by hitting the interdiff button:
309309+310310+<div class="flex justify-center items-start gap-2">
311311+ <figure class="w-1/2 m-0 flex flex-col items-center">
312312+ <a href="https://assets.tangled.network/blog/diff_1_difference.jpeg">
313313+ <img class="my-1 w-full h-auto" src="https://assets.tangled.network/blog/diff_1_difference.jpeg" alt="Diff from round #0">
314314+ </a>
315315+ <figcaption class="text-center">Diff from round #0</figcaption>
316316+ </figure>
317317+318318+ <figure class="w-1/2 m-0 flex flex-col items-center">
319319+ <a href="https://assets.tangled.network/blog/diff_2_difference.jpeg">
320320+ <img class="my-1 w-full h-auto" src="https://assets.tangled.network/blog/diff_2_difference.jpeg" alt="Diff from round #1">
321321+ </a>
322322+ <figcaption class="text-center">Diff from round #1</figcaption>
323323+ </figure>
324324+</div>
325325+326326+<figure class="max-w-[550px] m-auto flex flex-col items-center justify-center">
327327+ <a href="https://assets.tangled.network/blog/interdiff_difference.jpeg">
328328+ <img class="my-1 w-full h-auto" src="https://assets.tangled.network/blog/interdiff_difference.jpeg" alt="Interdiff between round #0 and #1">
329329+ </a>
330330+ <figcaption class="text-center">Interdiff between round #1 and #0</figcaption>
331331+</figure>
332332+333333+Indeed, the logic bug has been addressed!
334334+335335+## start stacking today
336336+337337+If you are a jujutsu user, you can enable this flag on more
338338+recent versions of jujutsu:
339339+340340+```
341341+λ jj --version
342342+jj 0.29.0-8c7ca30074767257d75e3842581b61e764d022cf
343343+344344+# -- in your config.toml file --
345345+[git]
346346+write-change-id-header = true
347347+```
348348+349349+This feature writes `change-id` headers directly into the
350350+git commit object, and is visible to code forges upon push,
351351+and allows you to stack your PRs on Tangled.