Tucker McKnight <tmcknight@instructure.com> | Mon Jan 19 2026
Allow all branch names to be glob patterns Change the way that branches are matched to those patterns so that, if multiple patterns match a branch name, the most specific pattern is the one that is used. Also allows new fields on a branch: description and compareTo. Prints out the branch description on the branches page. Also makes the max value work -- kicks out branches from the list if there are already the max number of branches in the list. TODO: need to make sure this only kicks out the branches that are the oldest (that is, not recently committed to).
18 19 20 21 22 23
${branch.branchName === data.branchInfo.branchName ? '<div class="badge rounded-pill bg-secondary mx-1">current</div>' : ''}
${branch.branchName === data.reposConfig.repos[branch.repoName].defaultBranch ? '<div class="badge rounded-pill bg-info text-dark mx-1">default</div>' : ''}
</div>
<div class="card-body">Data about branch goes here</div>
</div>
`
: ''18 19 20 21 22 23
${branch.branchName === data.branchInfo.branchName ? '<div class="badge rounded-pill bg-secondary mx-1">current</div>' : ''}
${branch.branchName === data.reposConfig.repos[branch.repoName].defaultBranch ? '<div class="badge rounded-pill bg-info text-dark mx-1">default</div>' : ''}
</div>
<div class="card-body">${branch.description || ''}</div>
</div>
`
: ''13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
{
"additionalProperties": false,
"properties": {
"glob": {
"type": "string"
},
"max": {
"type": "number"
}
},
"required": [
"glob",
"max"
],
"type": "object"
}13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
{
"additionalProperties": false,
"properties": {
"compareTo": {
"type": "string"
},
"description": {
"type": "string"
},
"max": {
"type": "number"
},
"pattern": {
"type": "string"
}
},
"required": [
"pattern"
],
"type": "object"
}9 10 11 12 13 14 15 16 17
cachedBranches = repos.flatMap((repo) => {
return repo.branches.map((branch) => {
return {
branchName: branch.name,
repoName: repo.name,
}
})
})
9 10 11 12 13 14 15 16 17 18 19 20
cachedBranches = repos.flatMap((repo) => {
return repo.branches.map((branch) => {
const result = {
branchName: branch.name,
repoName: repo.name,
}
if (branch.description) { result['description'] = branch.description }
if (branch.compareTo) { result['compareTo'] = branch.compareTo }
return result
})
})
10 11 12 13 14 15
* repos: {
* "My Git Project": {
* defaultBranch: 'main',
* branches: ['main', 'develop']
* },
* },
* }10 11 12 13 14 15
* repos: {
* "My Git Project": {
* defaultBranch: 'main',
* branchesToPull: ['main', 'develop']
* },
* },
* }52 53 54 55 56 57
location: string,
description?: string,
defaultBranch: string,
branchesToPull: Array<string | {glob: string, max: number}>,
tags?: Array<string | {glob: string, max: number}>,
languageExtensions?: {
[fileExtension: string]: string52 53 54 55 56 57
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]: string4 5 6 7 8
defaultBranch: string,
branches: Array<{
name: string,
head: string,
fileList: Array<string>,
}>,4 5 6 7 8 9 10
defaultBranch: string,
branches: Array<{
name: string,
description?: string,
compareTo?: string,
head: string,
fileList: Array<string>,
}>,13 14 15 16 17 18 19 20
const exec = util.promisify(childProcess.exec)
const branchesForReposMap: Map<string, string[]> = new Map()
const getBranchNames = async (repoConfig: GitConfig, repoName: string): Promise<Array<string>> => {
const cachedBranchNames = branchesForReposMap.get(repoName)
if (cachedBranchNames !== undefined) {
return cachedBranchNames13 14 15 16 17 18 19 20
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 cachedBranchNames23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
// Get all branches available in the repository
const allBranches = (await exec(`git -C ${repoConfig.location} branch --format="%(refname:short)"`)).stdout.split("\n")
const matchingBranchesFromGlobs: Map<string, string[]> = new Map()
const literalBranchNames: string[] = []
repoConfig.branchesToPull.forEach((branch) => {
if (typeof branch === "string") {
literalBranchNames.push(branch)
}
else {
// If we're here, then the "branch" was an object like:
// { glob: string, max: number }
// TODO: figure out which branch is the newest/oldest, and
// keep them less than `max` by kicking out the oldest from the list.
const glob = branch['glob']
const matching = allBranches.filter((possibleBranch) => {
const match = path.matchesGlob(possibleBranch, glob)
return match
})
matchingBranchesFromGlobs.set(
glob,
(matchingBranchesFromGlobs.get(glob) || []).concat(matching)
)
}
})
const matchedBranchesFromGlobs = Array.from(matchingBranchesFromGlobs.values()).flat()
const branchNames: string[] = Array.from(new Set(matchedBranchesFromGlobs.concat(literalBranchNames)))
if (branchesForReposMap.get(repoName) === undefined) {
branchesForReposMap.set(repoName, branchNames)
}
return branchNames
}
let cachedRepos: Array<Repository> | null = null23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
// Get all branches available in the repository
const allBranches = (await exec(`git -C ${repoConfig.location} branch --format="%(refname:short)"`)).stdout.split("\n")
// 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') {
if (branchDescription.max) { rules.max = branchDescription.max }
if (branchDescription.description) { rules.description = branchDescription.description }
if (branchDescription.compareTo) { rules.compareTo = branchDescription.compareTo }
}
return {
pattern: (typeof branchDescription === 'string' ? branchDescription : branchDescription.pattern),
matches: [],
rules,
}
})
branchRules = branchRules.toSorted((a, b) => b.pattern.length - a.pattern.length)
allBranches.forEach((branchName) => {
const matchingPatternIndex = branchRules.findIndex((branchRule) => {
return path.matchesGlob(branchName, branchRule.pattern)
})
if (matchingPatternIndex === -1) { return }
const matchedRule = branchRules[matchingPatternIndex]
matchedRule.matches.push(branchName)
if (
matchedRule.rules?.max
&& matchedRule.rules?.max < matchedRule.matches.length
) {
matchedRule.matches.pop()
}
})
const branches: Array<{
name: string,
description?: string,
compareTo?: string,
}> = branchRules.map((branchRule) => {
return branchRule.matches.map((match) => {
const result = {name: match}
if (branchRule.rules.description) { result['description'] = branchRule.rules.description }
if (branchRule.rules.compareTo) { result['compareTo'] = branchRule.rules.compareTo }
return result
}).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 = null67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
for (const repoName of repoNames) {
const repoLocation = getLocation(reposConfig, outputDir, repoName)
const branchNames = await getBranchNames(reposConfig.repos[repoName], repoName)
const commits: Repository['commits'] = new Map()
for (const branchName of branchNames) {
await addBranchToCommitsMap(branchName, repoLocation, commits)
}
const branches = await Promise.all(branchNames.map(async (branchName) => {
const repoLocation = getLocation(reposConfig, outputDir, repoName)
const branchHeadRes = await exec(`git -C ${repoLocation} show-ref refs/heads/${branchName}`)
const branchHead = branchHeadRes.stdout.split(" ")[0]
return {
name: branchName,
head: branchHead,
fileList: await getFileList(branchName, repoLocation)
}
}))
cachedRepos.push({67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
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]
const result = {
name: branch.name,
head: branchHead,
fileList: await getFileList(branch.name, repoLocation)
}
if (branch.description) { result['description'] = branch.description }
if (branch.compareTo) { result['compareTo'] = branch.compareTo }
return result
}))
cachedRepos.push({41 42
- this is a good reason to put the deployed repo config on the public site, too.
I currently have different example site configs between my laptop and the deployed
site.41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
- this is a good reason to put the deployed repo config on the public site, too.
I currently have different example site configs between my laptop and the deployed
site.
## Jan 18, 2026
*An idea:* allow the object with a glob in it to also specify things like the description
and which parent branch the branch should be compared to.
Multiple branch patterns (let's use the word "pattern" instead of "glob") can match
the same branch -- the more specific one should be used.
E.g.:
```
branchesToPull: [
{
pattern: "deploy/**",
description: "A deployed branch",
},
{
pattern: "deploy/v2**",
description: "A version 2 deployed branch",
}
]
```
In the above example, branch `deploy/v1.0.1` should have the description "A deployed
branch," but branch `deploy/v2.0.1` should have the description "A version 2
deployed branch."
Is the "more specific pattern" just always the one that is longer? Try this for now.