gitdiff

package
v0.3.5 Latest Latest
Warning

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

Go to latest
Published: May 30, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

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

Constants

This section is empty.

Variables

View Source
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

func ChangedCount(entries []FileEntry) int

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

func CurrentBranch(repo string) string

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 HighlightLine

func HighlightLine(filename, content string) template.HTML

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

func HighlightLines(filename string, contents []string) []template.HTML

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

func IsHTMLPath(path string) bool

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

func IsMarkdownPath(path string) bool

IsMarkdownPath reports whether path is a Markdown file by extension.

func IsValidRef

func IsValidRef(repo, ref string) bool

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

func ListBranches(repo string) []string

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

func ListRemoteBranches(repo string) []string

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.

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

func CollapseToHunks(lines []DiffLine, ctx int) []DiffLine

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
}

FileDiff is the full-file, line-by-line diff for one path.

func LoadDiff

func LoadDiff(repo, base, path string) (*FileDiff, error)

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

func LoadDiffNoGit(repo, path string) (*FileDiff, error)

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

func ListFiles(repo, base string) ([]FileEntry, error)

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

func ListFilesNoGit(repo, single string) ([]FileEntry, error)

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

type HTMLBlock struct {
	HTML      template.HTML
	StartLine int
	EndLine   int
}

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

func RenderHTMLBlocks(src []byte) []HTMLBlock

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.

Empty input → nil. Input without a <body> → nil (no commentable surface).

type Heading added in v0.2.0

type Heading struct {
	Level int
	ID    string
	Text  string
	Line  int
}

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

func ExtractHeadings(src []byte) []Heading

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

type MarkdownBlock struct {
	HTML      template.HTML
	StartLine int
	EndLine   int
}

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) []MarkdownBlock

RenderMarkdownBlocks parses src and returns each commentable unit as safe HTML tagged with its source line span. Empty input → nil.

Jump to

Keyboard shortcuts

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