import type { Arrayable, Nullable } from "@libs/typing"
import { command } from "@libs/run/command"
export type { Arrayable, Nullable }
export function log(sha: string, { stdout = "", ...options } = {} as LogOptions): LogEntry[] {
if (!stdout) {
;({ stdout } = command("git", ["log", "--pretty=<<%H>> <<%at>> <<%an>> %s", `${sha}..${options?.head ?? ""}`], { sync: true, throw: true }))
}
let entries = []
for (const line of stdout.trim().split("\n").filter(Boolean)) {
try {
const { sha, author, time, summary } = line.match(/^<<(?<sha>.{40})>> <<(?<time>\d+)>> <<(?<author>.*)>> (?<summary>.*)$/)!.groups!
const match = summary.match(/^(?<type>[^\(\):]+)(?:\((?<scopes>[^\(\):]+)\))?(?<breaking>!?):\s+(?<subject>[\s\S]*)/)?.groups
entries.push({
sha,
author,
time: Number(time),
conventional: !!match,
type: match?.type ?? null,
scopes: match?.scopes?.split(",").map((scope) => scope.trim()) ?? [],
breaking: match ? !!match?.breaking : null,
subject: match?.subject ?? summary,
summary,
})
} catch (error) {
throw new TypeError(`Failed to parse git log entry: "${line}" (${error})`)
}
}
if (options?.filter?.conventional) {
entries = entries.filter(({ conventional }) => conventional)
}
if (options?.filter?.breaking) {
entries = entries.filter(({ breaking }) => breaking)
}
if (options?.filter?.types) {
entries = entries.filter(({ type }) => [options?.filter?.types].filter(Boolean).flat().includes(type as string))
}
if (options?.filter?.scopes?.length) {
entries = entries.filter(({ scopes }) => scopes.some((scope) => [options?.filter?.scopes].filter(Boolean).flat().includes(scope)))
}
return entries
}
export type LogOptions = {
stdout?: string
head?: string
filter?: {
conventional?: boolean
types?: Arrayable<string>
scopes?: Arrayable<string>
breaking?: boolean
}
}
export type LogEntry = {
sha: string
author: string
time: number
conventional: boolean
type: Nullable<string>
scopes: string[]
breaking: Nullable<boolean>
subject: string
summary: string
}
|