[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
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")
After
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")
js_templates/file.ts:14
Before
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)
        ])
      )
    ),
After
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)
        ])
      )
    ),
js_templates/file.ts:67
Before
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"},
After
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"},
js_templates/file.ts:91
Before
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"},
After
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"},
js_templates/file.ts:114
Before
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')
                  )
After
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')
                  )
js_templates/files.ts:7
Before
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"}, [
After
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"}, [
js_templates/raw.ts:1
Before


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)
  }
}
After
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
  }
}
js_templates/repo.ts:13
Before
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 files
After
12
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 files
js_templates/repo.ts:28
Before
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 = branch.fileList.length

  for (const entry of languageCounts) {
    languagePercentages.push([entry[0], entry[1] / total])
After
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])
main.ts:7
Before
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'
After
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'
main.ts:128
Before
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) => {
After
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) => {
main.ts:215
Before
After
main.ts:363
Before
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('.')}`
      },
⁣
⁣
⁣
⁣
⁣
⁣
⁣
    }
  )
After
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
        }),
      }
    }
  )
src/dataTypes.ts:1
Before







0
1
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
export type Repository = {
  name: string,
  description?: string,
After
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,
src/flatFiles.ts:1
Before
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,
After
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,
src/repos.ts:164
Before
163
164
165
166
167
168
      branches: branchesWithCompareToInfo,
      cloneUrl: cloneUrl.cloneUrl(reposConfig.baseUrl, repoName),
      defaultBranch: reposConfig.repos[repoName].defaultBranch,
      tags: [], // todo
      commits,
    })
  }
After
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,
    })
  }
src/vcses/git/operations.ts:2
Before
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)
After
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)
src/vcses/git/operations.ts:20
Before
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) => {
After
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) => {
src/vcses/git/operations.ts:32
Before
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> => {
After
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> => {
src/vcses/git/operations.ts:105
Before
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)
After
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)
src/vcses/git/operations.ts:132
Before
131
132
133
134
135
136
  }, initialValue)

  let currentAuthor = ''
  let authorsAndShasByLine = []
  chunked.forEach((chunk) => {
    let shaAndLineNumberParts = chunk[0].split(' ')
    let line = parseInt(shaAndLineNumberParts[2])
After
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])