publish(task 13): git-diff change-detection für post-ordner
filterPostDirs(lines, contentRoot) extrahiert post-verzeichnisse aus git-diff-ausgabe (index.md-matches + asset-changes), ignoriert _drafts/. contentRoot ist parameter (blaupausen-tauglich für nicht- hugo-layouts). changedPostDirs(from, to, contentRoot, runner?) ruft "git diff --name-only A..B" via dependency-injected runner. Plus allPostDirs() für den --force-all-modus. 4 tests grün. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
02a955c46f
commit
b6196f1052
|
|
@ -0,0 +1,67 @@
|
|||
export type GitRunner = (args: string[]) => Promise<string>
|
||||
|
||||
function escapeRegex(s: string): string {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
export function filterPostDirs(lines: string[], contentRoot: string): string[] {
|
||||
const root = contentRoot.replace(/\/$/, '')
|
||||
const prefix = root + '/'
|
||||
const indexRe = new RegExp(`^${escapeRegex(prefix)}([^/]+)/index\\.md$`)
|
||||
const assetRe = new RegExp(`^${escapeRegex(prefix)}([^/]+)/`)
|
||||
const drafts = prefix + '_'
|
||||
const dirs = new Set<string>()
|
||||
for (const line of lines) {
|
||||
const l = line.trim()
|
||||
if (!l) continue
|
||||
if (l.startsWith(drafts)) continue
|
||||
const indexMatch = l.match(indexRe)
|
||||
if (indexMatch) {
|
||||
dirs.add(`${prefix}${indexMatch[1]}`)
|
||||
continue
|
||||
}
|
||||
const assetMatch = l.match(assetRe)
|
||||
if (assetMatch && !l.endsWith('.md')) {
|
||||
dirs.add(`${prefix}${assetMatch[1]}`)
|
||||
}
|
||||
}
|
||||
return [...dirs].sort()
|
||||
}
|
||||
|
||||
const defaultRunner: GitRunner = async (args) => {
|
||||
const proc = new Deno.Command('git', { args, stdout: 'piped', stderr: 'piped' })
|
||||
const out = await proc.output()
|
||||
if (out.code !== 0) {
|
||||
throw new Error(`git ${args.join(' ')} failed: ${new TextDecoder().decode(out.stderr)}`)
|
||||
}
|
||||
return new TextDecoder().decode(out.stdout)
|
||||
}
|
||||
|
||||
export interface DiffArgs {
|
||||
from: string
|
||||
to: string
|
||||
contentRoot: string
|
||||
runner?: GitRunner
|
||||
}
|
||||
|
||||
export async function changedPostDirs(args: DiffArgs): Promise<string[]> {
|
||||
const runner = args.runner ?? defaultRunner
|
||||
const stdout = await runner(['diff', '--name-only', `${args.from}..${args.to}`])
|
||||
return filterPostDirs(stdout.split('\n'), args.contentRoot)
|
||||
}
|
||||
|
||||
export async function allPostDirs(contentRoot: string): Promise<string[]> {
|
||||
const result: string[] = []
|
||||
for await (const entry of Deno.readDir(contentRoot)) {
|
||||
if (entry.isDirectory && !entry.name.startsWith('_')) {
|
||||
const indexPath = `${contentRoot}/${entry.name}/index.md`
|
||||
try {
|
||||
const stat = await Deno.stat(indexPath)
|
||||
if (stat.isFile) result.push(`${contentRoot}/${entry.name}`)
|
||||
} catch {
|
||||
// skip folders without index.md
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.sort()
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { assertEquals } from '@std/assert'
|
||||
import {
|
||||
changedPostDirs,
|
||||
filterPostDirs,
|
||||
type GitRunner,
|
||||
} from '../src/core/change-detection.ts'
|
||||
|
||||
Deno.test('filterPostDirs: extrahiert post-ordner aus dateipfaden (content/posts)', () => {
|
||||
const lines = [
|
||||
'content/posts/a/index.md',
|
||||
'content/posts/b/image.png',
|
||||
'content/posts/c/other.md',
|
||||
'README.md',
|
||||
'app/src/lib/x.ts',
|
||||
]
|
||||
assertEquals(
|
||||
filterPostDirs(lines, 'content/posts').sort(),
|
||||
['content/posts/a', 'content/posts/b'],
|
||||
)
|
||||
})
|
||||
|
||||
Deno.test('filterPostDirs: respektiert alternativen root (blog/)', () => {
|
||||
const lines = [
|
||||
'blog/x/index.md',
|
||||
'blog/y/pic.png',
|
||||
'content/posts/z/index.md',
|
||||
'README.md',
|
||||
]
|
||||
assertEquals(filterPostDirs(lines, 'blog').sort(), ['blog/x', 'blog/y'])
|
||||
})
|
||||
|
||||
Deno.test('filterPostDirs: ignoriert _drafts und non-index.md', () => {
|
||||
const lines = [
|
||||
'content/posts/a/index.md',
|
||||
'content/posts/a/extra.md',
|
||||
'content/posts/_drafts/x/index.md',
|
||||
]
|
||||
assertEquals(filterPostDirs(lines, 'content/posts'), ['content/posts/a'])
|
||||
})
|
||||
|
||||
Deno.test('changedPostDirs: nutzt git diff --name-only A..B', async () => {
|
||||
const runner: GitRunner = (args) => {
|
||||
assertEquals(args[0], 'diff')
|
||||
assertEquals(args[1], '--name-only')
|
||||
assertEquals(args[2], 'HEAD~1..HEAD')
|
||||
return Promise.resolve('content/posts/x/index.md\nREADME.md\n')
|
||||
}
|
||||
const dirs = await changedPostDirs({
|
||||
from: 'HEAD~1',
|
||||
to: 'HEAD',
|
||||
contentRoot: 'content/posts',
|
||||
runner,
|
||||
})
|
||||
assertEquals(dirs, ['content/posts/x'])
|
||||
})
|
||||
Loading…
Reference in New Issue