diff --git a/go.mod b/go.mod
--- a/go.mod
+++ b/go.mod
@@ -61,9 +61,11 @@
require (
dario.cat/mergo v1.0.1 // indirect
+ github.com/BurntSushi/toml v0.3.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect
+ github.com/adrg/frontmatter v0.2.0 // indirect
github.com/alecthomas/repr v0.5.2 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
@@ -231,6 +233,7 @@
gopkg.in/fsnotify.v1 v1.4.7 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
gotest.tools/v3 v3.5.2 // indirect
lukechampine.com/blake3 v1.4.1 // indirect
)
diff --git a/go.sum b/go.sum
--- a/go.sum
+++ b/go.sum
@@ -4,6 +4,7 @@
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Blank-Xu/sql-adapter v1.1.1 h1:+g7QXU9sl/qT6Po97teMpf3GjAO0X9aFaqgSePXvYko=
github.com/Blank-Xu/sql-adapter v1.1.1/go.mod h1:o2g8EZhZ3TudnYEGDkoU+3jCTCgDgx1o/Ig5ajKkaLY=
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
@@ -11,6 +12,8 @@
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg=
github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0=
+github.com/adrg/frontmatter v0.2.0 h1:/DgnNe82o03riBd1S+ZDjd43wAmC6W35q67NHeLkPd4=
+github.com/adrg/frontmatter v0.2.0/go.mod h1:93rQCj3z3ZlwyxxpQioRKC1wDLto4aXHrbqIsnH9wmE=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
@@ -770,6 +773,7 @@
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/tailwind.config.js b/tailwind.config.js
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -2,74 +2,87 @@
const colors = require("tailwindcss/colors");
module.exports = {
- content: ["./appview/pages/templates/**/*.html", "./appview/pages/chroma.go", "./docs/*.html"],
- darkMode: "media",
- theme: {
- container: {
- padding: "2rem",
- center: true,
- screens: {
- sm: "500px",
- md: "600px",
- lg: "800px",
- xl: "1000px",
- "2xl": "1200px",
- },
- },
- extend: {
- fontFamily: {
- sans: ["InterVariable", "system-ui", "sans-serif", "ui-sans-serif"],
- mono: [
- "IBMPlexMono",
- "ui-monospace",
- "SFMono-Regular",
- "Menlo",
- "Monaco",
- "Consolas",
- "Liberation Mono",
- "Courier New",
- "monospace",
- ],
- },
- typography: {
- DEFAULT: {
- css: {
- maxWidth: "none",
- pre: {
- "@apply font-normal text-black bg-gray-50 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-700 border": {},
- },
- code: {
- "@apply font-normal font-mono p-1 rounded text-black bg-gray-100 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-700": {},
- },
- "code::before": {
- content: '""',
- },
- "code::after": {
- content: '""',
- },
- blockquote: {
- quotes: "none",
- },
- 'h1, h2, h3, h4': {
- "@apply mt-4 mb-2": {}
- },
- h1: {
- "@apply mt-3 pb-3 border-b border-gray-300 dark:border-gray-600": {}
- },
- h2: {
- "@apply mt-3 pb-3 border-b border-gray-200 dark:border-gray-700": {}
- },
- h3: {
- "@apply mt-2": {}
- },
- },
- },
- },
- gridTemplateColumns: {
- '14': 'repeat(14, minmax(0, 1fr))',
- '28': 'repeat(28, minmax(0, 1fr))',
- }
- },
- },
- plugins: [require("@tailwindcss/typography")],
+ content: [
+ "./appview/pages/templates/**/*.html",
+ "./appview/pages/chroma.go",
+ "./docs/*.html",
+ "./blog/templates/**/*.html",
+ "./blog/posts/**/*.md",
+ ],
+ darkMode: "media",
+ theme: {
+ container: {
+ padding: "2rem",
+ center: true,
+ screens: {
+ sm: "500px",
+ md: "600px",
+ lg: "800px",
+ xl: "1000px",
+ "2xl": "1200px",
+ },
+ },
+ extend: {
+ fontFamily: {
+ sans: ["InterVariable", "system-ui", "sans-serif", "ui-sans-serif"],
+ mono: [
+ "IBMPlexMono",
+ "ui-monospace",
+ "SFMono-Regular",
+ "Menlo",
+ "Monaco",
+ "Consolas",
+ "Liberation Mono",
+ "Courier New",
+ "monospace",
+ ],
+ },
+ typography: {
+ DEFAULT: {
+ css: {
+ maxWidth: "none",
+ pre: {
+ "@apply font-normal text-black bg-gray-50 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-700 border":
+ {},
+ },
+ code: {
+ "@apply font-normal font-mono p-1 rounded text-black bg-gray-100 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-700":
+ {},
+ },
+ "code::before": {
+ content: '""',
+ },
+ "code::after": {
+ content: '""',
+ },
+ blockquote: {
+ quotes: "none",
+ },
+ "h1, h2, h3, h4": {
+ "@apply mt-4 mb-2": {},
+ },
+ h1: {
+ "@apply mt-3 pb-3 border-b border-gray-300 dark:border-gray-600":
+ {},
+ },
+ h2: {
+ "@apply mt-3 pb-3 border-b border-gray-200 dark:border-gray-700":
+ {},
+ },
+ h3: {
+ "@apply mt-2": {},
+ },
+ img: {
+ "@apply rounded border border-gray-200 dark:border-gray-700": {},
+ },
+ },
+ },
+ },
+ gridTemplateColumns: {
+ 14: "repeat(14, minmax(0, 1fr))",
+ 28: "repeat(28, minmax(0, 1fr))",
+ },
+ },
+ },
+ plugins: [require("@tailwindcss/typography")],
};
diff --git a/blog/blog.go b/blog/blog.go
new file mode 100644
--- /dev/null
+++ b/blog/blog.go
@@ -0,0 +1,174 @@
+package blog
+
+import (
+ "bytes"
+ "cmp"
+ "html/template"
+ "io"
+ "io/fs"
+ "os"
+ "slices"
+ "strings"
+ "time"
+
+ "github.com/adrg/frontmatter"
+ "github.com/gorilla/feeds"
+
+ "tangled.org/core/appview/pages"
+ "tangled.org/core/appview/pages/markup"
+ textension "tangled.org/core/appview/pages/markup/extension"
+)
+
+type Author struct {
+ Name string `yaml:"name"`
+ Email string `yaml:"email"`
+ Handle string `yaml:"handle"`
+}
+
+type PostMeta struct {
+ Slug string `yaml:"slug"`
+ Title string `yaml:"title"`
+ Subtitle string `yaml:"subtitle"`
+ Date string `yaml:"date"`
+ Authors []Author `yaml:"authors"`
+ Image string `yaml:"image"`
+ Draft bool `yaml:"draft"`
+}
+
+type Post struct {
+ Meta PostMeta
+ Body template.HTML
+}
+
+func (p Post) ParsedDate() time.Time {
+ t, _ := time.Parse("2006-01-02", p.Meta.Date)
+ return t
+}
+
+type indexParams struct {
+ LoggedInUser any
+ Posts []Post
+ Featured []Post
+}
+
+type postParams struct {
+ LoggedInUser any
+ Post Post
+}
+
+// Posts parses and returns all non-draft posts sorted newest-first.
+func Posts(postsDir string) ([]Post, error) {
+ return parsePosts(postsDir, false)
+}
+
+// AllPosts parses and returns all posts including drafts, sorted newest-first.
+func AllPosts(postsDir string) ([]Post, error) {
+ return parsePosts(postsDir, true)
+}
+
+func parsePosts(postsDir string, includeDrafts bool) ([]Post, error) {
+ fsys := os.DirFS(postsDir)
+
+ entries, err := fs.ReadDir(fsys, ".")
+ if err != nil {
+ return nil, err
+ }
+
+ rctx := &markup.RenderContext{
+ RendererType: markup.RendererTypeDefault,
+ Sanitizer: markup.NewSanitizer(),
+ }
+ var posts []Post
+ for _, entry := range entries {
+ if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
+ continue
+ }
+
+ data, err := fs.ReadFile(fsys, entry.Name())
+ if err != nil {
+ return nil, err
+ }
+
+ var meta PostMeta
+ rest, err := frontmatter.Parse(bytes.NewReader(data), &meta)
+ if err != nil {
+ return nil, err
+ }
+
+ if meta.Draft && !includeDrafts {
+ continue
+ }
+
+ htmlStr := rctx.RenderMarkdownWith(string(rest), markup.NewMarkdownWith("", textension.Dashes))
+ sanitized := rctx.SanitizeDefault(htmlStr)
+
+ posts = append(posts, Post{
+ Meta: meta,
+ Body: template.HTML(sanitized),
+ })
+ }
+
+ slices.SortFunc(posts, func(a, b Post) int {
+ return cmp.Compare(b.Meta.Date, a.Meta.Date)
+ })
+
+ return posts, nil
+}
+
+func AtomFeed(posts []Post, baseURL string) (string, error) {
+ feed := &feeds.Feed{
+ Title: "the tangled blog",
+ Link: &feeds.Link{Href: baseURL},
+ Author: &feeds.Author{Name: "Tangled"},
+ Created: time.Now(),
+ }
+
+ for _, p := range posts {
+ postURL := strings.TrimRight(baseURL, "/") + "/" + p.Meta.Slug
+
+ var authorName string
+ for i, a := range p.Meta.Authors {
+ if i > 0 {
+ authorName += " & "
+ }
+ authorName += a.Name
+ }
+
+ feed.Items = append(feed.Items, &feeds.Item{
+ Title: p.Meta.Title,
+ Link: &feeds.Link{Href: postURL},
+ Description: p.Meta.Subtitle,
+ Author: &feeds.Author{Name: authorName},
+ Created: p.ParsedDate(),
+ })
+ }
+
+ return feed.ToAtom()
+}
+
+// RenderIndex renders the blog index page to w.
+func RenderIndex(p *pages.Pages, templatesDir string, posts []Post, w io.Writer) error {
+ tpl, err := p.ParseWith(os.DirFS(templatesDir), "index.html")
+ if err != nil {
+ return err
+ }
+ var featured []Post
+ for _, post := range posts {
+ if post.Meta.Image != "" {
+ featured = append(featured, post)
+ if len(featured) == 3 {
+ break
+ }
+ }
+ }
+ return tpl.ExecuteTemplate(w, "layouts/base", indexParams{Posts: posts, Featured: featured})
+}
+
+// RenderPost renders a single blog post page to w.
+func RenderPost(p *pages.Pages, templatesDir string, post Post, w io.Writer) error {
+ tpl, err := p.ParseWith(os.DirFS(templatesDir), "post.html")
+ if err != nil {
+ return err
+ }
+ return tpl.ExecuteTemplate(w, "layouts/base", postParams{Post: post})
+}
diff --git a/blog/config.yaml b/blog/config.yaml
new file mode 100644
--- /dev/null
+++ b/blog/config.yaml
@@ -0,0 +1,12 @@
+preBuild:
+ - tailwindcss -i ../input.css -o static/tw.css
+title: the tangled blog
+# note the trailing slash!
+url: "https://blog.tangled.org/"
+description: ""
+author:
+ name: "Anirudh Oppiliappan"
+ email: "anirudh@tangled.sh"
+defaultTemplate: text.html
+extraTemplateDirs:
+ - ../appview/pages/templates
diff --git a/blog/posts/6-months.md b/blog/posts/6-months.md
new file mode 100644
--- /dev/null
+++ b/blog/posts/6-months.md
@@ -0,0 +1,160 @@
+---
+atroot: true
+template:
+slug: 6-months
+title: 6 months of Tangled
+subtitle: a quick recap, and notes on the future
+date: 2025-10-21
+image: https://assets.tangled.network/blog/6-months.png
+authors:
+ - name: Anirudh
+ email: anirudh@tangled.org
+ handle: anirudh.fi
+ - name: Akshay
+ email: akshay@tangled.org
+ handle: oppi.li
+draft: false
+---
+
+Hello Tanglers! It's been over 6 months since we first announced
+Tangled, so we figured we'd do a quick retrospective of what we built so
+far and what's next.
+
+If you're new here, here's a quick overview: Tangled is a git hosting
+and collaboration platform built on top of the [AT
+Protocol](https://atproto.com). You can read a bit more about our
+architecture [here](/intro).
+
+## new logo and mascot: dolly!
+
+Tangled finally has a logo! Designed by Akshay himself, Dolly is in
+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).
+
+
+
+With that, let's recap the major platform improvements so far!
+
+## pull requests: doubling down on jujutsu
+
+One of the first major features we built was our [pull requests
+system](/pulls), which follows a unique round-based submission & review
+approach. This was really fun to innovate on -- it remains one of
+Tangled's core differentiators, and one we plan to keep improving.
+
+In the same vein, we're the first ever code forge to support [stacking
+pull requests](/stacking) using Jujutsu! We're big fans of the tool and
+we use it everyday as we hack on
+[tangled.org/core](https://tangled.org/@tangled.org/core).
+
+Ultimately, we think PR-based collaboration should evolve beyond the
+traditional model, and we're excited to keep experimenting with new
+ideas that make code review and contribution easier!
+
+## spindle
+
+CI was our most requested feature, and we spent a *lot* of time debating
+how to approach it. We considered integrating with existing platforms,
+but none were good fits. So we gave in to NIH and [built spindle
+ourselves](/ci)! This allowed us to go in on Nix using Nixery to build
+CI images on the fly and cache them.
+
+Spindle is still early but designed to be extensible and is AT-native.
+The current Docker/Nixery-based engine is limiting -- we plan to switch
+to micro VMs down the line to run full-fledged NixOS (and other base
+images). Meanwhile, if you've got ideas for other spindle backends
+(Kubernetes?!), we'd love to [hear from you](https://chat.tangled.org).
+
+## XRPC APIs
+
+We introduced a complete migration of the knotserver to an
+[XRPC](https://atproto.com/specs/xrpc) API. Alongside this, we also
+decoupled the knot from the appview by getting rid of the registration
+secret, which was centralizing. Knots (and spindles) simply declare
+their owner, and any appview can verify ownership. Once we stabilize the
+[lexicon definitions](lexicons) for these XRPC calls, building clients
+for knots, or alternate implementations should become much simpler.
+
+[lexicons]: https://tangled.sh/@tangled.sh/core/tree/master/lexicons
+
+## issues rework
+
+Issues got a major rework (and facelift) too! They are now threaded:
+top-level comments with replies. This makes Q/A style discussions much
+easier to follow!
+
+
+
+## hosted PDS
+
+A complaint we often recieved was the need for a Bluesky account to use
+Tangled; and besides, we realised that the overlap between Bluesky users
+and possible Tangled users only goes so far -- we aim to be a generic
+code forge after all, AT just happens to be an implementation
+detail.
+
+To address this, we spun up the tngl.sh PDS hosted right here in
+Finland. The only way to get an account on this PDS is by [signing
+up](https://tangled.sh/signup). There's a lot we can do to improve this
+experience as a generic PDS host, but we're still working out details
+around that.
+
+## labels
+
+You can easily categorize issues and pulls via labels! There is plenty
+of customization available:
+
+- labels can be basic, or they can have a key and value set, for example:
+`wontfix` or `priority/high`
+- labels can be constrained to a set of values: `priority: [high medium low]`
+- there can be multiple labels of a given type: `reviewed-by: @oppi.li`,
+`reviewed-by: @anirudh.fi`
+
+The options are endless! You can access them via your repo's settings page.
+
+
+
+
+
+
+ A set of labels applied to an issue.
+
+
+
+
+
+
+ Create custom key-value type labels.
+
+
+
+
+## notifications
+
+In-app notifications now exist! You get notifications for a variety of events now:
+
+* new issues/pulls on your repos (also for collaborators)
+* comments on your issues/pulls (also for collaborators)
+* close/reopen (or merge) of issues/pulls
+* new stars
+* new follows
+
+All of this can be fine-tuned in [/settings/notifications](https://tangled.org/settings/notifications).
+
+
+
+
+## the future
+
+We're working on a *lot* of exciting new things and possibly some big
+announcements to come. Be on the lookout for:
+
+* email notifications
+* preliminary support for issue and PR search
+* total "atprotation" [^1] -- the last two holdouts here are repo and pull records
+* total federation -- i.e. supporting third-party appviews by making it
+ reproducible
+* achieve complete independence from Bluesky PBC by hosting our own relay
+
+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!
+
+[^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.
diff --git a/blog/posts/ci.md b/blog/posts/ci.md
new file mode 100644
--- /dev/null
+++ b/blog/posts/ci.md
@@ -0,0 +1,214 @@
+---
+atroot: true
+template:
+slug: ci
+title: introducing spindle
+subtitle: tangled's new CI runner is now generally available
+date: 2025-08-06
+authors:
+ - name: Anirudh
+ email: anirudh@tangled.sh
+ handle: anirudh.fi
+ - name: Akshay
+ email: akshay@tangled.sh
+ handle: oppi.li
+---
+
+Since launching Tangled, continuous integration has
+consistently topped our feature request list. Today, CI is
+no longer a wishlist item, but a fully-featured reality.
+
+Meet **spindle**: Tangled's new CI runner built atop Nix and
+AT Protocol. In typical Tangled fashion we've been
+dogfooding spindle for a while now; this very blog post
+you're reading was [built and published using
+spindle](https://tangled.sh/@tangled.sh/site/pipelines/452/workflow/deploy.yaml).
+
+Tangled is a new social-enabled Git collaboration platform,
+[read our intro](/intro) for more about the project.
+
+
+
+## how spindle works
+
+Spindle is designed around simplicity and the decentralized
+nature of the AT Protocol. In ingests "pipeline" records and
+emits job status updates.
+
+When you push code or open a pull request, the knot hosting
+your repository emits a pipeline event
+(`sh.tangled.pipeline`). Running as a dedicated service,
+spindle subscribes to these events via websocket connections
+to your knot.
+
+Once triggered, spindle reads your pipeline manifest, spins
+up the necessary execution environment (covered below), and
+runs your defined workflow steps. Throughout execution, it
+streams real-time logs and status updates
+(`sh.tangled.pipeline.status`) back through websockets,
+which the Tangled appview subscribes to for live updates.
+
+Over at the appview, these updates are ingested and stored,
+and logs are streamed live.
+
+## spindle pipelines
+
+The pipeline manifest is defined in YAML, and should be
+relatively familiar to those that have used other CI
+solutions. Here's a minimal example:
+
+```yaml
+# test.yaml
+
+when:
+ - event: ["push", "pull_request"]
+ branch: ["master"]
+
+dependencies:
+ nixpkgs:
+ - go
+
+steps:
+ - name: run all tests
+ environment:
+ CGO_ENABLED: 1
+ command: |
+ go test -v ./...
+```
+
+You can read the [full manifest spec
+here](https://tangled.sh/@tangled.sh/core/blob/master/docs/spindle/pipeline.md),
+but the `dependencies` block is the real interesting bit.
+Dependencies for your workflow, like Go, Node.js, Python
+etc. can be pulled in from nixpkgs.
+[Nixpkgs](https://github.com/nixos/nixpkgs/) -- for the
+uninitiated -- is a vast collection of packages for the Nix
+package manager. Fortunately, you needn't know nor care
+about Nix to use it! Just head to https://search.nixos.org
+to find your package of choice (I'll bet 1€ that it's
+there[^1]), toss it in the list and run your build. The
+Nix-savvy of you lot will be happy to know that you can use
+custom registries too.
+
+[^1]: I mean, if it isn't there, it's nowhere.
+
+Workflow manifests are intentionally simple. We do not want
+to include a "marketplace" of workflows or complex job
+orchestration. The bulk of the work should be offloaded to a
+build system, and CI should be used simply for finishing
+touches. That being said, this is still the first revision
+for CI, there is a lot more on the roadmap!
+
+Let's take a look at how spindle executes workflow steps.
+
+## workflow execution
+
+At present, the spindle "engine" supports just the Docker
+backend[^2]. Podman is known to work with the Docker socket
+feature enabled. Each step is run in a separate container,
+with the `/tangled/workspace` and `/nix` volumes persisted
+across steps.
+
+[^2]: Support for additional backends like Firecracker are
+ planned. Contributions welcome!
+
+The container image is built using
+[Nixery](https://nixery.dev). Nixery is a nifty little tool
+that takes a path-separated set of Nix packages and returns
+an OCI image with each package in a separate layer. Try this
+in your terminal if you've got Docker installed:
+
+```
+docker run nixery.dev/bash/hello-go hello-go
+```
+
+This should output `Hello, world!`. This is running the
+[hello-go](https://search.nixos.org/packages?channel=25.05&show=hello-go)
+package from nixpkgs.
+
+Nixery is super handy since we can construct these images
+for CI environments on the fly, with all dependencies baked
+in, and the best part: caching for commonly used packages is
+free thanks to Docker (pre-existing layers get reused). We
+run a Nixery instance of our own at
+https://nixery.tangled.sh but you may override that if you
+choose to.
+
+## debugging CI
+
+We understand that debugging CI can be the worst. There are
+two parts to this problem:
+
+- CI services often bring their own workflow definition
+ formats and it can sometimes be difficult to know why the
+ workflow won't run or why the workflow definition is
+ incorrect
+- The CI job itself fails, but this has more to do with the
+ build system of choice
+
+To mend the first problem: we are making use of git
+[push-options](https://git-scm.com/docs/git-push#Documentation/git-push.txt--ooption).
+When you push to a repository with an option like so:
+
+```
+git push origin master -o verbose-ci
+```
+
+The server runs a basic set of analysis rules on your
+workflow file, and reports any errors:
+
+```
+λ git push origin main -o verbose-ci
+ .
+ .
+ .
+ .
+remote: error: failed to parse workflow(s):
+remote: - at .tangled/workflows/fmt.yml: yaml: line 14: did not find expected key
+remote:
+remote: warning(s) on pipeline:
+remote: - at build.yml: workflow skipped: did not match trigger push
+```
+
+The analysis performed at the moment is quite basic (expect
+it to get better over time), but it is already quite useful
+to help debug workflows that don't trigger!
+
+## pipeline secrets
+
+Secrets are a bit tricky since atproto has no notion of
+private data. Secrets are instead written directly from the
+appview to the spindle instance using [service
+auth](https://docs.bsky.app/docs/api/com-atproto-server-get-service-auth).
+In essence, the appview makes a signed request using the
+logged-in user's DID key; spindle verifies this signature by
+fetching the public key from the DID document.
+
+
+
+The secrets themselves are stored in a secret manager. By
+default, this is the same sqlite database that spindle uses.
+This is *fine* for self-hosters. The hosted, flagship
+instance at https://spindle.tangled.sh however uses
+[OpenBao](https://openbao.org), an OSS fork of HashiCorp
+Vault.
+
+## get started now
+
+You can run your own spindle instance pretty easily: the
+[spindle self-hosting
+guide](https://tangled.sh/@tangled.sh/core/blob/master/docs/spindle/hosting.md)
+should have you covered. Once done, head to your
+repository's settings tab and set it up! Doesn't work? Feel
+free to pop into [Discord](https://chat.tangled.sh) to get
+help -- we have a nice little crew that's always around to
+help.
+
+All Tangled users have access to our hosted spindle
+instance, free of charge[^3]. You don't have any more
+excuses to not migrate to Tangled now -- [get
+started](https://tangled.sh/login) with your AT Protocol
+account today.
+
+[^3]: We can't promise we won't charge for it at some point
+ but there will always be a free tier.
diff --git a/blog/posts/docs.md b/blog/posts/docs.md
new file mode 100644
--- /dev/null
+++ b/blog/posts/docs.md
@@ -0,0 +1,241 @@
+---
+atroot: true
+template:
+slug: docs
+title: we rolled our own documentation site
+subtitle: you don't need mintlify
+date: 2026-01-12
+authors:
+ - name: Akshay
+ email: akshay@tangled.org
+ handle: oppi.li
+draft: false
+---
+
+We recently organized our documentation and put it up on
+https://docs.tangled.org, using just pandoc. For several
+reasons, using pandoc to roll your own static sites is more
+than sufficient for small projects.
+
+
+
+## requirements
+
+- Lives in [our
+ monorepo](https://tangled.org/tangled.org/core).
+- No JS: a collection of pages containing just text
+ should not require JS to view!
+- Searchability: in practice, documentation engines that
+ come bundled with a search-engine have always been lack
+ lustre. I tend to Ctrl+F or use an actual search engine in
+ most scenarios.
+- Low complexity: building, testing, deploying should be
+ easy.
+- Easy to style
+
+## evaluating the ecosystem
+
+I took the time to evaluate several documentation engine
+solutions:
+
+- [Mintlify](https://www.mintlify.com/): It is quite obvious
+ from their homepage that mintlify is performing an AI
+ pivot for the sake of doing so.
+- [Docusaurus](https://docusaurus.io/): The generated
+ documentation site is quite nice, but the value of pages
+ being served as a full-blown React SPA is questionable.
+- [MkDocs](https://www.mkdocs.org/): Works great with JS
+ disabled, however the table of contents needs to be
+ maintained via `mkdocs.yml`, which can be quite tedious.
+- [MdBook](https://rust-lang.github.io/mdBook/index.html):
+ As above, you need a `SUMMARY.md` file to control the
+ table-of-contents.
+
+MkDocs and MdBook are still on my radar however, in case we
+need a bigger feature set.
+
+## using pandoc
+
+[pandoc](https://pandoc.org/) is a wonderfully customizable
+markup converter. It provides a "chunkedhtml" output format,
+which is perfect for generating documentation sites. Without
+any customization,
+[this](https://pandoc.org/demo/example33/) is the generated
+output, for this [markdown file
+input](https://pandoc.org/demo/MANUAL.txt).
+
+- You get an autogenerated TOC based on the document layout
+- Each section is turned into a page of its own
+
+Massaging pandoc to work for us was quite straightforward:
+
+- I first combined all our individual markdown files into
+ [one big
+ `DOCS.md`](https://tangled.org/tangled.org/core/blob/master/docs/DOCS.md)
+ file.
+- Modified the [default
+ template](https://github.com/jgm/pandoc-templates/blob/master/default.chunkedhtml)
+ to put the TOC on every page, to form a "sidebar", see
+ [`docs/template.html`](https://tangled.org/tangled.org/core/blob/master/docs/template.html)
+- Inserted tailwind `prose` classes where necessary, such
+ that markdown content is rendered the same way between
+ `tangled.org` and `docs.tangled.org`
+
+Generating the docs is done with one pandoc command:
+
+```bash
+pandoc docs/DOCS.md \
+ -o out/ \
+ -t chunkedhtml \
+ --variable toc \
+ --toc-depth=2 \
+ --css=docs/stylesheet.css \
+ --chunk-template="%i.html" \
+ --highlight-style=docs/highlight.theme \
+ --template=docs/template.html
+```
+
+## avoiding javascript
+
+The "sidebar" style table-of-contents needs to be collapsed
+on mobile displays. Most of the engines I evaluated seem to
+require JS to collapse and expand the sidebar, with MkDocs
+being the outlier, it uses a checkbox with the
+[`:checked`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:checked)
+pseudo-class trick to avoid JS.
+
+The other ways to do this are:
+
+- Use ``: this is definitely a
+ "hack", clicking outside the sidebar does not collapse it.
+ Using Ctrl+F or "Find in page" still works through the
+ details tag though.
+- Use the new `popover` API: this seems like the perfect fit
+ for a "sidebar" component.
+
+The bar at the top includes a button to trigger the popover:
+
+```html
+
+```
+
+And a `fixed` position div includes the TOC itself:
+
+```html
+
+
+ Quick Start
+
...
+
...
+
...
+
+
+```
+
+The TOC is scrollable independently and can be collapsed by
+clicking anywhere on the screen outside the sidebar.
+Searching for content in the page via "Find in page" does
+not show any results that are present in the popover
+however. The collapsible TOC is only available on smaller
+viewports, the TOC is not hidden on larger viewports.
+
+## search
+
+There is no native search on the site for now. Taking
+inspiration from [https://htmx.org](https://htmx.org)'s search bar, our search
+bar also simply redirects to Google:
+
+```html
+
+```
+
+I mentioned earlier that Ctrl+F has typically worked better
+for me than, say, the search engine provided by Docusaurus.
+To that end, the same docs have been exported to a ["single
+page" format](https://docs.tangled.org/single-page.html), by
+just removing the `chunkedhtml` related options:
+
+```diff
+ pandoc docs/DOCS.md \
+ -o out/ \
+- -t chunkedhtml \
+ --variable toc \
+ --toc-depth=2 \
+ --css=docs/stylesheet.css \
+- --chunk-template="%i.html" \
+ --highlight-style=docs/highlight.theme \
+ --template=docs/template.html
+```
+
+With all the content on a single page, it is trivial to
+search through the entire site with the browser. If the docs
+do outgrow this, I will consider other options!
+
+## building and deploying
+
+We use [nix](https://nixos.org) and
+[colmena](https://colmena.cli.rs/) to build and deploy all
+Tangled services. A nix derivation to [build the
+documentation](https://tangled.org/tangled.org/core/blob/master/nix/pkgs/docs.nix)
+site is written very easily with the `runCommandLocal`
+helper:
+
+```nix
+runCommandLocal "docs" {} ''
+ .
+ .
+ .
+ ${pandoc}/bin/pandoc ${src}/docs/DOCS.md ...
+ .
+ .
+ .
+''
+```
+
+The NixOS machine is configured to serve the site [via
+nginx](https://tangled.org/tangled.org/infra/blob/master/hosts/nixery/services/nginx.nix#L7):
+
+```nix
+services.nginx = {
+ enable = true;
+ virtualHosts = {
+ "docs.tangled.org" = {
+ root = "${tangled-pkgs.docs}";
+ locations."/" = {
+ tryFiles = "$uri $uri/ =404";
+ index = "index.html";
+ };
+ };
+ };
+};
+```
+
+And deployed using `colmena`:
+
+```bash
+nix run nixpkgs#colmena -- apply
+```
+
+To update the site, I first run:
+
+```bash
+nix flake update tangled
+```
+
+Which bumps the `tangled` flake input, and thus
+`tangled-pkgs.docs`. The above `colmena` invocation applies
+the changes to the machine serving the site.
+
+## notes
+
+Going homegrown has made it a lot easier to style the
+documentation site to match the main site. Unfortunately
+there are still a few discrepancies between pandoc's
+markdown rendering and
+[goldmark's](https://pkg.go.dev/github.com/yuin/goldmark/)
+markdown rendering (which is what we use in Tangled). We may
+yet roll our own SSG,
+[TigerStyle](https://tigerbeetle.com/blog/2025-02-27-why-we-designed-tigerbeetles-docs-from-scratch/)!
diff --git a/blog/posts/intro.md b/blog/posts/intro.md
new file mode 100644
--- /dev/null
+++ b/blog/posts/intro.md
@@ -0,0 +1,64 @@
+---
+atroot: true
+template:
+slug: intro
+title: introducing tangled
+subtitle: a git collaboration platform, built on atproto
+date: 2025-03-02
+authors:
+ - name: Anirudh
+ email: anirudh@tangled.sh
+ handle: anirudh.fi
+---
+
+
+[Tangled](https://tangled.sh) is a new social-enabled Git collaboration
+platform, built on top of the [AT Protocol](https://atproto.com). We
+envision a place where developers have complete ownership of their code,
+open source communities can freely self-govern and most importantly,
+coding can be social and fun again.
+
+There are several models for decentralized code collaboration platforms,
+ranging from ActivityPub's (Forgejo) federated model, to Radicle's
+entirely P2P model. Our approach attempts to be the best of both worlds
+by adopting atproto -- a protocol for building decentralized social
+applications with a central identity.
+
+
+
+Our approach to this is the idea of "knots". Knots are lightweight,
+headless servers that enable users to host Git repositories with ease.
+Knots are designed for either single or multi-tenant use which is
+perfect for self-hosting on a Raspberry Pi at home, or larger
+"community" servers. By default, Tangled provides managed knots where
+you can host your repositories for free.
+
+The [App View][appview] at [tangled.sh](https://tangled.sh) acts as a
+consolidated "view" into the whole network, allowing users to access,
+clone and contribute to repositories hosted across different knots --
+completely seamlessly.
+
+Tangled is still in its infancy, and we're building out several of its
+core features as we [dogfood it ourselves][dogfood]. We developed these
+three tenets to guide our decisions:
+
+1. Ownership of data
+2. Low barrier to entry
+3. No compromise on user-experience
+
+Collaborating on code isn't easy, and the tools and workflows we use
+should feel natural and stay out of the way. Tangled's architecture
+enables common workflows to work as you'd expect, all while remaining
+decentralized.
+
+We believe that atproto has greatly simplfied one of the hardest parts
+of social media: having your friends on it. Today, we're rolling out
+invite-only access to Tangled -- join us on IRC at `#tangled` on
+[libera.chat](https://libera.chat) and we'll get you set up.
+
+**Update**: Tangled is open to public, simply login at
+[tangled.sh/login](https://tangled.sh/login)! Have fun!
+
+[pds]: https://atproto.com/guides/glossary#pds-personal-data-server
+[appview]: https://docs.bsky.app/docs/advanced-guides/federation-architecture#app-views
+[dogfood]: https://tangled.sh/@tangled.sh/core
diff --git a/blog/posts/pulls.md b/blog/posts/pulls.md
new file mode 100644
--- /dev/null
+++ b/blog/posts/pulls.md
@@ -0,0 +1,195 @@
+---
+atroot: true
+template:
+slug: pulls
+title: the lifecycle of a pull request
+subtitle: we shipped a bunch of PR features recently; here's how we built it
+date: 2025-04-16
+image: https://assets.tangled.network/blog/hidden-ref.png
+authors:
+ - name: Anirudh
+ email: anirudh@tangled.sh
+ handle: anirudh.fi
+ - name: Akshay
+ email: akshay@tangled.sh
+ handle: oppi.li
+draft: false
+---
+
+We've spent the last couple of weeks building out a pull
+request system for Tangled, and today we want to lift the
+hood and show you how it works.
+
+If you're new to Tangled, [read our intro](/intro) for the
+full story!
+
+You have three options to contribute to a repository:
+
+- Paste a patch on the web UI
+- Compare two local branches (you'll see this only if you're a
+collaborator on the repo)
+- Compare across forks
+
+Whatever you choose, at the core of every PR is the patch.
+First, you write some code. Then, you run `git diff` to
+produce a patch and make everyone's lives easier, or push to
+a branch, and we generate it ourselves by comparing against
+the target.
+
+## patch generation
+
+When you create a PR from a branch, we create a "patch" by
+calculating the difference between your branch and the
+target branch. Consider this scenario:
+
+
+
+ A is the merge-base for
+feature and main.
+
+
+Your `feature` branch has advanced 2 commits since you first
+branched out, but in the meanwhile, `main` has also advanced
+2 commits. Doing a trivial `git diff feature main` will
+produce a confusing patch:
+
+- the patch will apply the changes from `X` and `Y`
+- the patch will **revert** the changes from `B` and `C`
+
+We obviously do not want the second part! To only show the
+changes added by `feature`, we have to identify the
+"merge-base": the nearest common ancestor of `feature` and
+`main`.
+
+
+In this case, `A` is the nearest common ancestor, and
+subsequently, the patch calculated will contain just `X` and
+`Y`.
+
+### ref comparisons across forks
+
+The plumbing described above is easy to do across two
+branches, but what about forks? And what if they live on
+different servers altogether (as they can in Tangled!)?
+
+Here's the concept: since we already have all the necessary
+components to compare two local refs, why not simply
+"localize" the remote ref?
+
+In simpler terms, we instruct Git to fetch the target branch
+from the original repository and store it in your fork under
+a special name. This approach allows us to compare your
+changes against the most current version of the branch
+you're trying to contribute to, all while remaining within
+your fork.
+
+
+
+ Hidden tracking ref.
+
+
+We call this a "hidden tracking ref." When you create a pull
+request from a fork, we establish a refspec that tracks the
+remote branch, which we then use to generate a diff. A
+refspec is essentially a rule that tells Git how to map
+references between a remote and your local repository during
+fetch or push operations.
+
+For example, if your fork has a feature branch called
+`feature-1`, and you want to make a pull request to the
+`main` branch of the original repository, we fetch the
+remote `main` into a local hidden ref using a refspec like
+this:
+
+```
++refs/heads/main:refs/hidden/feature-1/main
+```
+
+Since we already have a remote (`origin`, by default) to the
+original repository (remember, we cloned it earlier), we can
+use `fetch` with this refspec to bring the remote `main`
+branch into our local hidden ref. Each pull request gets its
+own hidden ref, hence the `refs/hidden/:localRef/:remoteRef`
+format. We keep this ref updated whenever you push new
+commits to your feature branch, ensuring that comparisons --
+and any potential merge conflicts -- are always based on the
+latest state of the target branch.
+
+And just like earlier, we produce the patch by diffing your
+feature branch with the hidden tracking ref. Also, the entire pull
+request is stored as [an atproto record][atproto-record] and updated
+each time the patch changes.
+
+[atproto-record]: https://pdsls.dev/at://did:plc:qfpnj4og54vl56wngdriaxug/sh.tangled.repo.pull/3lmwniim2i722
+
+Neat, now that we have a patch; we can move on the hard
+part: code review.
+
+
+## your patch does the rounds
+
+Tangled uses a "round-based" review format. Your initial
+submission starts "round 0". Once your submission receives
+scrutiny, you can address reviews and resubmit your patch.
+This resubmission starts "round 1". You keep whittling on
+your patch till it is good enough, and eventually merged (or
+closed if you are unlucky).
+
+
+
+ A new pull request with a couple
+rounds of reviews.
+
+
+Rounds are a far superior to standard branch-based
+approaches:
+
+- Submissions are immutable: how many times have your
+ reviews gone out-of-date because the author pushed commits
+ _during_ your review?
+- Reviews are attached to submissions: at a glance, it is
+ easy to tell which comment applies to which "version" of
+ the pull-request
+- The author can choose when to resubmit! They can commit as
+ much as they want to their branch, but a new round begins
+ when they choose to hit "resubmit"
+- It is possible to "interdiff" and observe changes made
+ across submissions (this is coming very soon to Tangled!)
+
+This [post by Mitchell
+Hashimoto](https://mitchellh.com/writing/github-changesets)
+goes into further detail on what can be achieved with
+round-based reviews.
+
+## future plans
+
+To close off this post, we wanted to share some of our
+future plans for pull requests:
+
+* `format-patch` support: both for pasting in the UI and
+ internally. This allows us to show commits in the PR page,
+ and offer different merge strategies to choose from
+ (squash, rebase, ...).
+ **Update 2025-08-12**: We have format-patch support!
+
+* Gerrit-style `refs/for/main`: we're still hashing out the
+ details but being able to push commits to a ref to
+ "auto-create" a PR would be super handy!
+
+* Change ID support: This will allow us to group changes
+ together and track them across multiple commits, and to
+ provide "history" for each change. This works great with [Jujutsu][jj].
+ **Update 2025-08-12**: This has now landed: https://blog.tangled.org/stacking
+
+Join us on [Discord](https://chat.tangled.sh) or
+`#tangled` on libera.chat (the two are bridged, so we will
+never miss a message!). We are always available to help
+setup knots, listen to feedback on features, or even
+shepherd contributions!
+
+**Update 2025-08-12**: We move fast, and we now have jujutsu support, and an
+early in-house CI: https://blog.tangled.org/ci. You no longer need a Bluesky
+account to sign-up; head to https://tangled.sh/signup and sign up with your
+email!
+
+[jj]: https://jj-vcs.github.io/jj/latest/
diff --git a/blog/posts/seed.md b/blog/posts/seed.md
new file mode 100644
--- /dev/null
+++ b/blog/posts/seed.md
@@ -0,0 +1,74 @@
+---
+atroot: true
+template:
+slug: seed
+title: announcing our €3,8M seed round
+subtitle: and more on what's next
+date: 2026-03-02
+image: https://assets.tangled.network/blog/seed.png
+authors:
+ - name: Anirudh
+ email: anirudh@tangled.org
+ handle: anirudh.fi
+---
+
+
+
+Today, we're announcing our €3,8M ($4.5M) financing round led by
+[byFounders](https://byfounders.vc), with participation from [Bain
+Capital Crypto](https://baincapitalcrypto.com/),
+[Antler](https://antler.co), Thomas Dohmke (former GitHub CEO), Avery
+Pennarun (CEO of Tailscale), among other incredible angels.
+
+For the past year, we've been building Tangled from the ground up --
+starting from first principles and asking ourselves what code
+collaboration should really look like. We made deliberate,
+[future-facing technology](https://anirudh.fi/future) choices. We chose
+to build on top of the AT Protocol as it helped us realize a federated,
+open network where users can own their code and social data. We shipped
+stacked PRs to enable more efficient contribution and review workflows.
+What started off as a side project, grew to over 7k+ users, who've
+created over 5k+ repositories.
+
+Our vision for Tangled has always been big: we want to build the best
+code forge ever, and become foundational infrastructure for the next
+generation of open source. Whatever that looks like: hundreds of devs
+building artisanal libraries, or one dev and a hundred agents building a
+micro-SaaS.
+
+And finding the right investors to help us acheive this vision wasn't
+something we took lightly. We spent months getting to know potential
+partners -- among which, byFounders stood out immediately. Like us,
+they're community-driven at their core, and their commitment to
+transparency runs deep -- you can see the very term sheet we signed on
+their website! With these shared fundamental values, we knew byFounders
+were the right people to have in our corner and we're incredibly excited
+to work with them.
+
+## what's next
+
+We're heads down building. For 2026, expect to see:
+
+* a fully revamped CI (spindle v2!) built on micro VMs to allow for
+ faster builds and more choice of build environments. Oh, and a proper
+ Nix CI -- we know you want it.
+* protocol-level improvements across the board that'll unlock nifty
+ things like repo migrations across knots, organizations, and more!
+* a customizable "mission control" dashboard for your active PRs,
+ issues, and anything else you might want to track.
+* a migration tool to help you move off GitHub
+* all things search: code search, repo search, etc.
+* platform and infrastructure performance improvements & more global
+presence
+* Tangled CLI!
+
+If all this sounds exciting to you: we're growing our team! Shoot us
+[an email](mailto:team@tangled.org) telling us a bit about yourself and
+any past work that might be relevant, and what part of the roadmap
+interests you most. We can hire from (almost) anywhere.
+
+New to Tangled? [Get started here](https://docs.tangled.org/). Oh, and
+come hang on [Discord](https://chat.tangled.org)!
+
+A sincere thank you to everyone that helped us get here -- we're giddy
+about what's to come.
diff --git a/blog/posts/stacking.md b/blog/posts/stacking.md
new file mode 100644
--- /dev/null
+++ b/blog/posts/stacking.md
@@ -0,0 +1,351 @@
+---
+atroot: true
+template:
+slug: stacking
+title: jujutsu on tangled
+subtitle: tangled now supports jujutsu change-ids!
+date: 2025-06-02
+image: https://assets.tangled.network/blog/interdiff_difference.jpeg
+authors:
+ - name: Akshay
+ email: akshay@tangled.sh
+ handle: oppi.li
+draft: false
+---
+
+Jujutsu is built around structuring your work into
+meaningful commits. Naturally, during code-review, you'd
+expect reviewers to be able to comment on individual
+commits, and also see the evolution of a commit over time,
+as reviews are addressed. We set out to natively support
+this model of code-review on Tangled.
+
+Tangled is a new social-enabled Git collaboration platform,
+[read our intro](/intro) for more about the project.
+
+For starters, I would like to contrast the two schools of
+code-review, the "diff-soup" model and the interdiff model.
+
+## the diff-soup model
+
+When you create a PR on traditional code forges (GitHub
+specifically), the UX implicitly encourages you to address
+your code review by *adding commits* on top of your original
+set of changes:
+
+- GitHub's "Apply Suggestion" button directly commits the
+ suggestion into your PR
+- GitHub only shows you the diff of all files at once by
+ default
+- It is difficult to know what changed across force pushes
+
+Consider a hypothetical PR that adds 3 commits:
+
+```
+[c] implement new feature across the board (HEAD)
+ |
+[b] introduce new feature
+ |
+[a] some small refactor
+```
+
+And when only newly added commits are easy to review, this
+is what ends up happening:
+
+```
+[f] formatting & linting (HEAD)
+ |
+[e] update name of new feature
+ |
+[d] fix bug in refactor
+ |
+[c] implement new feature across the board
+ |
+[b] introduce new feature
+ |
+[a] some small refactor
+```
+
+It is impossible to tell what addresses what at a glance,
+there is an implicit relation between each change:
+
+```
+[f] formatting & linting
+ |
+[e] update name of new feature -------------.
+ | |
+[d] fix bug in refactor -----------. |
+ | | |
+[c] implement new feature across the board |
+ | | |
+[b] introduce new feature <-----------------'
+ | |
+[a] some small refactor <----------'
+```
+
+This has the downside of clobbering the output of `git
+blame` (if there is a bug in the new feature, you will first
+land on `e`, and upon digging further, you will land on
+`b`). This becomes incredibly tricky to navigate if reviews
+go on through multiple cycles.
+
+
+## the interdiff model
+
+With jujutsu however, you have the tools at hand to
+fearlessly edit, split, squash and rework old commits (you
+can absolutely achieve this with git and interactive
+rebasing, but it is certainly not trivial).
+
+Let's try that again:
+
+```
+[c] implement new feature across the board (HEAD)
+ |
+[b] introduce new feature
+ |
+[a] some small refactor
+```
+
+To fix the bug in the refactor:
+
+```
+$ jj edit a
+Working copy (@) now at: [a] some small refactor
+
+$ # hack hack hack
+
+$ jj log -r a::
+Rebased 2 descendant commits onto updated working copy
+[c] implement new feature across the board (HEAD)
+ |
+[b] introduce new feature
+ |
+[a] some small refactor
+```
+
+Jujutsu automatically rebases the descendants without having
+to lift a finger. Brilliant! You can repeat the same
+exercise for all review comments, and effectively, your
+PR will have evolved like so:
+
+```
+ a -> b -> c initial attempt
+ | | |
+ v v v
+ a' -> b' -> c' after first cycle of reviews
+```
+
+## the catch
+
+If you use `git rebase`, you will know that it modifies
+history and therefore changes the commit SHA. How then,
+should one tell the difference between the "old" and "new"
+state of affairs?
+
+Tools like `git-range-diff` make use of a variety of
+text-based heuristics to roughly match `a` to `a'` and `b`
+to `b'` etc.
+
+Jujutsu however, works around this by assigning stable
+"change id"s to each change (which internally point to a git
+commit, if you use the git backing). If you edit a commit,
+its SHA changes, but its change-id remains the same.
+
+And this is the essence of our new stacked PRs feature!
+
+## interdiff code review on tangled
+
+To really explain how this works, let's start with a [new
+codebase](https://tangled.sh/@oppi.li/stacking-demo/):
+
+```
+$ jj git init --colocate
+
+# -- initialize codebase --
+
+$ jj log
+@ n set: introduce Set type main HEAD 1h
+```
+
+I have kicked things off by creating a new go module that
+adds a `HashSet` data structure. My first changeset
+introduces some basic set operations:
+
+```
+$ jj log
+@ so set: introduce set difference HEAD
+├ sq set: introduce set intersection
+├ mk set: introduce set union
+├ my set: introduce basic set operations
+~
+
+$ jj git push -c @
+Changes to push to origin:
+ Add bookmark push-soqmukrvport to fc06362295bd
+```
+
+When submitting a pull request, select "Submit as stacked PRs":
+
+
+
+
+
+ Submitting Stacked PRs
+
+
+This submits each change as an individual pull request:
+
+
+
+
+
+ The "stack" is similar to Gerrit's relation chain
+
+
+After a while, I receive a couple of review comments, not on
+my entire submission, but rather, on each *individual
+change*. Additionally, the reviewer is happy with my first
+change, and has gone ahead and merged that:
+
+
+
+
+
+
+ The first change has been merged
+
+
+
+
+
+
+ A review on the set union implementation
+
+
+
+
+
+
+ A review on the set difference implementation
+
+
+
+Let us address the first review:
+
+> can you use the new `maps.Copy` api here?
+
+```
+$ jj log
+@ so set: introduce set difference push-soqmukrvport
+├ sq set: introduce set intersection
+├ mk set: introduce set union
+├ my set: introduce basic set operations
+~
+
+# let's edit the implementation of `Union`
+$ jj edit mk
+
+# hack, hack, hack
+
+$ jj log
+Rebased 2 descendant commits onto updated working copy
+├ so set: introduce set difference push-soqmukrvport*
+├ sq set: introduce set intersection
+@ mk set: introduce set union
+├ my set: introduce basic set operations
+~
+```
+
+Next, let us address the bug:
+
+> there is a logic bug here, the condition should be negated.
+
+```
+# let's edit the implementation of `Difference`
+$ jj edit so
+
+# hack, hack, hack
+```
+
+We are done addressing reviews:
+```
+$ jj git push
+Changes to push to origin:
+ Move sideways bookmark push-soqmukrvport from fc06362295bd to dfe2750f6d40
+```
+
+Upon resubmitting the PR for review, Tangled is able to
+accurately trace the commit across rewrites, using jujutsu
+change-ids, and map it to the corresponding PR:
+
+
+
+
+
+
+ PR #2 advances to the next round
+
+
+
+
+
+
+ PR #4 advances to the next round
+
+
+
+Of note here are a few things:
+
+- The initial submission is still visible under `round #0`
+- By resubmitting, the round has simply advanced to `round
+ #1`
+- There is a helpful "interdiff" button to look at the
+ difference between the two submissions
+
+The individual diffs are still available, but most
+importantly, the reviewer can view the *evolution* of a
+change by hitting the interdiff button:
+
+
+
+
+
+
+
+ Interdiff between round #1 and #0
+
+
+Indeed, the logic bug has been addressed!
+
+## start stacking today
+
+If you are a jujutsu user, you can enable this flag on more
+recent versions of jujutsu:
+
+```
+λ jj --version
+jj 0.29.0-8c7ca30074767257d75e3842581b61e764d022cf
+
+# -- in your config.toml file --
+[git]
+write-change-id-header = true
+```
+
+This feature writes `change-id` headers directly into the
+git commit object, and is visible to code forges upon push,
+and allows you to stack your PRs on Tangled.
diff --git a/blog/templates/index.html b/blog/templates/index.html
new file mode 100644
--- /dev/null
+++ b/blog/templates/index.html
@@ -0,0 +1,76 @@
+{{ define "title" }}the tangled blog{{ end }}
+
+{{ define "extrameta" }}
+
+
+{{ end }}
+
+
+{{ define "bodyClasses" }}!bg-white dark:!bg-gray-900{{ end }}
+
+{{ define "topbarLayout" }}
+
+ {{ template "layouts/fragments/topbar" . }}
+
+{{ end }}
+
+{{ define "content" }}
+