Tucker McKnight <tucker@pangolin.lan> | Sat Jul 04 2026
Put commit pages as a single permalink, not per-branch or per-tag Commit pages will be a special case where there isn't one on each /branch/branch-name or /tags/tag-name URL. Instead, commit pages are their own thing, detached from branches and tags. So the currentRef for a commit page is just the hash of the commit. There can be a 'commit' refType on the nav helper now. Commit pages will have their nav links just go back to the default branch. Additionally, this change makes pages use the nav helper for determining their permalinks, and uses the nav helper in other places where it wasn't being used before, to DRY things up and avoid broken links. Also, the URL scheme on the default branch for all pages will now omit the /branches/main part of the URL. Instead, default branch pages are just at the repo root path. E.g. /repos/my-repo/files for the default branch, but still /repos/my-repo/branches/development/files for the development branch.
0 1 2 3
import m from 'mithril'
import htmlPage from './common/htmlPage.ts'
export default async (
reposConfig: any,0 1 2 3 4
import m from 'mithril'
import htmlPage from './common/htmlPage.ts'
import { NavHelper } from './helpers/nav.ts'
export default async (
reposConfig: any,9 10 11 12 13
const slugify = eleventyConfig.getFilter("slugify")
const lineNumbers = eleventyConfig.getFilter("lineNumbers")
const languageExtension = eleventyConfig.getFilter("languageExtension")
return await htmlPage(reposConfig, eleventyConfig, data, [
m('div', {class: "row"},9 10 11 12 13 14 15 16 17 18 19 20
const slugify = eleventyConfig.getFilter("slugify")
const lineNumbers = eleventyConfig.getFilter("lineNumbers")
const languageExtension = eleventyConfig.getFilter("languageExtension")
const nav = NavHelper({
reposConfig,
slugify,
currentRepoName: data.currentRepo.name,
currentRefName: data.currentRef.name,
currentRefType: data.currentRefType,
})
return await htmlPage(reposConfig, eleventyConfig, data, [
m('div', {class: "row"},55 56 57 58 59 60
return m('div', {class: 'hunk'}, [
m('span', {class: "font-monospace fw-bold"},
m('a', {
href: `${data.reposPath}/${slugify(data.patchInfo.repoName)}/${data.patchInfo.type}/${slugify(data.patchInfo.refName)}/files/${hunk.fileName.split('/').map((filePart) => { return filePart.split('.').map((subpart) => { return slugify(subpart)}).join('.')}).join('/')}.html`
}, `${hunk.fileName}:${hunk.lineNumber}`)
),
m('div', {class: "diff d-flex"}, [55 56 57 58 59 60
return m('div', {class: 'hunk'}, [
m('span', {class: "font-monospace fw-bold"},
m('a', {
href: nav.file(hunk.fileName)
}, `${hunk.fileName}:${hunk.lineNumber}`)
),
m('div', {class: "diff d-flex"}, [48 49 50 51 52 53
return m('li', {class: "page-item"},
m('a', {
class: `page-link ${pageObj.pageNumber === data.patchPage.pageNumber ? 'active' : ''}`,
href: `${nav.commits()}/page${pageObj.pageNumber}`,
}, pageObj.pageNumber)
)
})48 49 50 51 52 53
return m('li', {class: "page-item"},
m('a', {
class: `page-link ${pageObj.pageNumber === data.patchPage.pageNumber ? 'active' : ''}`,
href: `${nav.commits(pageObj.pageNumber)}`,
}, pageObj.pageNumber)
)
})82 83 84 85 86 87
return m('li', {class: "page-item"},
m('a', {
class: `page-link ${pageObj.pageNumber === data.patchPage.pageNumber ? 'active' : ''}`,
href: `${nav.commits()}/page${pageObj.pageNumber}`,
}, pageObj.pageNumber)
)
})82 83 84 85 86 87
return m('li', {class: "page-item"},
m('a', {
class: `page-link ${pageObj.pageNumber === data.patchPage.pageNumber ? 'active' : ''}`,
href: `${nav.commits(pageObj.pageNumber)}`,
}, pageObj.pageNumber)
)
})0 1 2 3
type Ref = { name: string, href: string, date: string }
type RefType = 'branch' | 'tag'
// These are the menu items that show up in the ref: dropdown menu.
const htmlForRef = (ref: Ref, refType: RefType, currentPageRef: string, currentPageRefType: RefType, defaultBranch: string) => {0 1 2 3
type Ref = { name: string, href: string, date: string }
type RefType = 'branch' | 'tag' | 'commit'
// These are the menu items that show up in the ref: dropdown menu.
const htmlForRef = (ref: Ref, refType: RefType, currentPageRef: string, currentPageRefType: RefType, defaultBranch: string) => {68 69 70 71 72 73
),
m('li', {class: "nav-item me-4 mb-1"}, [
m('span', {class: "nav-link d-inline-block"}, 'ref:'),
relDropDown(ref, data.currentRefType, repo, branchesWithHrefs),
]),
m('ul', {class: "main-nav navbar-nav flex-wrap"}, [
m('li', {class: "nav-item"},68 69 70 71 72 73
),
m('li', {class: "nav-item me-4 mb-1"}, [
m('span', {class: "nav-link d-inline-block"}, 'ref:'),
relDropDown(ref.name, data.currentRefType, repo, branchesWithHrefs),
]),
m('ul', {class: "main-nav navbar-nav flex-wrap"}, [
m('li', {class: "nav-item"},78 79 80 81 82 83
m('a', {class: `nav-link ${data.navTab === 'files' ? 'active' : ''}`, href: nav.files()}, 'Files')
),
m('li', {class: "nav-item"},
m('a', {class: `nav-link ${data.navTab === 'commits' ? 'active' : ''}`, href: nav.commits()}, 'Commits')
),
m('li', {class: "nav-item"},
m('a', {class: `nav-link ${data.navTab === 'branches' ? 'active' : ''}`, href: nav.branches()}, 'Branches')78 79 80 81 82 83
m('a', {class: `nav-link ${data.navTab === 'files' ? 'active' : ''}`, href: nav.files()}, 'Files')
),
m('li', {class: "nav-item"},
m('a', {class: `nav-link ${data.navTab === 'commits' ? 'active' : ''}`, href: nav.commits(1)}, 'Commits')
),
m('li', {class: "nav-item"},
m('a', {class: `nav-link ${data.navTab === 'branches' ? 'active' : ''}`, href: nav.branches()}, 'Branches')34 35 36 37 38 39
]),
m('div', {class: "dropdown-divider"}),
m('div', {id: "dropdown-branches-results", class: "dropdown-branches"},
m.trust(branchesListItems(branchesWithHrefs, repo.defaultBranch, ref.name, refType, 'date'))
)
])
])34 35 36 37 38 39
]),
m('div', {class: "dropdown-divider"}),
m('div', {id: "dropdown-branches-results", class: "dropdown-branches"},
m.trust(branchesListItems(branchesWithHrefs, repo.defaultBranch, refName, refType, 'date'))
)
])
])2 3 4 5 6 7
import { type Repository } from '../src/dataTypes.ts'
import getFlatRefs from '../src/flatRefs.ts'
import { dateToRfc3339 } from '@11ty/eleventy-plugin-rss'
import flatPatches from '../src/flatPatches.ts'
export default async (
eleventyConfig: any,2 3 4 5 6 7
import { type Repository } from '../src/dataTypes.ts'
import getFlatRefs from '../src/flatRefs.ts'
import { dateToRfc3339 } from '@11ty/eleventy-plugin-rss'
import { flatPatches } from '../src/flatPatches.ts'
export default async (
eleventyConfig: any,1 2 3 4 5
import htmlPage from './common/htmlPage.ts'
import { type Repository } from '../src/dataTypes.ts'
import { type FlatFileEntry } from '../src/flatFiles.ts'
type Branch = Repository['branches'][0]
export default async (reposConfig: any, eleventyConfig: any, data: any) => {1 2 3 4 5 6
import htmlPage from './common/htmlPage.ts'
import { type Repository } from '../src/dataTypes.ts'
import { type FlatFileEntry } from '../src/flatFiles.ts'
import { NavHelper } from './helpers/nav.ts'
type Branch = Repository['branches'][0]
export default async (reposConfig: any, eleventyConfig: any, data: any) => {13 14 15 16 17 18 19 20 21 22
const highlightCode = eleventyConfig.getFilter("highlightCode")
const languageExtension = eleventyConfig.getFilter("languageExtension")
const renderContentIfAvailable = eleventyConfig.getFilter("renderContentIfAvailable")
const currentRef: Branch = data.currentRef
const currentRepo: Repository = data.currentRepo
const fileInfo: FlatFileEntry = data.fileInfo
const pageContent = [
m('div', {class: "row mt-3 mb-1"},
m('div', {class: "col"},13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
const highlightCode = eleventyConfig.getFilter("highlightCode")
const languageExtension = eleventyConfig.getFilter("languageExtension")
const renderContentIfAvailable = eleventyConfig.getFilter("renderContentIfAvailable")
const currentRef: Branch = data.currentRef
const currentRepo: Repository = data.currentRepo
const fileInfo: FlatFileEntry = data.fileInfo
const nav = NavHelper({
reposConfig,
slugify,
currentRepoName: currentRepo.name,
currentRefName: currentRef.name,
currentRefType: data.currentRefType,
})
const pageContent = [
m('div', {class: "row mt-3 mb-1"},
m('div', {class: "col"},55 56 57 58 59 60 61 62 63 64
return m('li', {class: 'list-group-item'}, [
dir.isDirectory ? m('span', m.trust('📁 ')) : null,
m('a', {
href: `${data.reposPath}/${slugify(fileInfo.repoName)}/${fileInfo.type}/${slugify(fileInfo.refName)}/files/${dir.fullPath.split('/').map((pathPart) => {
return pathPart.split('.').map((subPart) => {
return slugify(subPart)
}).join('.')
}).join('/')}`},
getRelativePath(fileInfo.file, dir.name)
)
])55 56 57 58 59 60
return m('li', {class: 'list-group-item'}, [
dir.isDirectory ? m('span', m.trust('📁 ')) : null,
m('a', {
href: nav.file(dir.fullPath)},
getRelativePath(fileInfo.file, dir.name)
)
])71 72 73 74 75 76
m('div', {class: "row my-3"},
m('div', {class: "col"},
m('span', m('a', {
href: `${data.reposPath}/${slugify(fileInfo.repoName)}/${fileInfo.type}/${slugify(fileInfo.refName)}/raw/${fileInfo.file.split('.').map(filePart => slugify(filePart)).join('.')}`}, 'View raw file'))
)
),
(fileInfo.file.endsWith(".md") ?71 72 73 74 75 76
m('div', {class: "row my-3"},
m('div', {class: "col"},
m('span', m('a', {
href: nav.raw(fileInfo.file)}, 'View raw file'))
)
),
(fileInfo.file.endsWith(".md") ?126 127 128 129 130 131
m('code', {style: "white-space: pre;"}, [
m('pre', {class: "language-text"}, m.trust(
(await currentRepo.files(fileInfo.file, currentRef.sha)).blameLines.map((annotation) => {
return `<a href="${data.reposPath}/${slugify(fileInfo.repoName)}/${fileInfo.type}/${slugify(fileInfo.refName)}/commits/${annotation.sha}">${annotation.sha.substr(0, 6)}</a> ${annotation.author}`
}).join('\n')))
])
),126 127 128 129 130 131
m('code', {style: "white-space: pre;"}, [
m('pre', {class: "language-text"}, m.trust(
(await currentRepo.files(fileInfo.file, currentRef.sha)).blameLines.map((annotation) => {
return `<a href="${nav.commit(annotation.sha)}">${annotation.sha.substr(0, 6)}</a> ${annotation.author}`
}).join('\n')))
])
),35 36 37 38 39 40
return m('li', {class: 'list-group-item'}, [
file.isDirectory ? m.trust('<span>📁 </span>') : null,
m('a', {
href: nav.file(file)
}, file.name)
])
}))35 36 37 38 39 40
return m('li', {class: 'list-group-item'}, [
file.isDirectory ? m.trust('<span>📁 </span>') : null,
m('a', {
href: nav.file(file.fullPath)
}, file.name)
])
}))0 1 2
import { type SortedFileList } from '../../src/dataTypes.ts'
import { type ReposConfiguration } from "../../src/configTypes.ts"
/**0 1
i
mport { type ReposConfiguration } from "../../src/configTypes.ts"
/**13 14 15 16 17 18
slugify: Function,
currentRepoName: string,
currentRefName: string,
currentRefType: 'branch' | 'tag'
}
export const NavHelper = (args: CurrentRefArgs) => {
const reposPath = args.reposConfig.path || ""13 14 15 16 17 18
slugify: Function,
currentRepoName: string,
currentRefName: string,
currentRefType: 'branch' | 'tag' | 'commit'
}
export const NavHelper = (args: CurrentRefArgs) => {
const reposPath = args.reposConfig.path || ""23 24 25 26 27 28
// or 'repos/my-repo-name/branches.'
const repoBasePath = `${reposPath}/${args.slugify(args.currentRepoName)}`
const refPath = (refName, refType) => `${repoBasePath}/${refType}/${args.slugify(refName)}`
type RefArgument = {
refName: string,23 24 25 26 27 28 29 30 31 32 33 34
// or 'repos/my-repo-name/branches.'
const repoBasePath = `${reposPath}/${args.slugify(args.currentRepoName)}`
const refPath = (refName, refType) => {
if (refType === 'commit'
|| (refType === 'branch' && refName === args.reposConfig.repos[args.currentRepoName].defaultBranch)) {
return repoBasePath
}
return `${repoBasePath}/${refType}/${args.slugify(refName)}`
}
type RefArgument = {
refName: string,3 4 5 6 7 8
import {repos, getBranchesAndTags} from './src/repos.ts'
import getFlatRefs from './src/flatRefs.ts'
import { flatFiles, flatDirectories } from './src/flatFiles.ts'
import flatPatches from './src/flatPatches.ts'
import paginatedPatches, {type PatchPage} from './src/paginatedPatches.ts'
import {getLocation} from './src/helpers.ts'
import {ReposConfiguration} from './src/configTypes.ts'3 4 5 6 7 8
import {repos, getBranchesAndTags} from './src/repos.ts'
import getFlatRefs from './src/flatRefs.ts'
import { flatFiles, flatDirectories } from './src/flatFiles.ts'
import { flatPatches, commitsInRepo } from './src/flatPatches.ts'
import paginatedPatches, {type PatchPage} from './src/paginatedPatches.ts'
import {getLocation} from './src/helpers.ts'
import {ReposConfiguration} from './src/configTypes.ts'21 22 23 24 25
import tagsJsTemplate from './js_templates/tags.ts'
import rawJsTemplate from './js_templates/raw.ts'
import feedJsTemplate from './js_templates/feed.ts'
const ajv = new Ajv()
const exec = util.promisify(childProcess.exec)21 22 23 24 25 26
import tagsJsTemplate from './js_templates/tags.ts'
import rawJsTemplate from './js_templates/raw.ts'
import feedJsTemplate from './js_templates/feed.ts'
import { NavHelper } from './js_templates/helpers/nav.ts'
const ajv = new Ajv()
const exec = util.promisify(childProcess.exec)307 308 309 310 311 312 313 314 315
alias: "flatRef",
},
permalink: (data) => {
const repoName = data.flatRef.repoName
const refName = data.flatRef.refName
const refType = data.flatRef.type
return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/${refType}/${eleventyConfig.getFilter("slugify")(refName)}/branches/`
},
eleventyComputed: {
nav: {307 308 309 310 311 312 313 314 315 316 317 318 319 320
alias: "flatRef",
},
permalink: (data) => {
const nav = NavHelper({
reposConfig: reposConfiguration,
slugify,
currentRepoName: data.flatRef.repoName,
currentRefName: data.flatRef.refName,
currentRefType: data.flatRef.type,
})
return nav.branches() + '/'
},
eleventyComputed: {
nav: {347 348 349 350 351 352 353 354 355
alias: "flatRef",
},
permalink: (data) => {
const repoName = data.flatRef.repoName
const refName = data.flatRef.refName
const refType = data.flatRef.type
return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/${refType}/${eleventyConfig.getFilter("slugify")(refName)}/tags/`
},
eleventyComputed: {
nav: {347 348 349 350 351 352 353 354 355 356 357 358 359 360
alias: "flatRef",
},
permalink: (data) => {
const nav = NavHelper({
reposConfig: reposConfiguration,
slugify,
currentRepoName: data.flatRef.repoName,
currentRefName: data.flatRef.refName,
currentRefType: data.flatRef.type,
})
return nav.tags() + '/'
},
eleventyComputed: {
nav: {391 392 393 394 395 396 397 398 399
},
flatFiles: flatFilesData.concat(flatDirectoriesData),
permalink: (data) => {
const repoName = data.fileInfo.repoName
const refName = data.fileInfo.refName
const refType = data.fileInfo.type
return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/${refType}/${eleventyConfig.getFilter("slugify")(refName)}/files/${data.fileInfo.file.split('/').map((filePart) => filePart.split('.').map((subPart) => eleventyConfig.getFilter("slugify")(subPart)).join('.')).join('/')}.html`
},
eleventyComputed: {
nav: {391 392 393 394 395 396 397 398 399 400 401 402 403 404
},
flatFiles: flatFilesData.concat(flatDirectoriesData),
permalink: (data) => {
const nav = NavHelper({
reposConfig: reposConfiguration,
slugify,
currentRepoName: data.fileInfo.repoName,
currentRefName: data.fileInfo.refName,
currentRefType: data.fileInfo.type,
})
return nav.file(data.fileInfo.file)
},
eleventyComputed: {
nav: {433 434 435 436 437 438 439 440 441
eleventyAllowMissingExtension: true,
flatFiles: flatFilesData,
permalink: (data) => {
const repoName = data.fileInfo.repoName
const refName = data.fileInfo.refName
const refType = data.fileInfo.type
return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/${refType}/${eleventyConfig.getFilter("slugify")(refName)}/raw/${data.fileInfo.file.split('/').map((filePart) => filePart.split('.').map((subPart) => eleventyConfig.getFilter("slugify")(subPart)).join('.')).join('/')}`
},
eleventyComputed: {
currentRepo: (data) => reposData.find(repo => {433 434 435 436 437 438 439 440 441 442 443 444 445 446
eleventyAllowMissingExtension: true,
flatFiles: flatFilesData,
permalink: (data) => {
const nav = NavHelper({
reposConfig: reposConfiguration,
slugify,
currentRepoName: data.fileInfo.repoName,
currentRefName: data.fileInfo.refName,
currentRefType: data.fileInfo.type,
})
return nav.raw(data.fileInfo.file)
},
eleventyComputed: {
currentRepo: (data) => reposData.find(repo => {467 468 469 470 471 472 473 474 475
alias: "flatRef",
},
permalink: (data) => {
const repoName = data.flatRef.repoName
const refName = data.flatRef.refName
const refType = data.flatRef.type
return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/${refType}/${eleventyConfig.getFilter("slugify")(refName)}/files/`
},
eleventyComputed: {
nav: {467 468 469 470 471 472 473 474 475 476 477 478 479 480
alias: "flatRef",
},
permalink: (data) => {
const nav = NavHelper({
reposConfig: reposConfiguration,
slugify,
currentRepoName: data.flatRef.repoName,
currentRefName: data.flatRef.refName,
currentRefType: data.flatRef.type,
})
return nav.files() + '/'
},
eleventyComputed: {
nav: {506 507 508 509 510 511 512 513 514 515 516 517 518 519
alias: "flatRef",
},
permalink: (data) => {
const repoName = data.flatRef.repoName
const refName = data.flatRef.refName
const refType = data.flatRef.type
if (refType === "branch" && refName === reposConfiguration.repos[repoName].defaultBranch) {
return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/`
}
else {
return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/${refType}/${eleventyConfig.getFilter("slugify")(refName)}/`
}
},
eleventyComputed: {
nav: {506 507 508 509 510 511 512 513 514 515 516 517 518 519
alias: "flatRef",
},
permalink: (data) => {
const nav = NavHelper({
reposConfig: reposConfiguration,
slugify,
currentRepoName: data.flatRef.repoName,
currentRefName: data.flatRef.refName,
currentRefType: data.flatRef.type,
})
return nav.refHome() + '/'
},
eleventyComputed: {
nav: {552 553 554 555 556 557 558 559 560
},
paginatedPatches: paginatedPatchesData,
permalink: (data) => {
const repoName = data.patchPage.repoName
const refName = data.patchPage.refName
const refType = data.patchPage.type
return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/${refType}/${eleventyConfig.getFilter("slugify")(refName)}/commits/page${data.patchPage.pageNumber}/`
},
eleventyComputed: {
nav: {552 553 554 555 556 557 558 559 560 561 562 563 564 565
},
paginatedPatches: paginatedPatchesData,
permalink: (data) => {
const nav = NavHelper({
reposConfig: reposConfiguration,
slugify,
currentRepoName: data.patchPage.repoName,
currentRefName: data.patchPage.refName,
currentRefType: data.patchPage.type,
})
return nav.commits(data.patchPage.pageNumber) + '/'
},
eleventyComputed: {
nav: {190 191 192 193 194 195 196 197 198
max-width: 100%;
}
.branch-selector .dropdown-menu {
min-width: 220px;
max-height: 80vh;
overflow-y: scroll;
}
.badge {190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
max-width: 100%;
}
.branch-selector {
/* Special purple box to draw user's attention to the fact that
* commit pages are a special case. */
.current-ref-commit {
background: $purple;
color: white;
}
.dropdown-menu {
min-width: 220px;
max-height: 80vh;
overflow-y: scroll;
}
}
.badge {0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
import { type Repository } from "./dataTypes.ts"
type FlatPatchRecord = {
commit: ReturnType<Repository['commits']['get']>,
repoName: string,
refName: string,
type: "branch" | "tag",
}
let cachedFlatPatches: Array<FlatPatchRecord> | null = null
export default async (repos: Array<Repository>)
: Promise<Array<FlatPatchRecord>> => {
if (cachedFlatPatches !== null) { return cachedFlatPatches }
const itemsForRef: (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
import { type Repository } from "./dataTypes.ts"
type Commit = ReturnType<Repository['commits']['get']>
type FlatPatchRecord = {
commit: Commit,
repoName: string,
refName: string,
type: "branch" | "tag",
}
let cachedFlatPatches: Array<FlatPatchRecord> | null = null
let cachedRepoCommits: Array<{
repoName: string,
commit: Commit,
}> | null = null
const flatPatches = (repos: Array<Repository>)
: Array<FlatPatchRecord> => {
if (cachedFlatPatches !== null) { return cachedFlatPatches }
const itemsForRef: (46 47
return cachedFlatPatches
}46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
return cachedFlatPatches
}
const commitsInRepo = (repos: Array<Repository>) : Array<{repoName: string, commit: Commit}> => {
if (cachedRepoCommits !== null) { return cachedRepoCommits }
cachedRepoCommits = repos.flatMap((repo) => {
return Array.from(repo.commits.values()).map((commit) => {
return {
repoName: repo.name,
commit,
}
})
})
return cachedRepoCommits
}
export {
flatPatches,
commitsInRepo,
}0 1 2 3
import { type Repository } from './dataTypes.ts'
import flatPatchesFunc from './flatPatches.js'
export type PatchPage = {
repoName: string,0 1 2 3
import { type Repository } from './dataTypes.ts'
import { flatPatches as flatPatchesFunc } from './flatPatches.js'
export type PatchPage = {
repoName: string,12 13 14 15 16
## Works in Progress
- [ ] Clean up the template views, create mithril components for rows and columns
## Ideas and Todos
12 13 14 15 16 17
## Works in Progress
- [ ] Clean up the template views, create mithril components for rows and columns
- [ ] [Make it only generate one page per commit](./projects/one-page-per-commit.md.html)
## Ideas and Todos