view

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 20, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package view serves the graph viewer from the binary, on the loopback interface, for as long as the command runs.

The whole of the design is that this adds no artifact and no state. There is no cache directory, nothing written to the repository, and no second copy of the viewer: the assets are the ones site/ publishes (see site.Files), and the graph is the same JSON `signpost graph export -format json` writes, held in memory and discarded when the process exits. A `view` that wrote a graph.json would create exactly the stale second artifact ADR 0008 declined to commit.

**The graph is a snapshot, taken before the listener opens.** Nothing re-analyses on a request, and nothing watches the tree. That is a decision rather than a simplification: a viewer that quietly re-read the repository would change what it shows while somebody was reading it, and a viewer that re-analysed per request would spend seconds of CPU on a page reload. The page says which commit it describes and what was already out of step when it started, so a reader can tell how old the picture is. Restarting is the refresh.

Why loopback, and why the Host header is checked

The document interpolates strings that came out of a repository — module names, file paths — and lists every file in every module. That is a private repository's structure, so it is served to this machine and only this machine: the listener binds 127.0.0.1, never 0.0.0.0, and there is no flag to change that.

Binding loopback is not sufficient on its own. A page the user is browsing can issue requests to 127.0.0.1, and the same-origin policy stops it *reading* the responses only because no CORS header is ever set here. DNS rebinding is the case that defeats that: an attacker's hostname re-resolves to 127.0.0.1, so the browser treats the response as same-origin with the attacker's page. checkHost is the mitigation — a request whose Host is not a loopback name is refused before any repository content reaches the response.

Index

Constants

View Source
const DefaultPort = 7777

DefaultPort is what `signpost view` listens on when nothing says otherwise.

Fixed rather than ephemeral, so the URL is the same on every run and can be left open in a tab across restarts. High and unfashionable on purpose: 3000, 5000, 5173, 8000, and 8080 are all in use on a working developer's machine, and a default that usually collides is a default that always prints a different URL.

Variables

This section is empty.

Functions

func Serve

func Serve(ctx context.Context, o Options, out io.Writer, open bool) error

Serve analyses nothing, listens on loopback, and blocks until ctx is done.

The URL is printed before the browser is opened and before anything is served, so a machine with no browser — a container, a remote shell — is still told where to point one. That ordering is the difference between a command that works headless and one that appears to hang.

func WriteStatic

func WriteStatic(dir string, o Options) ([]string, error)

WriteStatic writes the viewer to dir as ordinary files, and returns what it wrote.

This is the half of the viewer a deploy needs. `Serve` holds the same bytes in memory behind a listener, which is right for one person looking at one repository and useless to GitHub Pages — Pages uploads a directory. Until this existed, a scaffolded Pages workflow had nothing to upload: the assets live in this binary (see site.Files), and the only command that could reach them bound a port and blocked.

**The files come from assets(), the same map handler() routes.** That is the point of the function rather than an implementation note. A separate writer listing the four files itself would be a second definition of what the viewer consists of, and the way that fails is a fifth asset added to the server and not to the export — a published page that 404s one request and renders a frame with a control missing. There is one list, and both callers read it.

The address recorded in the page is the empty string here, because there isn't one: a static export is served from wherever it is uploaded to, and the document says so instead of naming a host it cannot know. See view.html, where a missing Address is what distinguishes the two.

Nothing is removed. A stale file from a previous export is left where it is, which is deliberate: this writes into a directory the caller named, and a function that deletes what it does not recognise is one that eats a CNAME the first time somebody points it at a directory holding one. The Pages artifact is assembled by the workflow, and it is the workflow's business what else belongs in it.

Types

type Options

type Options struct {
	// Root is the repository path, as the analysis resolved it. Shown so a reader can
	// tell which checkout a tab is pointing at.
	Root string
	// Title heads the page: the repository's name if one is known, or the directory's.
	Title string
	// Commit is the short sha the analysis describes, or empty where there is no
	// readable history. Empty is printed as nothing rather than as "unknown", because
	// the Notes already say history was not read.
	Commit string
	// RepoBase is the URL prefix a file path is appended to, with a trailing slash, or
	// empty when nothing known can be built. Empty means graph.js renders a filename as
	// text instead of as a link — see repoBase.
	RepoBase string
	// Graph is the JSON document graph.js fetches, exactly as `graph export -format
	// json` writes it.
	Graph []byte
	// Nodes and Edges are what the page states in prose. Passed rather than counted
	// from Graph so this package does not parse what it only serves.
	Nodes, Edges int
	// Notes are what was already out of step before the server started — a bundle
	// behind the code, uncommitted edits the commit stamp does not cover. Printed
	// verbatim, so each entry is a whole sentence.
	Notes []string
	// Port is the TCP port on 127.0.0.1. Zero asks the kernel for a free one.
	Port int
	// PortWasAsked separates "-port 7777" from an unpassed flag that defaults to the
	// same number, and the two must not behave alike: a port somebody named is one they
	// want, most likely because something else is configured to reach it, so a
	// collision is an error. An unnamed one is a convenience, and a collision falls back
	// to whatever is free.
	//
	// A bool beside the port rather than a sentinel value, because every port is a
	// legitimate value and there is none left to mean "unset". Comparing Port against
	// DefaultPort was the first way this was written and it is wrong for exactly the
	// case that motivated the flag: `-port 7777` with 7777 taken fell back silently,
	// having been mistaken for the default. See applyConfig for the same distinction
	// drawn the same way.
	PortWasAsked bool
}

Options is everything the page shows. The caller has already analysed the repository; this package neither walks a tree nor runs git.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL