Documentation
¶
Overview ¶
Package gitdiff — deeplink.go centralises the prereview URL-hash grammar so every consumer (the SetURLHash action, the markdown + HTML link rewriters, the line-gutter permalink template) shares one parser and one stringifier. Extending the grammar means changing these two functions, not hunting through the codebase.
Grammar:
hash := path [":" target] path := <URL-encoded segments joined by "/"> target := lineRange | "h-" anchorID lineRange := "L" <n> ["-L" <m>] anchorID := <opaque, URL-encoded>
Examples:
foo.go — open foo.go subdir/foo.go:L42 — open subdir/foo.go, select line 42 subdir/foo.go:L42-L48 — open subdir/foo.go, select lines 42–48 README.md:h-architecture — open README.md, scroll to heading "architecture"
Package gitdiff shells out to the git CLI to enumerate changed files and load per-file unified diffs for prereview. Shelling out (vs. a Go git library) is deliberate — it respects the user's local git config (rename detection thresholds, autocrlf, etc.) so what prereview shows matches what `git diff` shows in their terminal.
Index ¶
- Variables
- func ChangedCount(entries []FileEntry) int
- func CurrentBranch(repo string) string
- func ExtractHTMLAnchorIDs(blocks []HTMLBlock) map[string]int
- func FormatHash(path string, fromLine, toLine int, anchor string) string
- func HighlightLine(filename, content string) template.HTML
- func HighlightLines(filename string, contents []string) []template.HTML
- func IsHTMLPath(path string) bool
- func IsMarkdownPath(path string) bool
- func IsValidRef(repo, ref string) bool
- func ListBranches(repo string) []string
- func ListRemoteBranches(repo string) []string
- func ResolveRelativeLink(currentFile, target string) (string, bool)
- type DiffLine
- type FileDiff
- type FileEntry
- type HTMLBlock
- type Heading
- type MarkdownBlock
- type ParsedHash
Constants ¶
This section is empty.
Variables ¶
var HighlightCSS = func() string { var buf bytes.Buffer if err := chromaFormatter.WriteCSS(&buf, chromaStyle); err != nil { return "" } return buf.String() }()
HighlightCSS returns the chroma stylesheet for the active style. Computed once at package-init time; main.go serves it as /syntax.css.
Functions ¶
func ChangedCount ¶
ChangedCount returns how many entries have a non-empty Status — i.e. how many of these files differ from the active base. Useful for the drawer header ("N files · M changed").
func CurrentBranch ¶
CurrentBranch returns the short name of the checked-out branch (e.g. "main", "feature/foo"). Returns "HEAD" in a detached state (which is what `git rev-parse --abbrev-ref HEAD` emits there). Errors return an empty string — callers should display "current branch" or similar generic copy when the result is blank.
func ExtractHTMLAnchorIDs ¶ added in v0.3.6
ExtractHTMLAnchorIDs returns a map from each `id` attribute found inside the supplied HTMLBlocks to the block index that contains it. Used by the deep-link scroll target (`#path:h-id` → scroll the matching block into view). Block-level granularity — the id element itself lives in a shadow root and needs a second client primitive to scroll to (deferred). Empty input → nil.
Within a block we walk only the rendered HTML (no head preamble, no script content — those are already stripped by RenderHTMLBlocks). Duplicate ids are kept at the FIRST occurrence (HTML5 says ids SHOULD be unique; if they're not, the deep link is ambiguous and going to the first instance is the standard browser behaviour).
func FormatHash ¶ added in v0.3.6
FormatHash serialises path + line range + anchor back into a URL hash (without leading "#"). Empty path → "" (caller decides whether to render an empty data-attr). Line range takes precedence over anchor when both are present — the line selection is the more specific target.
func HighlightLine ¶
HighlightLine is retained for callers that want to highlight a single line in isolation (currently none in the project — kept for backward compatibility and potential ad-hoc use). For bulk highlighting use HighlightLines, which amortises tokenizer startup across all lines.
func HighlightLines ¶
HighlightLines tokenises every line of `contents` in a single pass and returns the syntax-highlighted HTML per line. Falls back to per-line HTML escape on tokenizer errors. Empty input returns nil.
Joining with '\n' before tokenising lets chroma understand line transitions naturally — string tokens that span lines keep their type, etc. The output is then split back into per-line groups by walking the token stream and splitting tokens on their '\n' bytes.
func IsHTMLPath ¶ added in v0.3.3
IsHTMLPath reports whether path is an HTML file by extension. The viewer uses this to flip the preview area into a sandboxed iframe instead of the syntax-highlighted line view.
func IsMarkdownPath ¶
IsMarkdownPath reports whether path is a Markdown file by extension.
func IsValidRef ¶
IsValidRef reports whether `ref` resolves to a commit in this repo. Used by the runtime base picker to validate user-typed refs before swapping state.Base. Treats both branch names and commit-ish refs (HEAD~3, abc1234) uniformly via `git rev-parse --verify`.
func ListBranches ¶
ListBranches returns the short names of every local branch in the repo, sorted alphabetically. Used to populate the base-picker dropdown — `git for-each-ref` runs in microseconds even on big repos, so calling it from every Mount is fine.
func ListRemoteBranches ¶
ListRemoteBranches returns the short names of every remote-tracking branch (e.g. "origin/main"), sorted. The symbolic "<remote>/HEAD" entries are skipped — they're aliases, not useful review bases. Lets the base picker offer "review since I branched off origin/main" without a freeform input.
func ResolveRelativeLink ¶ added in v0.3.6
ResolveRelativeLink rewrites a link target from inside currentFile to a prereview hash URL when it points at a relative path inside the repo, or returns the original target unchanged when it points elsewhere (http://, https://, mailto:, tel:, data:, // protocol- relative, or absolute path starting with `/`).
Returns the rewritten target and isExternal=false for in-repo relative paths (caller should write the new hash). Returns the original target and isExternal=true for everything else (caller should pass through unchanged AND, for HTML, add target="_blank" rel="noopener" for safety).
Intra-doc anchors (`#foo`) resolve to `currentFile:h-foo`. Pure relative paths (`OTHER.md`, `./sibling.md`, `../parent/file.md`) resolve against currentFile's directory. A path with its own `#anchor` suffix gets the anchor as the h- target. A path with `?query` is treated as external (we don't have a query-string channel into the hash grammar).
Types ¶
type DiffLine ¶
type DiffLine struct {
OldNum int // 0 when the line doesn't exist on the old side
NewNum int // 0 when the line doesn't exist on the new side
Kind string // "add" | "del" | "ctx"
Content string // raw line text, no leading +/-/space, no trailing \n
// HighlightedContent is Content rendered as syntax-highlighted HTML
// spans via chroma. Templates render this instead of Content; the
// raw Content stays around for CSV exports and other consumers that
// don't want HTML markup.
HighlightedContent template.HTML
}
DiffLine is one rendered line in the viewer. Exactly one of OldNum / NewNum may be zero (for additions and deletions respectively); for context lines (Kind == "ctx") both are populated.
func CollapseToHunks ¶
CollapseToHunks turns a full-file line list (every line tagged add/del/ctx, as LoadDiff produces with -U999999) into a true diff view: only changed lines plus `ctx` unchanged lines on each side of a change. Each maximal run of dropped unchanged lines is replaced by a single synthetic fold line:
Kind == "fold", Content == "<N>" (N = number of skipped lines)
The fold line's OldNum/NewNum are set to the first skipped line's numbers so the template can derive a stable, collision-free data-key (gaps never overlap).
If the input contains no add/del line (an unchanged file — shown via the all-files scope toggle or the clean-tree fallback) there is no diff to collapse, so the input is returned unchanged. Callers treat that as "show the whole file".
type FileDiff ¶
type FileDiff struct {
Path string
Lines []DiffLine
IsBinary bool // true when git reports "Binary files differ" — Lines is nil
Note string // e.g. "file added", "file deleted"; informational, may be empty
// MaxLineChars is the longest Content length (in characters) across
// all DiffLines. Used by the template to set `.code` width in `ch`
// units up front; without this hint, the browser would compute
// `width: max-content` for every line-row to determine the diff
// container's scrollable extent — expensive on big files (950+
// rows) and the dominant cost in perceived vertical-scroll lag.
MaxLineChars int
// MarkdownBlocks is the rendered-Markdown view of the current
// (new-side) file: top-level blocks tagged with their source line
// range. Populated only for .md/.markdown paths; nil otherwise.
// Computed once per load and cached with the FileDiff.
MarkdownBlocks []MarkdownBlock
// HTMLBlocks is the rendered-HTML equivalent for .html/.htm paths:
// each top-level <body> child tagged with its source line span,
// embedded with the document's head stylesheets so per-block
// Declarative Shadow DOM hydration in the client renders with the
// user's CSS isolated from the SPA. Nil for non-HTML paths.
HTMLBlocks []HTMLBlock
// Headings is the TOC source for the rendered-Markdown view:
// every Markdown heading in the new-side file, in document order,
// with goldmark's slugified anchor `id`. Populated only for
// .md/.markdown paths; nil otherwise. Filtering by level (e.g.
// h1–h3) and the "≥ 2 entries" visibility gate live at the caller
// (state.RenderedHeadings()).
Headings []Heading
// HTMLAnchors maps every `id=...` attribute in the HTML preview
// source to the index of the HTMLBlock that contains it. Used by
// the deep-link scroll target (`#path:h-id` → scroll the matching
// block into view). Populated only for HTML paths; nil otherwise.
// Block-level granularity — finer scroll (the id element itself
// inside the shadow root) requires a second client primitive and
// is deferred.
HTMLAnchors map[string]int
}
FileDiff is the full-file, line-by-line diff for one path.
func LoadDiff ¶
LoadDiff returns the full-file diff for one path against base.
Uses `git diff --no-color -U999999` so every line of the file appears (additions, deletions, context). For files that are pure additions (A) or pure deletions (D) git produces a diff against /dev/null, which the parser handles naturally — every line is "add" or "del" respectively.
Files larger than maxRenderableFileBytes short-circuit with a Note instead of loading content — the viewer renders the note and skips the per-line markup entirely.
func LoadDiffNoGit ¶ added in v0.1.0
LoadDiffNoGit returns the full-file view for one path when there is no git base to diff against — the sibling of LoadDiff. Every line is an "add" (the file is wholly "new" with no base), produced by the same loadUntrackedAsAdded reader git mode already uses for untracked files, then run through the shared highlightLines hook so syntax highlighting and rendered-Markdown blocks populate identically to the git path. Files over the render cap short-circuit with a Note, exactly like LoadDiff, so a huge file never gets read into the page.
type FileEntry ¶
type FileEntry struct {
Path string // post-rename / current path
Status string // "A","M","D","R","C","T","U"
Renamed string // pre-rename path for R/C; empty otherwise
Added int // lines added vs base; -1 means "binary / unknown"
Deleted int // lines deleted vs base; -1 means "binary / unknown"
CommentCount int // populated by the controller, not by gitdiff
}
FileEntry is one row in the changed-files list.
func ListFiles ¶
ListFiles enumerates every file currently in the working tree (tracked + untracked-non-ignored), annotated with diff information vs base where applicable.
This is the foundation of "show all files; diff is an overlay": prereview now always lets the reviewer browse the whole repo, with diff metadata layered on top of the files that happen to differ from base. Files unchanged vs base appear in the list with Status="", Added=0, Deleted=0 — the template renders them without a diff badge.
Deleted files (present in base, absent in the working tree) are intentionally omitted — they don't exist in the repo "right now". Reviewers who care about deletions can flip the base or read git history directly.
base may be "HEAD", a branch name, or any revision spec git accepts. If base is empty, diff annotation is skipped and every entry is bare.
func ListFilesNoGit ¶ added in v0.1.0
ListFilesNoGit enumerates the reviewable files when the target is NOT a git repo — the sibling of ListFiles for loose files / non-git directories.
- single != "" → exactly one entry (single-file review). single is the basename relative to repo (repo is its parent directory).
- single == "" → walk repo recursively. Skips the .git/ and .prereview/ control dirs, dotfiles and dotdirs (editor/state cruft a non-git tree has no .gitignore to hide), and files over the render cap (reviewing megabytes in a browser is pointless and would only render a "too large" placeholder anyway).
Every entry is Status "A": with no base there is no diff, so the whole file is "new" — the same convention ListFiles uses for untracked files and the empty-tree (clean-tree whole-branch) review. Added carries the line count so the drawer still shows a "+N" badge; binary files report 0 (countLines can't read them) and render as a binary placeholder when opened. Entries are sorted by path so the drawer is stable.
type HTMLBlock ¶ added in v0.3.3
HTMLBlock is one independently-commentable rendered unit of an HTML file plus the 1-based, inclusive SOURCE line range it came from. The line range is what makes HTML commenting line-accurate: a comment placed on a block anchors to these real source lines so it round-trips with the raw view and the CSV — same contract as MarkdownBlock.
HTML carries the block element wrapped in the document's <head> stylesheets (each block's shadow root needs its own copy for visual fidelity inside the isolation boundary). The client hydrates these into Declarative Shadow DOM shadow roots, so the user's CSS applies only inside the block — no leakage to the SPA chrome.
func RenderHTMLBlocks ¶ added in v0.3.3
RenderHTMLBlocks parses src and returns each top-level <body> child as a block tagged with its source line span. Each block's HTML embeds the head's stylesheets (link/style) so the per-block shadow root has access to the user's CSS. Stripped of <script> elements, on* event- handler attributes, and javascript: URLs — the inline DOM renders in the SPA origin (within a shadow root) which would let those mutate livetemplate state or fire on element load.
currentPath drives the relative-link rewriter: `<a href="other.html">` and `<a href="#section">` inside the preview are rewritten to deep- link hashes so a click stays in the SPA; external `href`s pass through. Empty currentPath disables rewriting.
Empty input → nil. Input without a <body> → nil (no commentable surface).
type Heading ¶ added in v0.2.0
Heading captures one rendered Markdown heading for the TOC sidebar. Level is 1–6 (matches the source HN); ID is the slugified anchor goldmark's WithAutoHeadingID emits (stable across renders, collisions disambiguated by `-1`/`-2` suffix); Text is the plain-text inline content; Line is the 1-based source line where the heading starts — used to map a clicked TOC entry to the MarkdownBlock containing it so a server-side scroll directive can target the right block. Empty source → nil.
func ExtractHeadings ¶ added in v0.2.0
ExtractHeadings walks src as Markdown and returns each heading in document order, with the `id` attribute goldmark's WithAutoHeadingID transformer attached during parse. Callers filter by Level for TOC depth (we typically render h1–h3 only). Empty / heading-less input → nil.
type MarkdownBlock ¶
MarkdownBlock is one independently-commentable rendered unit plus the 1-based, inclusive SOURCE line range it came from. The line range is what makes rendered-Markdown commenting line-accurate: a comment placed on a unit anchors to these real source lines, so it round-trips with the raw line view and the CSV — nothing is renumbered.
Single-line nodes (heading, code fence, blockquote, …) are one block. Containers are descended ONE level so review comments can target a single line: a list yields one block per list item, a table one block for the header row plus one per body row. A paragraph that spans multiple SOURCE lines is split into one block per source line ONLY when it is authored one-sentence-per-line (every line except possibly the last ends a sentence) — CommonMark soft-wraps such a paragraph into one <p>, and splitting restores per-line commenting. A hard-wrapped paragraph (lines break mid-sentence) instead renders as a single reflowed CommonMark paragraph, so a sentence is never broken across visual lines; it stays commentable at paragraph granularity (its wrap points are arbitrary, so per-line comments there would be meaningless). An inline span that crosses a soft line-break degrades to literal text on that one line; it never produces invalid HTML.
func RenderMarkdownBlocks ¶
func RenderMarkdownBlocks(src []byte, currentPath string) []MarkdownBlock
RenderMarkdownBlocks parses src and returns each commentable unit as safe HTML tagged with its source line span. currentPath drives the relative-link rewriter: in-repo links like `[other](OTHER.md)` and `[section](#anchor)` are rewritten to deep-link hashes so a click stays in the SPA; external links pass through unchanged. Empty currentPath disables rewriting (used by tests and any caller that doesn't care about deep links). Empty input → nil.
type ParsedHash ¶ added in v0.3.6
ParsedHash is the structured form of a prereview URL hash. Empty fields mean "not in this hash". Path is the only required field for the hash to be a meaningful deep link — a hash without a path (e.g. "L42" or ":h-foo") is rejected (Path == "").
func ParseHash ¶ added in v0.3.6
func ParseHash(hash string) ParsedHash
ParseHash splits a URL hash (without leading "#") into its components. Tolerant: a hash without a target part is fine (just the path); a hash that doesn't parse as a deep link returns ParsedHash{Path: hash} on the assumption that the leading segment is still a file path, OR an empty struct if the hash isn't even path-shaped (no extension, no slash, no colon — likely a dialog/ popover/details id from the existing setupHashLink machinery).
The server treats any unresolvable Path as "no such file" and no-ops, so a permissive parse is safe — the worst case is a no-op dispatch.