Tucker McKnight <tucker@pangolin.lan> | Sun Feb 15 2026
[WIP] things broken here, getting types correct for file caching Adds a new fileList map underneath a branch. fileList entries point to a fileInfo object, which should eventually reference the cached file contents underneath the commits.cachedFiles object. (It's an object so that we get a reference to it, rather than copying the whole value.) Also adds a new blameLines entry so we can save those in the object, too. Currently this is just using placeholder, empty arrays, and does not actually contain the contents of the files. Every file page is blank, as is the git blame info.
0 1 2 3 4 5 6 7 8 9 10
import m from 'mithril'
import render from 'mithril-node-render'
export default async (eleventyConfig: any, data: any) => {
const isDirectory = eleventyConfig.getFilter("isDirectory")
const topLevelFilesOnly = eleventyConfig.getFilter("topLevelFilesOnly")
const getDirectoryContents = eleventyConfig.getFilter("getDirectoryContents")
const getFileContents = eleventyConfig.getFilter("getFileContents")
const getFileLastTouchInfo = eleventyConfig.getFilter("getFileLastTouchInfo")
const getRelativePath = eleventyConfig.getFilter("getRelativePath")
const slugify = eleventyConfig.getFilter("slugify")
const lineNumbers = eleventyConfig.getFilter("lineNumbers")0 1 2 3 4 5 6 7 8 9 10 11
import m from 'mithril'
import render from 'mithril-node-render'
import { type Repository } from '../src/dataTypes.ts'
import { type FlatFileEntry } from '../src/flatFiles.ts'
type Branch = Repository['branches'][0]
export default async (eleventyConfig: any, data: any) => {
const isDirectory = eleventyConfig.getFilter("isDirectory")
const topLevelFilesOnly = eleventyConfig.getFilter("topLevelFilesOnly")
const getDirectoryContents = eleventyConfig.getFilter("getDirectoryContents")
const getRelativePath = eleventyConfig.getFilter("getRelativePath")
const slugify = eleventyConfig.getFilter("slugify")
const lineNumbers = eleventyConfig.getFilter("lineNumbers")13 14 15 16 17 18 19 20 21 22 23
const languageExtension = eleventyConfig.getFilter("languageExtension")
const renderContentIfAvailable = eleventyConfig.getFilter("renderContentIfAvailable")
return render([
m('div', {class: "row mt-3 mb-1"},
m('div', {class: "col"},
m('p', [
'Files snapshot from ',
m('span', {class: "font-monospace"}, data.fileInfo.branchName)
])
)
),13 14 15 16 17 18 19 20 21 22 23 24 25 26
const languageExtension = eleventyConfig.getFilter("languageExtension")
const renderContentIfAvailable = eleventyConfig.getFilter("renderContentIfAvailable")
const currentBranch: Branch = data.currentBranch
const fileInfo: FlatFileEntry = data.fileInfo
return render([
m('div', {class: "row mt-3 mb-1"},
m('div', {class: "col"},
m('p', [
'Files snapshot from ',
m('span', {class: "font-monospace"}, fileInfo.branchName)
])
)
),66 67 68 69 70 71 72 73 74
m('div', {class: "row my-3"},
m('div', {class: "col"},
m('span', m('a', {
href: `${data.reposPath}/${slugify(data.fileInfo.repoName)}/branches/${slugify(data.fileInfo.branchName)}/raw/${data.fileInfo.file.split('.').map(filePart => slugify(filePart)).join('.')}`}, 'View raw file'))
)
),
(data.fileInfo.file.endsWith(".md") ?
[
m('div', {class: "row my-2"},
m('div', {class: "col"},66 67 68 69 70 71 72 73 74
m('div', {class: "row my-3"},
m('div', {class: "col"},
m('span', m('a', {
href: `${data.reposPath}/${slugify(fileInfo.repoName)}/branches/${slugify(fileInfo.branchName)}/raw/${fileInfo.file.split('.').map(filePart => slugify(filePart)).join('.')}`}, 'View raw file'))
)
),
(fileInfo.file.endsWith(".md") ?
[
m('div', {class: "row my-2"},
m('div', {class: "col"},90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
),
m('div', {class: "row rendered-content"},
m('div', {class: "col"},
m.trust(await renderContentIfAvailable(await getFileContents(
data.fileInfo.repoName,
data.fileInfo.branchName,
data.fileInfo.file
), data.fileInfo.branchName)
))
)
]
: null),
m('div', {class: `row code-content ${data.fileInfo.file.endsWith('.md') ? 'd-none' : ''}`},
m('div', {class: "col"}, [
m('div', {class: "row"},
m('div', {class: "col-auto"},90 91 92 93 94 95 96 97 98 99 100 101 102
),
m('div', {class: "row rendered-content"},
m('div', {class: "col"},
m.trust(await renderContentIfAvailable(
currentBranch.fileList.get(fileInfo.file).fileInfo.contents
))
)
)
]
: null),
m('div', {class: `row code-content ${fileInfo.file.endsWith('.md') ? 'd-none' : ''}`},
m('div', {class: "col"}, [
m('div', {class: "row"},
m('div', {class: "col-auto"},113 114 115 116 117 118
m('div', {class: "col-auto p-0"},
m('code', {style: "white-space: pre;"},
m('pre', {class: "language-text"},
lineNumbers(await getFileContents(data.fileInfo.repoName, data.fileInfo.branchName, data.fileInfo.file)).map((lineNumber) => {
return lineNumber
}).join('\n')
)113 114 115 116 117 118
m('div', {class: "col-auto p-0"},
m('code', {style: "white-space: pre;"},
m('pre', {class: "language-text"},
lineNumbers(currentBranch.fileList.get(fileInfo.file).fileInfo.contents).map((lineNumber) => {
return lineNumber
}).join('\n')
)6 7 8 9 10 11
const topLevelFilesOnly = eleventyConfig.getFilter("topLevelFilesOnly")
const slugify = eleventyConfig.getFilter("slugify")
const files: SortedFileList = topLevelFilesOnly(branch.fileList, '')
return render([
m('div', {class: "row mt-3 mb-1"}, [6 7 8 9 10 11
const topLevelFilesOnly = eleventyConfig.getFilter("topLevelFilesOnly")
const slugify = eleventyConfig.getFilter("slugify")
const files: SortedFileList = topLevelFilesOnly(Array.from(branch.fileList.keys()), '')
return render([
m('div', {class: "row mt-3 mb-1"}, [0 1 2 3 4 5
export default async (eleventyConfig: any) => {
const getFileContents = eleventyConfig.getFilter("getFileContents")
return async (data) => {
return await getFileContents(data.fileInfo.repoName, data.fileInfo.branchName, data.fileInfo.file)
}
}0 1 2 3 4 5 6 7 8
import { type Repository } from "../src/dataTypes.ts"
type Branch = Repository['branches'][0]
export default async (eleventyConfig: any) => {
return async (data) => {
const branch: Branch = data.currentBranch
return branch.fileList.get(data.fileInfo.file).fileInfo.contents
}
}12 13 14 15 16 17
? latestCommit.message.split('\n')[0].substr(0, 72) + '...'
: latestCommit.message
const languageCounts = branch.fileList.reduce((counts, currentFile) => {
const fileParts = currentFile.split(".")
const fileExtension = fileParts[fileParts.length - 1]
// todo: add more ignoreable extensions or specific files12 13 14 15 16 17
? latestCommit.message.split('\n')[0].substr(0, 72) + '...'
: latestCommit.message
const languageCounts = Array.from(branch.fileList.keys()).reduce((counts, currentFile) => {
const fileParts = currentFile.split(".")
const fileExtension = fileParts[fileParts.length - 1]
// todo: add more ignoreable extensions or specific files27 28 29 30 31 32
// todo: this is probably broken for repos that use fewer than 6 languages
let languagePercentages: Array<[string, number]> = []
const total = branch.fileList.length
for (const entry of languageCounts) {
languagePercentages.push([entry[0], entry[1] / total])27 28 29 30 31 32
// todo: this is probably broken for repos that use fewer than 6 languages
let languagePercentages: Array<[string, number]> = []
const total = Array.from(branch.fileList.keys()).length
for (const entry of languageCounts) {
languagePercentages.push([entry[0], entry[1] / total])6 7 8 9 10 11
import flatPatches from './src/flatPatches.ts'
import paginatedPatches, {type PatchPage} from './src/paginatedPatches.ts'
import {getLocation} from './src/helpers.ts'
import * as operations from './src/vcses/git/operations.ts'
import {ReposConfiguration} from './src/configTypes.ts'
import { type SortedFileList } from './src/dataTypes.ts'
import {Ajv} from 'ajv'6 7 8 9 10
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'
import { type SortedFileList } from './src/dataTypes.ts'
import {Ajv} from 'ajv'127 128 129 130 131 132
)
eleventyConfig.addFilter("getDirectoryContents", (repo: string, branch: string, dirPath: string) => {
return reposData.find(current => current.name === repo).branches.find(current => current.name === branch).fileList.filter(file => file.startsWith(dirPath) && file !== dirPath)
})
eleventyConfig.addFilter("getRelativePath", (currentDir: string, fullFilePath: string) => {127 128 129 130 131 132 133 134 135 136 137 138 139 140
)
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(
file => file.startsWith(dirPath) && file !== dirPath
)
})
eleventyConfig.addFilter("getRelativePath", (currentDir: string, fullFilePath: string) => {362 363 364 365 366
const branchName = data.fileInfo.branchName
return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branchName)}/raw/${data.fileInfo.file.split('.').map(filePart => eleventyConfig.getFilter("slugify")(filePart)).join('.')}`
},
}
)
362 363 364 365 366 367 368 369 370 371 372 373
const branchName = data.fileInfo.branchName
return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branchName)}/raw/${data.fileInfo.file.split('.').map(filePart => eleventyConfig.getFilter("slugify")(filePart)).join('.')}`
},
eleventyComputed: {
currentBranch: (data) => reposData.find(repo => {
return repo.name === data.fileInfo.repoName
}).branches.find(branch => {
return branch.name === data.fileInfo.branchName
}),
}
}
)
0 1
export type Repository = {
name: string,
description?: string,0 1 2 3 4 5 6 7 8 9
import { type BlameInfo } from "./vcses/git/operations.ts"
export type FileInfo = {
contents: string,
lastModified: Date,
blameLines: Array<BlameInfo>,
}
export type Repository = {
name: string,
description?: string,0 1 2 3 4 5 6 7 8 9 10 11
import { type Repository } from "./dataTypes.ts"
let cachedFlatFiles = null
export default (repos: Array<Repository>) => {
if (cachedFlatFiles !== null) { return cachedFlatFiles }
cachedFlatFiles = repos.flatMap((repo) => {
return repo.branches.flatMap((branch) => {
return branch.fileList.map((file) => {
return {
file,
branchName: branch.name,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
import { type Repository } from "./dataTypes.ts"
let cachedFlatFiles: Array<FlatFileEntry> | null = null
export type FlatFileEntry = {
file: string,
branchName: string,
repoName: string,
}
export default (repos: Array<Repository>) : Array<FlatFileEntry> => {
if (cachedFlatFiles !== null) { return cachedFlatFiles }
cachedFlatFiles = repos.flatMap((repo) => {
return repo.branches.flatMap((branch) => {
return Array.from(branch.fileList.keys()).map((file) => {
return {
file,
branchName: branch.name,163 164 165 166 167 168
branches: branchesWithCompareToInfo,
cloneUrl: cloneUrl.cloneUrl(reposConfig.baseUrl, repoName),
defaultBranch: reposConfig.repos[repoName].defaultBranch,
tags: [], // todo
commits,
})
}163 164 165 166 167 168
branches: branchesWithCompareToInfo,
cloneUrl: cloneUrl.cloneUrl(reposConfig.baseUrl, repoName),
defaultBranch: reposConfig.repos[repoName].defaultBranch,
tags: [], // todo fill in tags, similar to branches
commits,
})
}1 2 3 4 5 6 7 8
import childProcess from 'child_process'
const exec = util.promisify(childProcess.exec)
import { getGitDiffsFromPatchText} from '../../helpers.ts'
import { type Repository } from '../../dataTypes.ts'
export const getFileList = async (branchName: string, repoLocation: string) => {
const command = `git -C ${repoLocation} ls-tree -r --name-only ${branchName}`
const result = await exec(command)1 2 3 4 5 6 7 8 9 10
import childProcess from 'child_process'
const exec = util.promisify(childProcess.exec)
import { getGitDiffsFromPatchText} from '../../helpers.ts'
import { type Repository, type FileInfo } from '../../dataTypes.ts'
export const getFileList = async (
branchName: string, repoLocation: string
) : Promise<Map<string, {fileInfo: FileInfo}>> => {
const command = `git -C ${repoLocation} ls-tree -r --name-only ${branchName}`
const result = await exec(command)19 20 21 22 23 24
// be [posts, posts/blog, posts/blog/one.md, posts/blog/two.md].
// This is because it's convenient to have the directories show up as their own "file"
// in the file list, even though git doesn't treat them that way.
const fileSet: Set<string> = new Set()
files.forEach((file) => {
const fileParts = file.split("/")
const allPathsInFile = fileParts.reduce((accumulator, currentValue, index) => {19 20 21 22 23 24
// be [posts, posts/blog, posts/blog/one.md, posts/blog/two.md].
// This is because it's convenient to have the directories show up as their own "file"
// in the file list, even though git doesn't treat them that way.
const filesMap: Map<string, {fileInfo: FileInfo}> = new Map()
files.forEach((file) => {
const fileParts = file.split("/")
const allPathsInFile = fileParts.reduce((accumulator, currentValue, index) => {31 32 33 34 35 36 37 38 39
}, [fileParts[0]])
allPathsInFile.forEach((path) => {
fileSet.add(path)
})
})
return Array.from(fileSet)
}
export const addBranchToCommitsMap = async(branchName: string, repoLocation: string, commits: Repository['commits']): Promise<void> => {31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
}, [fileParts[0]])
allPathsInFile.forEach((path) => {
filesMap.set(path, {
get fileInfo() {
return {
contents: "",
lastModified: new Date(),
blameLines: []
}
}
})
})
})
return filesMap
}
export const addBranchToCommitsMap = async(branchName: string, repoLocation: string, commits: Repository['commits']): Promise<void> => {104 105 106 107 108 109 110 111 112 113 114
date: new Date(date),
diffs,
parent: null,
})
} while (gitLogSubset.length > 1)
}
}
export const getFileLastTouchInfo = async (branch: string, filename: string, repoLocation: string) => {
const regex = RegExp(".* [0-9]+ [0-9]+")
const command = `git -C ${repoLocation} blame --porcelain ${branch} ${filename}`
const res = await exec(command)104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
date: new Date(date),
diffs,
parent: null,
cachedFiles: new Map(),
})
} while (gitLogSubset.length > 1)
}
}
export type BlameInfo = {
sha: string,
author: string,
}
export const getFileLastTouchInfo = async (
branch: string, filename: string, repoLocation: string
) : Promise<Array<BlameInfo>> => {
const regex = RegExp(".* [0-9]+ [0-9]+")
const command = `git -C ${repoLocation} blame --porcelain ${branch} ${filename}`
const res = await exec(command)131 132 133 134 135 136
}, initialValue)
let currentAuthor = ''
let authorsAndShasByLine = []
chunked.forEach((chunk) => {
let shaAndLineNumberParts = chunk[0].split(' ')
let line = parseInt(shaAndLineNumberParts[2])131 132 133 134 135 136
}, initialValue)
let currentAuthor = ''
let authorsAndShasByLine: Array<BlameInfo> = []
chunked.forEach((chunk) => {
let shaAndLineNumberParts = chunk[0].split(' ')
let line = parseInt(shaAndLineNumberParts[2])