[WIP] things broken here, getting types correct for file caching

cdb797f8466b8ff55815411acec54f4fbcddf7fa

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.
js_templates/file.ts:1
Before
1
2
  const getFileContents = eleventyConfig.getFilter("getFileContents")
  const getFileLastTouchInfo = eleventyConfig.getFilter("getFileLastTouchInfo")
After
1
2
import { type Repository } from '../src/dataTypes.ts'
import { type FlatFileEntry } from '../src/flatFiles.ts'
type Branch = Repository['branches'][0]
js_templates/file.ts:14
Before
14
          m('span', {class: "font-monospace"}, data.fileInfo.branchName)
After
14
  const currentBranch: Branch = data.currentBranch
  const fileInfo: FlatFileEntry = data.fileInfo

          m('span', {class: "font-monospace"}, fileInfo.branchName)
js_templates/file.ts:27
Before
27
28
29
30
31
32
33
            m('a', {href: `${data.reposPath}/${data.fileInfo.repoName}/branches/${data.fileInfo.branchName}/files`}, './')
          data.fileInfo.file.split('/').map((dir, index, arr) => {
                  href: `${data.reposPath}/${data.fileInfo.repoName}/branches/${data.fileInfo.branchName}/files/${arr.slice(0, index + 1).map((part) => slugify(part)).join('/')}.html`}, `${dir}/`)
    (isDirectory(data.fileInfo.file, data.fileInfo.repoName, data.fileInfo.branchName) ?
            topLevelFilesOnly(getDirectoryContents(data.fileInfo.repoName, data.fileInfo.branchName, data.fileInfo.file), data.fileInfo.file + '/').map((dir) => {
                  href: `${data.reposPath}/${slugify(data.fileInfo.repoName)}/branches/${slugify(data.fileInfo.branchName)}/files/${dir.fullPath.split('/').map((pathPart) => {
                  getRelativePath(data.fileInfo.file, dir.name)
After
27
28
29
30
31
32
33
            m('a', {href: `${data.reposPath}/${fileInfo.repoName}/branches/${fileInfo.branchName}/files`}, './')
          fileInfo.file.split('/').map((dir, index, arr) => {
                  href: `${data.reposPath}/${fileInfo.repoName}/branches/${fileInfo.branchName}/files/${arr.slice(0, index + 1).map((part) => slugify(part)).join('/')}.html`}, `${dir}/`)
    (isDirectory(fileInfo.file, fileInfo.repoName, fileInfo.branchName) ?
            topLevelFilesOnly(getDirectoryContents(fileInfo.repoName, fileInfo.branchName, fileInfo.file), fileInfo.file + '/').map((dir) => {
                  href: `${data.reposPath}/${slugify(fileInfo.repoName)}/branches/${slugify(fileInfo.branchName)}/files/${dir.fullPath.split('/').map((pathPart) => {
                  getRelativePath(fileInfo.file, dir.name)
js_templates/file.ts:67
Before
67
68
              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") ?
After
67
68
              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") ?
js_templates/file.ts:91
Before
91
92
93
94
95
96
97
                m.trust(await renderContentIfAvailable(await getFileContents(
                  data.fileInfo.repoName,
                  data.fileInfo.branchName,
                  data.fileInfo.file
                ), data.fileInfo.branchName)
              ))
        m('div', {class: `row code-content ${data.fileInfo.file.endsWith('.md') ? 'd-none' : ''}`},
After
91
92
93
94
95
96
97
                m.trust(await renderContentIfAvailable(
                  currentBranch.fileList.get(fileInfo.file).fileInfo.contents
                ))
              )
        m('div', {class: `row code-content ${fileInfo.file.endsWith('.md') ? 'd-none' : ''}`},
js_templates/file.ts:114
Before
114
                    lineNumbers(await getFileContents(data.fileInfo.repoName, data.fileInfo.branchName, data.fileInfo.file)).map((lineNumber) => {
After
114
                    lineNumbers(currentBranch.fileList.get(fileInfo.file).fileInfo.contents).map((lineNumber) => {
js_templates/file.ts:123
Before
123
124
125
126
127
128
129
130
131
132
133
134
135
                    (await getFileLastTouchInfo(
                      data.fileInfo.repoName,
                      data.fileInfo.branchName,
                      data.fileInfo.file
                    )).map((annotation) => {
                      return `<a href="${data.reposPath}/${slugify(data.fileInfo.repoName)}/branches/${slugify(data.fileInfo.branchName)}/commits/${annotation.sha}">${annotation.sha.substr(0, 6)}</a> ${annotation.author}`
                  await getFileContents(
                    data.fileInfo.repoName,
                    data.fileInfo.branchName,
                    data.fileInfo.file
                  ),
                    data.fileInfo.file,
                    data.fileInfo.repoName
After
123
124
125
126
127
128
129
130
131
132
133
134
135
                    currentBranch.fileList.get(fileInfo.file).fileInfo.blameLines.map((annotation) => {
                      return `<a href="${data.reposPath}/${slugify(fileInfo.repoName)}/branches/${slugify(fileInfo.branchName)}/commits/${annotation.sha}">${annotation.sha.substr(0, 6)}</a> ${annotation.author}`
                  currentBranch.fileList.get(fileInfo.file).fileInfo.contents,
                    fileInfo.file,
                    fileInfo.repoName
js_templates/files.ts:7
Before
7
  const files: SortedFileList = topLevelFilesOnly(branch.fileList, '')
After
7
  const files: SortedFileList = topLevelFilesOnly(Array.from(branch.fileList.keys()), '')
js_templates/raw.ts:1
Before
1
2
3
export default async (eleventyConfig: any) => {
  const getFileContents = eleventyConfig.getFilter("getFileContents")
    return await getFileContents(data.fileInfo.repoName, data.fileInfo.branchName, data.fileInfo.file)
After
1
2
3
import { type Repository } from "../src/dataTypes.ts"
type Branch = Repository['branches'][0]
export default async (eleventyConfig: any) => {
    const branch: Branch = data.currentBranch

    return branch.fileList.get(data.fileInfo.file).fileInfo.contents
js_templates/repo.ts:13
Before
13
  const languageCounts = branch.fileList.reduce((counts, currentFile) => {
After
13
  const languageCounts = Array.from(branch.fileList.keys()).reduce((counts, currentFile) => {
js_templates/repo.ts:28
Before
28
  const total = branch.fileList.length
After
28
  const total = Array.from(branch.fileList.keys()).length
main.ts:7
Before
7
import * as operations from './src/vcses/git/operations.ts'
After
7
main.ts:128
Before
128
    return reposData.find(current => current.name === repo).branches.find(current => current.name === branch).fileList.filter(file => file.startsWith(dirPath) && file !== dirPath)
After
128
    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
    )
main.ts:215
Before
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
  eleventyConfig.addAsyncFilter("getFileLastTouchInfo", async (repo: string, branch: string, filename: string) => {
    const ignoreExtensions = ['.png', '.jpg']
    if (ignoreExtensions.some(extension => filename.endsWith(extension))) {
      return ""
    }

    const location = getLocation(reposConfiguration, eleventyConfig.dir.output, repo)
    return operations.getFileLastTouchInfo(branch, filename, location)
  })

    const isDirectory = files.some((testFile) => {
  eleventyConfig.addAsyncFilter("getFileContents", async (repo: string, branch: string, filename: string) => {
    const location = getLocation(reposConfiguration, eleventyConfig.dir.output, repo)
    const command = `git -C ${location} show ${branch}:${filename}`
    const res = await exec(command)
    return res.stdout
  })
After
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
    const isDirectory = Array.from(files.keys()).some((testFile) => {
main.ts:363
Before
363
After
363
      eleventyComputed: {
        currentBranch: (data) => reposData.find(repo => {
          return repo.name === data.fileInfo.repoName
        }).branches.find(branch => {
          return branch.name === data.fileInfo.branchName
        }),
      }
src/dataTypes.ts:1
Before
1
After
1
import { type BlameInfo } from "./vcses/git/operations.ts"

export type FileInfo = {
  contents: string,
  lastModified: Date,
  blameLines: Array<BlameInfo>,
}
src/dataTypes.ts:10
Before
10
11
    fileList: Array<string>,
    fileList: Array<string>,
After
10
11
    // instead of strings, make it point to a function that returns
    // the file contents. The first time, search in both directions for a commit
    // that has already cached this file before a diff in that commit mentions
    // this filename. If one is found, return the *object* in the cached file
    // map (so that it returns a reference). Also set that in a const inside
    // the function so that we don't have to compute it again.
    // If one is not found, then `cat` the file and save it in the commit
    // cache.
    fileList: Map<string, {fileInfo: FileInfo}>,
    fileList: Map<string, {fileInfo: FileInfo}>,
    // TODO: put cached files map in here. Save file as an object
    // so that it gets saved by reference.
    cachedFiles: Map<string, FileInfo>,
src/flatFiles.ts:1
Before
1
2
3
let cachedFlatFiles = null
export default (repos: Array<Repository>) => {
      return branch.fileList.map((file) => {
After
1
2
3
let cachedFlatFiles: Array<FlatFileEntry> | null = null
export type FlatFileEntry = {
  file: string,
  branchName: string,
  repoName: string,
}
export default (repos: Array<Repository>) : Array<FlatFileEntry> => {
      return Array.from(branch.fileList.keys()).map((file) => {
src/repos.ts:164
Before
164
      tags: [], // todo
After
164
      tags: [], // todo fill in tags, similar to branches
src/vcses/git/operations.ts:2
Before
2
3
import { type Repository } from '../../dataTypes.ts'
export const getFileList = async (branchName: string, repoLocation: string) => {
After
2
3
import { type Repository, type FileInfo } from '../../dataTypes.ts'
export const getFileList = async (
  branchName: string, repoLocation: string
) : Promise<Map<string, {fileInfo: FileInfo}>> => {
src/vcses/git/operations.ts:20
Before
20
  const fileSet: Set<string> = new Set()
After
20
  const filesMap: Map<string, {fileInfo: FileInfo}> = new Map()
src/vcses/git/operations.ts:32
Before
32
33
      fileSet.add(path)
  return Array.from(fileSet)
After
32
33
      filesMap.set(path, {
        get fileInfo() {
          return {
            contents: "",
            lastModified: new Date(),
            blameLines: []
          }
        }
      })
  return filesMap
src/vcses/git/operations.ts:105
Before
105
export const getFileLastTouchInfo = async (branch: string, filename: string, repoLocation: string) => {
After
105
        cachedFiles: new Map(),
export type BlameInfo = {
  sha: string,
  author: string,
}

export const getFileLastTouchInfo = async (
  branch: string, filename: string, repoLocation: string
) : Promise<Array<BlameInfo>> => {
src/vcses/git/operations.ts:132
Before
132
  let authorsAndShasByLine = []
After
132
  let authorsAndShasByLine: Array<BlameInfo> = []