# Code Browser & Refs ## URL Patterns | View | Route | | ----------------------- | ------------------------------------------- | | Root tree | `/{owner}/{repo}/tree/{ref}` | | Subtree | `/{owner}/{repo}/tree/{ref}/{path...}` | | Blob | `/{owner}/{repo}/blob/{ref}/{path...}` | | Blame | `/{owner}/{repo}/blame/{ref}/{path...}` | | Raw file | `/{owner}/{repo}/raw/{ref}/{path...}` | | Commit log | `/{owner}/{repo}/commits/{ref}` | | Commit log (file scope) | `/{owner}/{repo}/commits/{ref}/{path...}` | | Single commit diff | `/{owner}/{repo}/commit/{sha}` | | Branches & Tags | `/{owner}/{repo}/refs` | | Edit file (GET/POST) | `/{owner}/{repo}/edit/{branch}/{path...}` | | Delete file (POST) | `/{owner}/{repo}/delete/{branch}/{path...}` | `{ref}` = branch name, tag name, or commit SHA. Pagination via `?page=N` (1-indexed, 30 per page). A ref may contain `/` (`feature/x`), so chi's one-segment `{ref}` can't tell where it ends. Handlers rejoin `{ref}` and `*` (`routeRefPath`) and split them with `CodeService.SplitRefPath`: the longest branch or tag that ends at a segment boundary wins, and otherwise the first segment is the ref. The new-file POST and archive routes take the whole tail as the ref. Raw goes through `SplitRefPath` too, so `/raw/feature/x/README.md` serves `README.md` on `feature/x`. Git allows `#`, `?`, `%` and `'` in refs and file names, so every link and redirect to a ref or path is built with `internal/codeurl`: `codeurl.Path(owner, repo, kind, ref, path)` escapes each `/`-separated segment of the ref and path with `url.PathEscape`, keeping the slashes as separators. Results, `data-path` and the `cz_tree_open` cookie keep the decoded values; only URLs carry the escaped form. Inbound, chi routes on `r.URL.RawPath` whenever Go sets it, which happens when the request's escaping differs from Go's (a browser leaves `'` unescaped, Go escapes it), and only then are `{ref}` and `*` still escaped. `routeRefPath` therefore decodes the params only when `RawPath` is set; for `pct%2541` chi already hands back `pct%41`, which a second decode would turn into `pctA`. chi isn't switched to route on the decoded path, because `middleware/scope.go` splits the same `RawPath` so an encoded `%2F` can't shift segments between the router and the token-target policy, and `/releases/tag/{tagName}` relies on a tag's `%2F` staying in one segment: release links `url.PathEscape` the whole tag, and `PageReleaseDetail` decodes it the same way. The refs page's delete buttons `url.QueryEscape` the name they send. A tree URL whose path is a file redirects (302) to its blob URL. Tree and blob pages share the file tree sidebar (`components.FileTreeSidebar`), built by `buildSidebarTree` from the requested ref and path. Folders the viewer opens stay open: `static/file_tree.js` keeps them in the per-repo session cookie `cz_tree_open`, which the server reads to render them open. A folder rendered closed loads its children once from `/fragments/{owner}/{repo}/tree/{ref}/{path}`. The filter shows each loaded item whose name matches along with the folders above it, deriving that from the DOM without touching `open` or the cookie. --- ## CodeService (`internal/service/code_service.go`) `CodeService` has **no store dependency** — it reads git data directly from bare repos on disk via go-git. It is wired in `services.New()` and receives `config.GitConfig` (for `ReposRoot`). ### Key Methods - `ResolveRef(owner, repoName, ref)` → `(*object.Commit, ref, error)` — the returned ref is the branch or tag name, the full SHA, or HEAD's branch - `GetTree(owner, repoName, ref, path)` → `*TreeResult` - `ListEntriesWithLastCommit(ctx, repo, ref, dir)` → `[]TreeEntryWithLastCommit` — each entry with the last commit that touched it; cached for 60s per repo ID, resolved commit and dir, so a new commit, or a new repo that takes a deleted or transferred repo's name, is always listed fresh - `GetBlob(owner, repoName, ref, path)` → `*BlobResult` - `OpenRawBlob(owner, repoName, ref, path, maxBytes)` → `*RawBlob` — the file open for streaming, with `Size` and `IsBinary`; `ErrBlobTooLarge` past `maxBytes`. The caller closes it - `GetBlame(owner, repoName, ref, path)` → `*BlameResult` - `GetCommits(owner, repoName, ref, page, pageSize)` → `*CommitLog` - `GetCommit(owner, repoName, sha)` → `*CommitDetail` - `HighlightDiffs(owner, repoName, files)` — fills `DiffLine.HTML` in place; see [Syntax highlighting](#syntax-highlighting) - `ListRefs(owner, repoName, defaultBranch)` → `*RefsResult` — an annotated tag's `Hash` is its tag object's - `ListRefsPeeled(owner, repoName, defaultBranch)` → `*RefsResult` — tags' `Hash` peeled to the tagged commit, at an object read per tag; for the refs page, which shows the hashes - `SplitRefPath(owner, repoName, refPath)` → `(ref, path)` - `GetBranchFile(owner, repoName, branch, path, maxContent)` → `*BranchFile` — the file on a branch's tip (any other ref is `ErrRefNotFound`) with its SHA and mode, and its content when it's text within `maxContent` - `EditFile(owner, repoName, branch, oldPath, newPath, baseSHA, content, author, message)` → `error`; `DeleteFile(owner, repoName, branch, path, baseSHA, author, message)` → `(remainingDir, error)` — see [Editing and Deleting Files](#editing-and-deleting-files) - `CreateBranch(owner, repoName, name, fromRef)` → `error` - `DeleteBranch(owner, repoName, name)` → `error` - `CreateTag(owner, repoName, name, fromRef)` → `error` - `DeleteTag(owner, repoName, name)` → `error` ### ResolveRef Priority 1. Branch: `repo.Reference(plumbing.NewBranchReferenceName(ref), true)` 2. Tag: `repo.Reference(plumbing.NewTagReferenceName(ref), true)`, peeled through annotated tags (and tags of tags) by `peelTag`; a tag of a tree or blob is not found 3. Raw SHA: `repo.CommitObject(plumbing.NewHash(ref))` 4. HEAD fallback (when `ref == ""`): `repo.Head()` Returns `ErrEmptyRepo` sentinel when HEAD resolution fails (repo has no commits). Handlers return 404 on this error. A raw SHA must be the full 40 characters; an abbreviated one doesn't resolve. The `Ref` on result types is therefore the full SHA, and every URL is built from it. Only displayed text shortens it, through `components.RefLabel`. ### Result Types - `TreeResult` — `Entries []TreeEntry` (dirs first, then files, both sorted), `Ref`, `Path`, `Breadcrumbs` - `BlobResult` — `Lines []CodeLine`, `IsBinary bool`, `SHA`, `IsSymlink`, `IsBranch` (the ref named a branch), `BlameURL`, breadcrumbs - `BlameResult` — `Lines []BlameLine` with `ShowMeta bool` (true when commit run changes), `BlobURL`, breadcrumbs - `CommitLog` — `Commits []CommitSummary`, `Ref`, `Page`, `PrevPage`, `NextPage`, `HasMore` - `CommitDetail` — full commit with `Files []FileDiff` (hunks with add/del/ctx lines), `TotalAdded`, `TotalDeleted` - `RefsResult` — `Branches []BranchInfo` (`Name`, `Hash`, `IsDefault`), `Tags []TagInfo` (`Name`, `Hash`) --- ## Editing and Deleting Files Writers edit, rename and delete files from the browser; each is one commit onto the branch, authored as New file's commits are (`UserService.CommitAuthor`, so the keep-email-private setting applies). - **Where:** the blob page shows Edit for a text file of at most 1 MiB and Delete for any file or symlink, only on a branch (not a tag or commit SHA), to a viewer with `CanWrite`, on a repo that isn't archived. The routes refuse the same cases: 404 for an unreadable repo, a ref that isn't a branch, or a path that isn't a file; 403 for a non-writer or an archived repo; 422 when the editor can't open the file (binary, symlink, over 1 MiB, or text a textarea would change: not UTF-8, or a CR outside a CRLF, per `service.IsEditableText`). Deleting a folder or submodule is 404. - **Protected branches:** New file, edit, rename and delete are refused with 422 on a branch whose rule has `require_pull_request` (`BranchProtectionService.CheckWebCommit`; the edit page re-renders with the user's text kept). The check runs on submit, not when the page opens. Admins aren't exempt; the change goes in on another branch and merges as a pull request. See [api-reference](./api-reference.md#branch-protections). - **Stale saves:** the edit page and the delete dialog carry the blob SHA they loaded. `CodeService.EditFile` and `DeleteFile` refuse with `ErrFileChanged` (409) unless the path on the branch still holds that blob, and otherwise land on the current tip, so a commit to another file doesn't block them. `gitref.Move` still turns a push mid-write into `ErrRefMoved` (409). Neither creates a branch, which `CommitFile` does for an empty repo. - **Edit page** (`PageEditFile` / `SubmitEditFile`): changing the path renames the file, keeping its mode, creating folders and pruning the ones it empties; a target that holds anything is `ErrPathCollision` (409). An edit that changes neither path nor content is `ErrFileUnchanged` (422). An untouched path field is never cleaned, so a pushed name like `a\b.txt` isn't renamed by an edit. A refusal re-renders the page at its status with the user's path, content and message kept. The textarea renders `"\n" + content` because the HTML parser drops a newline right after `