Tucker McKnight <tmcknight@instructure.com> | Sun May 17 2026
Make all pages have both /branch and /tag URLs
Make the "branches" dropdown menu also have tags, rename it to
"rel dropdown."
Also use navhelper for links in more places
Note: still has a "todo" for filling in the file contents in
src/vcses/git/operations.ts.0 1 2
import branchesListItems from '../dist/js_templates/common/branchesListItems.js'
const setCheckbox = window.setCheckbox
const currentTheme = window.currentTheme0 1 2
import {branchesAndTagsResults} from '../dist/js_templates/common/branchesListItems.js'
const setCheckbox = window.setCheckbox
const currentTheme = window.currentTheme150 151 152 153 154 155 156
}
})
showBranchesResults(searchBox.value)
const pageLoadLastTouch = document.getElementById("showLastTouch")
if (pageLoadLastTouch) {
setShowLastTouch(pageLoadLastTouch.checked)150 151 152 153 154
}
})
const pageLoadLastTouch = document.getElementById("showLastTouch")
if (pageLoadLastTouch) {
setShowLastTouch(pageLoadLastTouch.checked)48 49 50 51 52 53
return m('li', {class: "page-item"},
m('a', {
class: `page-link ${pageObj.pageNumber === data.patchPage.pageNumber ? 'active' : ''}`,
href: `${data.reposPath}/${slugify(data.patchPage.repoName)}/${data.patchPage.type}/${data.patchPage.relName}/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()}/page${pageObj.pageNumber}`,
}, pageObj.pageNumber)
)
})62 63 64 65 66 67
commit.isMerge ? m('span', {class: 'badge rounded-pill bg-primary me-1'}, 'merge') : null,
m('a', {
class: "fs-5",
href: `${data.reposPath}/${slugify(data.patchPage.repoName)}/${data.patchPage.type}/${slugify(data.patchPage.relName)}/commits/${commit.hash}`,
}, commit.message.split('\n').slice(0, 1)),
m('br'),
m('span', date(commit.date)),62 63 64 65 66 67
commit.isMerge ? m('span', {class: 'badge rounded-pill bg-primary me-1'}, 'merge') : null,
m('a', {
class: "fs-5",
href: nav.commit(commit.hash),
}, commit.message.split('\n').slice(0, 1)),
m('br'),
m('span', date(commit.date)),82 83 84 85 86 87
return m('li', {class: "page-item"},
m('a', {
class: `page-link ${pageObj.pageNumber === data.patchPage.pageNumber ? 'active' : ''}`,
href: `${data.reposPath}/${slugify(data.patchPage.repoName)}/${data.patchPage.type}/${data.patchPage.relName}/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()}/page${pageObj.pageNumber}`,
}, pageObj.pageNumber)
)
})0 1 2 3 4 5 6 7 8
export default (
branches: Array<{name: string, href: string, date: string}>,
defaultBranch: string,
currentBranch: string,
sortBy: 'name' | 'date',
): string => {
return branches.sort((a, b) => {
let comparison = 0
if (a[sortBy] < b[sortBy]) { comparison = -1 }
if (a[sortBy] > b[sortBy]) { comparison = 1 }0 1 2 3 4 5 6 7 8 9 10 11 12
export const branchesAndTagsResults = (
rels: {
branches: Array<{name: string, href: string, date: string}>,
tags: Array<{name: string, href: string, date: string}>,
},
defaultBranch: string,
currentRef: string,
currentRefType: "branch" | "tag",
sortBy: 'name' | 'date',
): string => {
return `<div class="branch">` + rels.branches.sort((a, b) => {
let comparison = 0
if (a[sortBy] < b[sortBy]) { comparison = -1 }
if (a[sortBy] > b[sortBy]) { comparison = 1 }12 13 14 15 16 17
return comparison
}).map((branch) => {
const currentBadge = currentBranch === branch.name
? '<div class="badge rounded-pill bg-secondary mx-1">current</div>'
: ''
12 13 14 15 16 17
return comparison
}).map((branch) => {
const currentBadge = currentRef === branch.name && currentRefType === "branch"
? '<div class="badge rounded-pill bg-secondary mx-1">current</div>'
: ''
1 2 3 4 5 6
import { type ReposConfiguration } from '../../src/configTypes.ts'
import { type Repository } from '../../src/dataTypes.ts'
import { NavHelper } from '../helpers/nav.ts'
import branchesListItems from './branchesListItems.ts'
export default async (reposConfig: ReposConfiguration, eleventyConfig: any, data: any, pageContent: any) => {
// this still necessary?1 2 3 4 5 6
import { type ReposConfiguration } from '../../src/configTypes.ts'
import { type Repository } from '../../src/dataTypes.ts'
import { NavHelper } from '../helpers/nav.ts'
import relDropDown from './relDropDown.ts'
export default async (reposConfig: ReposConfiguration, eleventyConfig: any, data: any, pageContent: any) => {
// this still necessary?21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
currentRefType: data.currentRefType,
})
const branchesWithHrefs = repo.branches.map((branch) => {
return {
name: branch.name,
href: nav.refHome({refName: branch.name, refType: 'branch'}) + '/' + data.nav.path,
date: repo.commits.get(branch.sha).date.toISOString(),
}
}).concat(repo.tags.map((tag) => {
return {
name: tag.name,
href: nav.refHome({refName: tag.name, refType: 'tag'}) + '/' + data.nav.path,
date: repo.commits.get(tag.sha).date.toISOString(),
}
}))
return {
rootPath: nav.rootPath(),21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
currentRefType: data.currentRefType,
})
const branchesWithHrefs = {
branches: repo.branches.map((branch) => {
return {
name: branch.name,
href: nav.refHome({refName: branch.name, refType: 'branch'}) + '/' + data.nav.path,
date: repo.commits.get(branch.sha).date.toISOString(),
}
}),
tags: (repo.tags.map((tag) => {
return {
name: tag.name,
href: nav.refHome({refName: tag.name, refType: 'tag'}) + '/' + data.nav.path,
date: repo.commits.get(tag.sha).date.toISOString(),
}
}))
}
return {
rootPath: nav.rootPath(),22 23 24 25 26 27
m('div', {class: "col"},
m('p', [
'Files snapshot from ',
m('span', {class: "font-monospace"}, fileInfo.branchName)
])
)
),22 23 24 25 26 27
m('div', {class: "col"},
m('p', [
'Files snapshot from ',
m('span', {class: "font-monospace"}, fileInfo.refName)
])
)
),30 31 32 33 34 35
m('div', {class: "col"},
m('h3', [
m('span', {class: "bezel-gray p-1 my-1 d-inline-block"},
m('a', {href: `${data.reposPath}/${fileInfo.repoName}/branch/${fileInfo.branchName}/files`}, './')
),
fileInfo.file.split('/').map((dir, index, arr) => {
if (index === arr.length - 1) {30 31 32 33 34 35
m('div', {class: "col"},
m('h3', [
m('span', {class: "bezel-gray p-1 my-1 d-inline-block"},
m('a', {href: `${data.reposPath}/${fileInfo.repoName}/${fileInfo.type}/${fileInfo.refName}/files`}, './')
),
fileInfo.file.split('/').map((dir, index, arr) => {
if (index === arr.length - 1) {70 71 72 73 74 75
m('div', {class: "row my-3"},
m('div', {class: "col"},
m('span', m('a', {
href: `${data.reposPath}/${slugify(fileInfo.repoName)}/branch/${slugify(fileInfo.branchName)}/raw/${fileInfo.file.split('.').map(filePart => slugify(filePart)).join('.')}`}, 'View raw file'))
)
),
(fileInfo.file.endsWith(".md") ?70 71 72 73 74 75
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") ?125 126 127 128 129 130
m('code', {style: "white-space: pre;"}, [
m('pre', {class: "language-text"}, m.trust(
currentRef.fileList.get(fileInfo.file).fileInfo.blameLines.map((annotation) => {
return `<a href="${data.reposPath}/${slugify(fileInfo.repoName)}/branch/${slugify(fileInfo.branchName)}/commits/${annotation.sha}">${annotation.sha.substr(0, 6)}</a> ${annotation.author}`
}).join('\n')))
])
),125 126 127 128 129 130
m('code', {style: "white-space: pre;"}, [
m('pre', {class: "language-text"}, m.trust(
currentRef.fileList.get(fileInfo.file).fileInfo.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')))
])
),0 1 2 3 4 5 6 7 8 9 10
import m from 'mithril'
import { type SortedFileList, type Repository } from "../src/dataTypes.ts"
import htmlPage from './common/htmlPage.ts'
export default async (reposConfig: any, eleventyConfig: any, data: any) => {
const branch: Repository['branches'][0] = data.currentRef
const topLevelFilesOnly = eleventyConfig.getFilter("topLevelFilesOnly")
const slugify = eleventyConfig.getFilter("slugify")
const files: SortedFileList = topLevelFilesOnly(Array.from(branch.fileList.keys()), '')
const pageContent = [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
import m from 'mithril'
import { type SortedFileList, type Repository } from "../src/dataTypes.ts"
import htmlPage from './common/htmlPage.ts'
import {NavHelper} from './helpers/nav.ts'
export default async (reposConfig: any, eleventyConfig: any, data: any) => {
const branch: Repository['branches'][0] = data.currentRef
const topLevelFilesOnly = eleventyConfig.getFilter("topLevelFilesOnly")
const slugify = eleventyConfig.getFilter("slugify")
const nav = NavHelper({
reposConfig,
slugify,
currentRepoName: data.currentRepo.name,
currentRefName: data.currentRef.name,
currentRefType: data.currentRefType,
})
const files: SortedFileList = topLevelFilesOnly(Array.from(branch.fileList.keys()), '')
const pageContent = [26 27 28 29 30 31 32 33 34 35 36 37
return m('li', {class: 'list-group-item'}, [
file.isDirectory ? m.trust('<span>📁 </span>') : null,
m('a', {
href: `${data.reposPath}/${slugify(data.flatRef.repoName)}/branch/${slugify(data.flatRef.relName)}/files/${
file.fullPath.split('/')
.map((pathPart) => {
return pathPart.split('.').map((subPart) => {
return slugify(subPart)
}).join('.')
}).join('/')}.html`
}, file.name)
])
}))26 27 28 29 30 31
return m('li', {class: 'list-group-item'}, [
file.isDirectory ? m.trust('<span>📁 </span>') : null,
m('a', {
href: nav.file(file)
}, file.name)
])
}))0 1
i
mport { type ReposConfiguration } from "../../src/configTypes.ts"
/**0 1 2
import { type SortedFileList } from '../../src/dataTypes.ts'
import { type ReposConfiguration } from "../../src/configTypes.ts"
/**49 50 51 52 53
? repoBasePath
: refPath(refName, refType)
},
files: (newRef: RefArgument = null) => {
const refName = newRef?.refName || args.currentRefName
const refType = newRef?.refType || args.currentRefType49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
? repoBasePath
: refPath(refName, refType)
},
file: (file: SortedFileList[0], newRef: RefArgument = null) => {
const refName = newRef?.refName || args.currentRefName
const refType = newRef?.refType || args.currentRefType
return `${refPath(refName, refType)}/files/${file.fullPath.split('/')
.map((pathPart) => {
return pathPart.split('.').map((subPart) => {
return args.slugify(subPart)
}).join('.')
}).join('/')}.html`
},
files: (newRef: RefArgument = null) => {
const refName = newRef?.refName || args.currentRefName
const refType = newRef?.refType || args.currentRefType15 16 17 18 19 20 21 22 23 24 25 26 27
return (
m('div', {class: "m-2 card bezel-gray flex-grow-1", style: "flex-basis: 20rem;"},
m('div', {class: "card-header"},
m('a', {class: "card-title fs-5", href: `${data.reposPath}/${slugify(repo.name)}/branch/${repo.defaultBranch}`}, repo.name)
),
m('div', {class: "card-body"},
repo.description ? m('p', {class: "card-text"}, repo.description) : null
),
m('div', {class: "card-footer"}, [
m('a', {
href: `${data.reposPath}/${slugify(repo.name)}/branch/${repo.defaultBranch}`,
class: "ms-0 me-2 my-2 btn btn-primary text-white"
}, 'Go to site'),
m('button', {15 16 17 18 19 20 21 22 23 24 25 26 27
return (
m('div', {class: "m-2 card bezel-gray flex-grow-1", style: "flex-basis: 20rem;"},
m('div', {class: "card-header"},
m('a', {class: "card-title fs-5", href: `${data.reposPath}/${slugify(repo.name)}`}, repo.name)
),
m('div', {class: "card-body"},
repo.description ? m('p', {class: "card-text"}, repo.description) : null
),
m('div', {class: "card-footer"}, [
m('a', {
href: `${data.reposPath}/${slugify(repo.name)}`,
class: "ms-0 me-2 my-2 btn btn-primary text-white"
}, 'Go to site'),
m('button', {17 18 19 20 21 22
reposConfig,
slugify,
currentRepoName: repo.name,
currentRefName: data.flatRef.name,
currentRefType: data.flatRef.type,
})
17 18 19 20 21 22
reposConfig,
slugify,
currentRepoName: repo.name,
currentRefName: data.currentRef.name,
currentRefType: data.flatRef.type,
})
148 149 150 151 152 153 154 155 156 157
}
)
eleventyConfig.addFilter("getDirectoryContents", (repo: string, branch: string, dirPath: string) => {
const fileList = reposData.find(
current => current.name === repo
).branches.find(
current => current.name === branch
).fileList
return Array.from(fileList.keys()).filter(148 149 150 151 152 153 154 155 156 157 158
}
)
eleventyConfig.addFilter("getDirectoryContents", (repo: string, refName: string, refType: 'branch' | 'tag', dirPath: string) => {
const refKey = refType === 'branch' ? 'branches' : 'tags'
const fileList = reposData.find(
current => current.name === repo
)[refKey].find(
current => current.name === refName
).fileList
return Array.from(fileList.keys()).filter(244 245 246 247 248 249 250 251
return sortedByDirectory
})
eleventyConfig.addFilter("isDirectory", (filename: string, repoName: string, branchName: string) => {
const repo = reposData.find(repo => repo.name === repoName)
const files = repo.branches.find(branch => branch.name === branchName).fileList
const isDirectory = Array.from(files.keys()).some((testFile) => {
return testFile.startsWith(filename + '/') && (testFile !== filename)
})244 245 246 247 248 249 250 251 252
return sortedByDirectory
})
eleventyConfig.addFilter("isDirectory", (filename: string, repoName: string, refName: string, refType: 'branch' | 'tag') => {
const repo = reposData.find(repo => repo.name === repoName)
const refKey = refType === 'branch' ? 'branches' : 'tags'
const files = repo[refKey].find(ref => ref.name === refName).fileList
const isDirectory = Array.from(files.keys()).some((testFile) => {
return testFile.startsWith(filename + '/') && (testFile !== filename)
})421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436
flatFiles: flatFilesData,
permalink: (data) => {
const repoName = data.fileInfo.repoName
const branchName = data.fileInfo.branchName
const refType = data.fileInfo.type
return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/${refType}/${eleventyConfig.getFilter("slugify")(branchName)}/raw/${data.fileInfo.file.split('.').map(filePart => eleventyConfig.getFilter("slugify")(filePart)).join('.')}`
},
eleventyComputed: {
currentRef: (data) => reposData.find(repo => {
return repo.name === data.fileInfo.repoName
}).branches.find(branch => {
return branch.name === data.fileInfo.branchName
}),
currentRefType: (data) => data.fileInfo.refType,
}
}
)421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440
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 => eleventyConfig.getFilter("slugify")(filePart)).join('.')}`
},
eleventyComputed: {
currentRef: (data) => {
const refKey = data.fileInfo.type === 'branch' ? 'branches' : 'tags'
return reposData.find(repo => {
return repo.name === data.fileInfo.repoName
})[refKey].find(ref => {
return ref.name === data.fileInfo.refName
})
},
currentRefType: (data) => data.fileInfo.type,
}
}
)174 175 176 177 178
font-size: 1rem;
}
.dropdown-branches {
list-style: none;
padding: 0;174 175 176 177 178 179 180 181 182 183 184 185
font-size: 1rem;
}
.rel-dropdown[data-selected=branch] .tag {
display: none;
}
.rel-dropdown[data-selected=tag] .branch {
display: none;
}
.dropdown-branches {
list-style: none;
padding: 0;36 37 38 39 40 41
filesMap.set(path, {
get fileInfo() {
return {
contents: "",
lastModified: new Date(),
blameLines: []
}36 37 38 39 40 41
filesMap.set(path, {
get fileInfo() {
return {
contents: "", // TODO: implement this part
lastModified: new Date(),
blameLines: []
}