[WIP] get pages un-broken and loading

e3012a590b2a16944db35cbdd14fa48a19490f22

Tucker McKnight <tucker@pangolin.lan> | Sun Feb 22 2026

[WIP] get pages un-broken and loading

Needs code cleanup here. Tags are not displayed, but pages
that expected to have branches available to them now have branches
available.
README.md:35
Before
34
35
36
37
38
39
+       "My Cool Project": {
+         location: "/home/me/code/cool-project",
+         defaultBranch: 'main',
+         branchesToPull: ['main', 'develop', 'release/**'],
+       },
+     },
+     path: "/repos",
After
34
35
36
37
38
39
+       "My Cool Project": {
+         location: "/home/me/code/cool-project",
+         defaultBranch: 'main',
+         branches: ['main', 'develop', 'release/**'],
+       },
+     },
+     path: "/repos",
main.ts:1
Before
0
1
2
3
4
5
import fsImport from 'fs'
import util from 'util'
import childProcess from 'child_process'
import {repos, getBranchNames} from './src/repos.ts'
import branches from './src/branches.ts'
import flatFiles from './src/flatFiles.ts'
import flatPatches from './src/flatPatches.ts'
After
0
1
2
3
4
5
import fsImport from 'fs'
import util from 'util'
import childProcess from 'child_process'
import {repos, getBranchesAndTags} from './src/repos.ts'
import branches from './src/branches.ts'
import flatFiles from './src/flatFiles.ts'
import flatPatches from './src/flatPatches.ts'
main.ts:26
Before
25
26
27
28






29
30
const exec = util.promisify(childProcess.exec)

// TODO document how people need to do this and why. And maybe file 11ty bug?
export function beforeHook(eleventyConfig, reposConfiguration) {
⁣
⁣
⁣
⁣
⁣
⁣
  return async ({directories}) => {
    const cwd = process.cwd()
    const reposPath = reposConfiguration.path || ""
After
25
26
27
28
29
30
31
32
33
34
35
36
const exec = util.promisify(childProcess.exec)

// TODO document how people need to do this and why. And maybe file 11ty bug?
export function beforeHook(eleventyConfig: any, reposConfiguration: ReposConfiguration) {
  const validator = ajv.compile(ConfigSchema)
  const valid = validator(reposConfiguration)
  if (!valid) {
    throw new Error(validator.errors.map(error => `config object at ${error.instancePath.replaceAll("/", ".")}: ${error.message}`).join("\n"))
  }

  return async ({directories}) => {
    const cwd = process.cwd()
    const reposPath = reposConfiguration.path || ""
main.ts:41
Before
40
41
42



43
44
45
        // git repos are just in the repos folder, not in their subdir
        // create string of commands saying 'git fetch origin branch:branch' for each branch
        const location = directories.output + reposPath + "/" + gitRepoName
⁣
⁣
⁣
        const fetchCommands = (await getBranchNames(repoConfig, repoName)).map(branch => `git -C ${location} fetch origin ${branch}:${branch}`).join('; ')
        await exec(`${fetchCommands} && git -C ${location} update-server-info`)
      } else {
        // If it is not there, do git clone
After
40
41
42
43
44
45
46
47
48
        // git repos are just in the repos folder, not in their subdir
        // create string of commands saying 'git fetch origin branch:branch' for each branch
        const location = directories.output + reposPath + "/" + gitRepoName
        const {branches, tags} = await getBranchesAndTags(repoConfig, repoName)
        const branchNames = branches.map(branch => branch.name)
        const tagNames = branches.map(tag => tag.name)
        const fetchCommands = branchNames.concat(tagNames).map(ref => `git -C ${location} fetch origin ${ref}:${ref}`).join('; ')
        await exec(`${fetchCommands} && git -C ${location} update-server-info`)
      } else {
        // If it is not there, do git clone
main.ts:109
Before
108
109
110



111
112
113
          const tempDirRepoPath = `${tempDir}/${eleventyConfig.getFilter("slugify")(repoName)}`
          await exec(`mkdir ${tempDir}`)
          await exec(`git clone -s ${directories.output}${eleventyConfig.getFilter("slugify")(repoName)}.git ${tempDirRepoPath}`)
⁣
⁣
⁣
          for (let branch of await getBranchNames(repoConfig, repoName)) {
            // TODO why doesn't git -C checkout work? Says that repo doesn't exist
            await exec(`(cd ${tempDirRepoPath} && git checkout ${branch})`)
            for (let buildStep of repoConfig.buildSteps) {
After
108
109
110
111
112
113
114
115
116
          const tempDirRepoPath = `${tempDir}/${eleventyConfig.getFilter("slugify")(repoName)}`
          await exec(`mkdir ${tempDir}`)
          await exec(`git clone -s ${directories.output}${eleventyConfig.getFilter("slugify")(repoName)}.git ${tempDirRepoPath}`)
          const branchesAndTags = await getBranchesAndTags(repoConfig, repoName)
          const branchNames = branchesAndTags.branches.map(branch => branch.name)
          const tagNames = branchesAndTags.tags.map(tag => tag.name)
          for (let branch of branchNames) {
            // TODO why doesn't git -C checkout work? Says that repo doesn't exist
            await exec(`(cd ${tempDirRepoPath} && git checkout ${branch})`)
            for (let buildStep of repoConfig.buildSteps) {
main.ts:119
Before
118
119
120









121
122
              await exec(`cp -r ${tempDirRepoPath}/${buildStep.copyFrom} ${directories.output}${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branch)}/${buildStep.copyTo}`)
            }
          }
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
          // delete the temp dirs
          await exec(`rm -r ${tempDir}`)
        }
After
118
119
120
121
122
123
124
125
126
127
128
129
130
131
              await exec(`cp -r ${tempDirRepoPath}/${buildStep.copyFrom} ${directories.output}${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branch)}/${buildStep.copyTo}`)
            }
          }
          for (let tag of tagNames) {
            await exec(`(cd ${tempDirRepoPath} && git checkout ${tag})`)
            for (let buildStep of repoConfig.buildSteps) {
              // Run the command for each step in each branch
              await exec(`(cd ${tempDirRepoPath} && ${buildStep.command})`)
              // Copy the specified folders from the "from" to the "to" dir
              await exec(`cp -r ${tempDirRepoPath}/${buildStep.copyFrom} ${directories.output}${eleventyConfig.getFilter("slugify")(repoName)}/tags/${eleventyConfig.getFilter("slugify")(tag)}/${buildStep.copyTo}`)
            }
          }
          // delete the temp dirs
          await exec(`rm -r ${tempDir}`)
        }
schemas/ReposConfiguration.json:5
Before
4
5
6
7
8
9
    "GitConfig": {
      "additionalProperties": false,
      "properties": {
        "branchesToPull": {
          "items": {
            "anyOf": [
              {
After
4
5
6
7
8
9
    "GitConfig": {
      "additionalProperties": false,
      "properties": {
        "branches": {
          "items": {
            "anyOf": [
              {
schemas/ReposConfiguration.json:114
Before
113
114
115
116
117
118
119
120
121
122
123
124
125
126
              {
                "additionalProperties": false,
                "properties": {
                  "glob": {
                    "type": "string"
                  },
                  "max": {
                    "type": "number"
                  }
                },
                "required": [
                  "glob",
                  "max"
                ],
                "type": "object"
After
113
114
115
116
117
118
119
120
121
122
123
124
125
126
              {
                "additionalProperties": false,
                "properties": {
                  "max": {
                    "type": "number"
                  },
                  "pattern": {
                    "type": "string"
                  }
                },
                "required": [
                  "pattern",
                  "max"
                ],
                "type": "object"
schemas/ReposConfiguration.json:135
Before
134
135
136
137
138
139
      "required": [
        "location",
        "defaultBranch",
        "branchesToPull"
      ],
      "type": "object"
    },
After
134
135
136
137
138
139
      "required": [
        "location",
        "defaultBranch",
        "branches"
      ],
      "type": "object"
    },
src/configTypes.ts:11
Before
10
11
12
13
14
15
 *   repos: {
 *     "My Git Project": {
 *       defaultBranch: 'main',
 *       branchesToPull: ['main', 'develop']
 *     },
 *   },
 * }
After
10
11
12
13
14
15
 *   repos: {
 *     "My Git Project": {
 *       defaultBranch: 'main',
 *       branches: ['main', 'develop']
 *     },
 *   },
 * }
src/configTypes.ts:53
Before
52
53
54
55

56
57
58
  location: string,
  description?: string,
  defaultBranch: string,
  branchesToPull: Array<string | {pattern: string, max?: number, compareTo?: string, description?: string}>,
⁣
  tags?: Array<string | {glob: string, max: number}>,
  languageExtensions?: {
    [fileExtension: string]: string
  },
After
52
53
54
55
56
57
58
59
  location: string,
  description?: string,
  defaultBranch: string,
  branches: Array<string | {pattern: string, max?: number, compareTo?: string, description?: string}>,
  // todo: make tags optional (and branches, too?) and auto-populate with ** if it's not filled in.
  tags: Array<string | {pattern: string, max: number}>,
  languageExtensions?: {
    [fileExtension: string]: string
  },
src/repos.ts:14
Before
13
14
15
16
17







18
19

20
21
22
23
24
25
26

27
28
29
30
31
32
33
34
35

const exec = util.promisify(childProcess.exec)

const branchesForReposMap: Map<string, Array<{name: string, description?: string, compareTO?: string}>> = new Map()

⁣
⁣
⁣
⁣
⁣
⁣
⁣
const getBranches = async (repoConfig: GitConfig, repoName: string): Promise<Array<{name: string, description?: string, compareTo?: string}>> => {
  const cachedBranchNames = branchesForReposMap.get(repoName)
⁣
  if (cachedBranchNames !== undefined) {
    return cachedBranchNames
  }

  // Get all branches available in the repository
  const allBranches = (await exec(`git -C ${repoConfig.location} branch --format="%(refname:short)"`)).stdout.split("\n").filter(branch => branch !== '')

⁣
  // Sort the list of branch descriptions from branchesToPull by the length
  // of their patterns.
  // Then, for each branch from the repository, see if it matches a pattern.
  // If that pattern has rules associated with it (like a description or a max),
  // apply those.
  type RulesObject = {max?: number, description?: string, compareTo?: string}
  let branchRules: Array<{pattern: string, matches: Array<string>, rules: RulesObject}> = repoConfig.branchesToPull.map((branchDescription) => {
    const rules: RulesObject = {}

    if (typeof branchDescription !== 'string') {
After
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

const exec = util.promisify(childProcess.exec)

const branchesForReposMap: Map<string, Array<{name: string, description?: string, compareTo?: string}>> = new Map()
const tagsForReposMap: Map<string, Array<{name: string}>> = new Map()

const getBranchesAndTags = async (
  repoConfig: GitConfig,
  repoName: string
): Promise<{
  branches: Array<{name: string, description?: string, compareTo?: string}>,
  tags: Array<{name: string}>,
}> => {
  const cachedBranchNames = branchesForReposMap.get(repoName)
  const cachedTagNames = tagsForReposMap.get(repoName)
  if (cachedBranchNames !== undefined) {
    return {branches: cachedBranchNames, tags: cachedTagNames}
  }

  // Get all branches and tags available in the repository
  const allBranches = (await exec(`git -C ${repoConfig.location} branch --format="%(refname:short)"`)).stdout.split("\n").filter(branch => branch !== '')
  const allTags = (await exec(`git -C ${repoConfig.location} tag`)).stdout.split("\n").filter(tag => tag !== '')

  // Sort the list of branch descriptions from `branches` by the length
  // of their patterns.
  // Then, for each branch from the repository, see if it matches a pattern.
  // If that pattern has rules associated with it (like a description or a max),
  // apply those.
  type RulesObject = {max?: number, description?: string, compareTo?: string}
  let branchRules: Array<{pattern: string, matches: Array<string>, rules: RulesObject}> = repoConfig.branches.map((branchDescription) => {
    const rules: RulesObject = {}

    if (typeof branchDescription !== 'string') {
src/repos.ts:47
Before
46
47
48














49
50

51
    }
  })

⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
  branchRules.sort((a, b) => b.pattern.length - a.pattern.length)

⁣
  allBranches.forEach((branchName) => {
    const matchingPatternIndex = branchRules.findIndex((branchRule) => {
After
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
    }
  })

  let tagRules: Array<{pattern: string, matches: Array<string>, rules: RulesObject}> = repoConfig.tags.map((tagDescription) => {
    const rules: RulesObject = {}

    if (typeof tagDescription !== 'string') {
      if (tagDescription.max) { rules.max = tagDescription.max }
    }

    return {
      pattern: (typeof tagDescription === 'string' ? tagDescription : tagDescription.pattern),
      matches: [],
      rules,
    }
  })

  branchRules.sort((a, b) => b.pattern.length - a.pattern.length)
  tagRules.sort((a, b) => b.pattern.length - a.pattern.length)

  allBranches.forEach((branchName) => {
    const matchingPatternIndex = branchRules.findIndex((branchRule) => {
src/repos.ts:66
Before
65
66
67

















68
69
    }
  })

⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
  const branches: Array<{
    name: string,
    description?: string,
After
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
    }
  })

  allTags.forEach((tagName) => {
    const matchingPatternIndex = tagRules.findIndex((tagRule) => {
      return minimatch(tagName, tagRule.pattern)
    })

    if (matchingPatternIndex === -1) { return }

    const matchedRule = tagRules[matchingPatternIndex]
    matchedRule.matches.push(tagName)
    if (
      matchedRule.rules?.max
      && matchedRule.rules?.max < matchedRule.matches.length
    ) {
      matchedRule.matches.pop()
    }
  })

  const branches: Array<{
    name: string,
    description?: string,
src/repos.ts:79
Before
78
79
80







81
82
83
84
85
86
87

88

89
90
91
    }).flat()
  }).flat()

⁣
⁣
⁣
⁣
⁣
⁣
⁣
  if (branchesForReposMap.get(repoName) === undefined) {
    branchesForReposMap.set(repoName, branches)
  }

  return branches
}

⁣
const getBranchNames = async (repoConfig, repoName) => {
⁣
  return (await getBranches(repoConfig, repoName)).map(branch => branch.name)
}

let cachedRepos: Array<Repository> | null = null
After
78
79
80
81
82
83
84
85
86
87
88
89
90




91
92
93
94
95
96
    }).flat()
  }).flat()

  const tags: Array<{ name: string }> = tagRules.map((tagRule) => {
    return tagRule.matches.map((match) => {
      const result = {name: match}
      return result
    }).flat()
  }).flat()

  if (branchesForReposMap.get(repoName) === undefined) {
    branchesForReposMap.set(repoName, branches)
  }
⁣
⁣
⁣
⁣
  if (tagsForReposMap.get(repoName) === undefined) {
    tagsForReposMap.set(repoName, tags)
  }
  return { branches, tags }
}

let cachedRepos: Array<Repository> | null = null
src/repos.ts:100
Before
99
100
101
102
103


104
105
106
107
108
109
110

  for (const repoName of repoNames) {
    const repoLocation = getLocation(reposConfig, outputDir, repoName)
    const branchesToAdd = await getBranches(reposConfig.repos[repoName], repoName)
    const commits: Repository['commits'] = new Map()
⁣
⁣
    for (const branchName of await getBranchNames(reposConfig.repos[repoName], repoName)) {
      await addBranchToCommitsMap(branchName, repoLocation, commits)
    }

    const branches = await Promise.all(branchesToAdd.map(async (branch) => {
      const repoLocation = getLocation(reposConfig, outputDir, repoName)
      const branchHeadRes = await exec(`git -C ${repoLocation} show-ref refs/heads/${branch.name}`)
      const branchHead = branchHeadRes.stdout.split(" ")[0]
After
99
100
101

102
103
104
105
106
107
108
109
110
111

  for (const repoName of repoNames) {
    const repoLocation = getLocation(reposConfig, outputDir, repoName)
⁣
    const commits: Repository['commits'] = new Map()
    const branchesAndTags = await getBranchesAndTags(reposConfig.repos[repoName], repoName)
    const branchNames = branchesAndTags.branches.map(branch => branch.name)
    for (const branchName of branchNames) {
      await addBranchToCommitsMap(branchName, repoLocation, commits)
    }

    const branches = await Promise.all(branchesAndTags.branches.map(async (branch) => {
      const repoLocation = getLocation(reposConfig, outputDir, repoName)
      const branchHeadRes = await exec(`git -C ${repoLocation} show-ref refs/heads/${branch.name}`)
      const branchHead = branchHeadRes.stdout.split(" ")[0]
src/repos.ts:122
Before
121
122
123
124
125
126
    }))

    const branchesWithCompareToInfo: Repository['branches'] = branches.map((branch) => {
      const branchDescription = branchesToAdd.find(branchToAdd => branchToAdd.name === branch.name)
      const compareTo = branchDescription.compareTo || reposConfig.repos[repoName].defaultBranch
      const compareToBranch = branches.find((test) => test.name === compareTo)
After
121
122
123
124
125
126
    }))

    const branchesWithCompareToInfo: Repository['branches'] = branches.map((branch) => {
      const branchDescription = branchesAndTags.branches.find(branchToAdd => branchToAdd.name === branch.name)
      const compareTo = branchDescription.compareTo || reposConfig.repos[repoName].defaultBranch
      const compareToBranch = branches.find((test) => test.name === compareTo)
src/repos.ts:172
Before
171
172
173
  return cachedRepos
}

export {repos, getBranchNames}
After
171
172
173
  return cachedRepos
}

export {repos, getBranchesAndTags}