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.
34 35 36 37 38 39
+ "My Cool Project": {
+ location: "/home/me/code/cool-project",
+ defaultBranch: 'main',
+ branchesToPull: ['main', 'develop', 'release/**'],
+ },
+ },
+ path: "/repos",34 35 36 37 38 39
+ "My Cool Project": {
+ location: "/home/me/code/cool-project",
+ defaultBranch: 'main',
+ branches: ['main', 'develop', 'release/**'],
+ },
+ },
+ path: "/repos",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'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'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 || ""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 || ""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 clone40 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 clone108 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) {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) {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}`)
}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}`)
}4 5 6 7 8 9
"GitConfig": {
"additionalProperties": false,
"properties": {
"branchesToPull": {
"items": {
"anyOf": [
{4 5 6 7 8 9
"GitConfig": {
"additionalProperties": false,
"properties": {
"branches": {
"items": {
"anyOf": [
{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"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"134 135 136 137 138 139
"required": [
"location",
"defaultBranch",
"branchesToPull"
],
"type": "object"
},134 135 136 137 138 139
"required": [
"location",
"defaultBranch",
"branches"
],
"type": "object"
},10 11 12 13 14 15
* repos: {
* "My Git Project": {
* defaultBranch: 'main',
* branchesToPull: ['main', 'develop']
* },
* },
* }10 11 12 13 14 15
* repos: {
* "My Git Project": {
* defaultBranch: 'main',
* branches: ['main', 'develop']
* },
* },
* }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
},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
},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') {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') {46 47 48 49 50 51
}
})
branchRules.sort((a, b) => b.pattern.length - a.pattern.length)
allBranches.forEach((branchName) => {
const matchingPatternIndex = branchRules.findIndex((branchRule) => {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) => {65 66 67 68 69
}
})
const branches: Array<{
name: string,
description?: string,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,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 = null78 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 = null99 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]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]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)
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)
171 172 173
return cachedRepos
}
export {repos, getBranchNames}171 172 173
return cachedRepos
}
export {repos, getBranchesAndTags}