@reelout/core 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/dist/analyzer.js +38 -0
  2. package/dist/analyzer.js.map +1 -0
  3. package/dist/builder.js +52 -0
  4. package/dist/builder.js.map +1 -0
  5. package/dist/changelog.js +58 -0
  6. package/dist/changelog.js.map +1 -0
  7. package/dist/formatError-BXNdyRfu.js +23 -0
  8. package/dist/formatError-BXNdyRfu.js.map +1 -0
  9. package/dist/index.js +175 -1
  10. package/dist/index.js.map +1 -0
  11. package/dist/types-la_KkjCS.js +1 -0
  12. package/dist/types.js +2 -0
  13. package/dist/workflow-BoVmbvH6.js +2342 -0
  14. package/dist/workflow-BoVmbvH6.js.map +1 -0
  15. package/package.json +27 -3
  16. package/src/analyzer.ts +63 -0
  17. package/src/builder.ts +104 -0
  18. package/src/cache/TTLCache.ts +100 -0
  19. package/src/cache/index.ts +4 -0
  20. package/src/changelog.ts +97 -0
  21. package/src/config/const.ts +97 -0
  22. package/src/config/index.ts +11 -0
  23. package/src/config/isSemVerGitTag.ts +25 -0
  24. package/src/config/normalizeCommitFilter.ts +37 -0
  25. package/src/config/normalizeReleaseRule.ts +42 -0
  26. package/src/config/normalizeWorkspace.ts +34 -0
  27. package/src/config/resolvePlugin.ts +56 -0
  28. package/src/config/resolveReleaseTarget.ts +71 -0
  29. package/src/config/sanitizePrereleaseLabel.ts +18 -0
  30. package/src/git/branch/getCurrentGitBranch.ts +18 -0
  31. package/src/git/branch/index.ts +4 -0
  32. package/src/git/commit/createGitCommit.ts +29 -0
  33. package/src/git/commit/extractMentions.ts +34 -0
  34. package/src/git/commit/getCurrentGitCommit.ts +14 -0
  35. package/src/git/commit/getParsedCommit.ts +53 -0
  36. package/src/git/commit/index.ts +9 -0
  37. package/src/git/commit/parseCommit.ts +99 -0
  38. package/src/git/commit/parseGitPrettyFormat.ts +76 -0
  39. package/src/git/history/countGitHistory.ts +28 -0
  40. package/src/git/history/expandGitHistory.ts +108 -0
  41. package/src/git/history/index.ts +9 -0
  42. package/src/git/history/mergeGitHistory.ts +76 -0
  43. package/src/git/history/restoreFullGitHistory.ts +19 -0
  44. package/src/git/history/showGitHistory.ts +80 -0
  45. package/src/git/history/showParsedGitHistory.ts +30 -0
  46. package/src/git/index.ts +9 -0
  47. package/src/git/repository/ensureGitUserConfig.ts +31 -0
  48. package/src/git/repository/getGitConfigValue.ts +27 -0
  49. package/src/git/repository/getGitRemote.ts +20 -0
  50. package/src/git/repository/index.ts +10 -0
  51. package/src/git/repository/isGitShallowRepository.ts +21 -0
  52. package/src/git/repository/pushGitChanges.ts +25 -0
  53. package/src/git/repository/resolveGitRepository.ts +40 -0
  54. package/src/git/repository/withShallowLock.ts +62 -0
  55. package/src/git/revision/detectGitRelationship.ts +43 -0
  56. package/src/git/revision/index.ts +7 -0
  57. package/src/git/revision/isGitAncestor.ts +33 -0
  58. package/src/git/revision/readGitFile.ts +29 -0
  59. package/src/git/revision/resolveGitRevision.ts +23 -0
  60. package/src/git/tag/createGitTag.ts +25 -0
  61. package/src/git/tag/deleteGitTag.ts +17 -0
  62. package/src/git/tag/ensureGitTag.ts +63 -0
  63. package/src/git/tag/generateGitTagName.ts +43 -0
  64. package/src/git/tag/getCurrentGitTag.ts +23 -0
  65. package/src/git/tag/getGitTagMessage.ts +35 -0
  66. package/src/git/tag/getLatestGitTag.ts +22 -0
  67. package/src/git/tag/index.ts +13 -0
  68. package/src/git/tag/isGitTagConnected.ts +33 -0
  69. package/src/git/tag/isGitTagPointed.ts +38 -0
  70. package/src/git/tag/isGitTagPresent.ts +17 -0
  71. package/src/index.ts +8 -2
  72. package/src/types.ts +217 -0
  73. package/src/util/buildTrailerPattern.ts +20 -0
  74. package/src/util/compact.ts +14 -0
  75. package/src/util/execCommand.ts +130 -0
  76. package/src/util/execGit.ts +18 -0
  77. package/src/util/formatError.ts +21 -0
  78. package/src/util/formatFile.ts +41 -0
  79. package/src/util/index.ts +16 -0
  80. package/src/util/interpolate.ts +141 -0
  81. package/src/util/isCI.ts +23 -0
  82. package/src/util/isDefined.ts +12 -0
  83. package/src/util/isMatch.ts +43 -0
  84. package/src/util/readJson.ts +47 -0
  85. package/src/util/unique.ts +26 -0
  86. package/src/util/writeJson.ts +28 -0
  87. package/src/workflow/analysis/analyzePackageChanges.ts +54 -0
  88. package/src/workflow/analysis/index.ts +9 -0
  89. package/src/workflow/analysis/isCommitForPackage.ts +23 -0
  90. package/src/workflow/analysis/promotePrereleaseVersions.ts +50 -0
  91. package/src/workflow/analysis/propagateDependencyUpdates.ts +71 -0
  92. package/src/workflow/analysis/sliceUnreleased.ts +36 -0
  93. package/src/workflow/analysis/streamPackageCommits.ts +34 -0
  94. package/src/workflow/changelog/buildChangelog.ts +79 -0
  95. package/src/workflow/changelog/buildReleaseNotes.ts +50 -0
  96. package/src/workflow/changelog/extractLatestVersion.ts +22 -0
  97. package/src/workflow/changelog/extractPlainText.ts +38 -0
  98. package/src/workflow/changelog/extractPreviousVersion.ts +77 -0
  99. package/src/workflow/changelog/extractSemVer.ts +33 -0
  100. package/src/workflow/changelog/index.ts +12 -0
  101. package/src/workflow/changelog/iterateChangelogVersions.ts +70 -0
  102. package/src/workflow/changelog/prependChangelog.ts +59 -0
  103. package/src/workflow/changelog/writeChangelog.ts +27 -0
  104. package/src/workflow/committing/generateReleaseCommitMessage.ts +96 -0
  105. package/src/workflow/committing/index.ts +5 -0
  106. package/src/workflow/committing/isReleaseCommit.ts +52 -0
  107. package/src/workflow/discovery/buildDependencyGraph.ts +43 -0
  108. package/src/workflow/discovery/discoverPackageTag.ts +98 -0
  109. package/src/workflow/discovery/index.ts +9 -0
  110. package/src/workflow/discovery/resolvePackageStableTag.ts +88 -0
  111. package/src/workflow/discovery/resolvePackageTag.ts +45 -0
  112. package/src/workflow/discovery/sortPackages.ts +73 -0
  113. package/src/workflow/discovery/verifyPackageTag.ts +43 -0
  114. package/src/workflow/index.ts +10 -0
  115. package/src/workflow/report/generateReleaseReport.ts +76 -0
  116. package/src/workflow/report/index.ts +5 -0
  117. package/src/workflow/report/printReleaseReport.ts +97 -0
  118. package/src/workflow/runner/index.ts +4 -0
  119. package/src/workflow/runner/runWorkflow.ts +120 -0
  120. package/src/workflow/versioning/bumpVersion.ts +67 -0
  121. package/src/workflow/versioning/deriveReleaseType.ts +33 -0
  122. package/src/workflow/versioning/determineReleaseType.ts +35 -0
  123. package/src/workflow/versioning/index.ts +8 -0
  124. package/src/workflow/versioning/isStableVersion.ts +31 -0
  125. package/src/workflow/versioning/pickPriorityReleaseType.ts +27 -0
@@ -0,0 +1,14 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { resolveGitRevision } from '../revision/resolveGitRevision'
5
+
6
+ /**
7
+ * Resolve the current commit hash for HEAD in the repository.
8
+ *
9
+ * @param cwd The repository working directory
10
+ * @returns A promise that resolves to the full, trimmed commit SHA-1 string
11
+ */
12
+ export function getCurrentGitCommit(cwd: string): Promise<string> {
13
+ return resolveGitRevision(cwd, 'HEAD')
14
+ }
@@ -0,0 +1,53 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { TTLCache } from '#cache'
5
+ import type { Commit } from '#types'
6
+ import { execGit } from '#util'
7
+ import { parseCommit } from './parseCommit'
8
+ import { GIT_PRETTY_FORMAT, parseGitPrettyFormat } from './parseGitPrettyFormat'
9
+
10
+ const cache = new TTLCache<string, Commit>()
11
+
12
+ /**
13
+ * Retrieve and parse a commit by its reference.
14
+ *
15
+ * Fetches the raw git log entry for the specified reference, checks the cache
16
+ * for an existing entry, and parses the log data if a cache miss occurs.
17
+ *
18
+ * @param cwd The current working directory path
19
+ * @param ref The git reference to fetch
20
+ * @param markers Optional array of custom trailer marker strings
21
+ * @returns A structured commit object or null if the operation fails
22
+ */
23
+ export async function getParsedCommit(
24
+ cwd: string,
25
+ ref: string,
26
+ markers: readonly string[] = []
27
+ ): Promise<Commit | null> {
28
+ try {
29
+ const stdout = await execGit(cwd, [
30
+ 'log',
31
+ '-1',
32
+ ref,
33
+ '--name-only',
34
+ `--pretty=format:${GIT_PRETTY_FORMAT}`,
35
+ ])
36
+
37
+ const entry = parseGitPrettyFormat(stdout)
38
+
39
+ const key = `${entry.hash}:${entry.parent}:${markers}`
40
+
41
+ const cached = cache.get(key)
42
+
43
+ if (cached) return cached
44
+
45
+ const parsed = await parseCommit(entry, markers)
46
+
47
+ cache.set(key, parsed)
48
+
49
+ return parsed
50
+ } catch {
51
+ return null
52
+ }
53
+ }
@@ -0,0 +1,9 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ export * from './createGitCommit'
5
+ export * from './extractMentions'
6
+ export * from './getCurrentGitCommit'
7
+ export * from './getParsedCommit'
8
+ export * from './parseCommit'
9
+ export * from './parseGitPrettyFormat'
@@ -0,0 +1,99 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import type { Commit, CommitNote, CommitTrailer, ParsedCommit } from '#types'
5
+ import { CommitParser } from 'conventional-commits-parser'
6
+ import { buildTrailerPattern } from '../../util/buildTrailerPattern'
7
+ import { extractMentions } from './extractMentions'
8
+
9
+ export interface GitLogEntry {
10
+ hash: string
11
+ parent: string[]
12
+ authorDate: string
13
+ committerDate: string
14
+ subject: string
15
+ body: string
16
+ files: string[]
17
+ }
18
+
19
+ const BREAKING_HEADER_REGEX = /^([\w-]+)(?:\(([^)]+)\))?!: (.*)$/
20
+
21
+ const BREAKING_MARKERS: readonly string[] = [
22
+ 'BREAKING CHANGE',
23
+ 'BREAKING-CHANGE',
24
+ ]
25
+
26
+ /**
27
+ * Parse a git log entry into a structured commit object.
28
+ *
29
+ * Combines the commit subject and body, extracts conventional commit metadata
30
+ * using custom trailer markers and breaking header patterns, and normalizes
31
+ * dates to ISO format strings.
32
+ *
33
+ * @param entry The raw git log entry to parse
34
+ * @param markers Optional array of custom trailer marker strings
35
+ * @returns A structured commit object containing parsed details, notes, and mentions
36
+ */
37
+ export async function parseCommit(
38
+ entry: GitLogEntry,
39
+ markers: readonly string[] = []
40
+ ): Promise<Commit> {
41
+ const message = [entry.subject, entry.body].filter(Boolean).join('\n\n')
42
+
43
+ const parser = new CommitParser({
44
+ breakingHeaderPattern: BREAKING_HEADER_REGEX,
45
+ noteKeywords: [],
46
+ notesPattern: () => buildTrailerPattern(BREAKING_MARKERS.concat(markers)),
47
+ })
48
+
49
+ const commit = parser.parse(message) as object as ParsedCommit
50
+
51
+ const notes: CommitNote[] = []
52
+
53
+ const trailers: CommitTrailer[] = []
54
+
55
+ for (const note of commit.notes) {
56
+ if (BREAKING_MARKERS.includes(note.title.toUpperCase())) {
57
+ notes.push(note)
58
+ }
59
+
60
+ trailers.push({
61
+ key: note.title,
62
+ value: note.text,
63
+ })
64
+ }
65
+
66
+ if (notes.length === 0 && BREAKING_HEADER_REGEX.test(commit.header)) {
67
+ const note: CommitNote = {
68
+ title: 'BREAKING CHANGE',
69
+ text: commit.subject,
70
+ }
71
+
72
+ notes.push(note)
73
+
74
+ trailers.push({
75
+ key: note.title,
76
+ value: note.text,
77
+ })
78
+ }
79
+
80
+ const mentions = extractMentions(message)
81
+
82
+ return {
83
+ ...commit,
84
+ hash: entry.hash,
85
+ parent: entry.parent,
86
+ authorDate: new Date(entry.authorDate).toISOString(),
87
+ committerDate: new Date(entry.committerDate).toISOString(),
88
+ type: commit.type?.toLowerCase() || null,
89
+ scope: commit.scope || null,
90
+ subject: commit.subject || null,
91
+ header: commit.header,
92
+ body: commit.body,
93
+ footer: commit.footer,
94
+ notes,
95
+ mentions,
96
+ trailers,
97
+ files: entry.files,
98
+ }
99
+ }
@@ -0,0 +1,76 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { colors } from 'consola/utils'
5
+ import type { GitLogEntry } from './parseCommit'
6
+
7
+ /**
8
+ * Field separator used in the git pretty format to produce a single-line
9
+ *, machine-parseable record.
10
+ */
11
+ export const GIT_PRETTY_FIELD_SEPARATOR = '\x1f'
12
+
13
+ /**
14
+ * Pretty format string passed to `git log --pretty=format:`. Produces seven
15
+ * fields separated by `GIT_PRETTY_FIELD_SEPARATOR`:
16
+ * hash, parents, author date, committer date, subject, body, files.
17
+ */
18
+ export const GIT_PRETTY_FORMAT = [
19
+ '%H',
20
+ '%P',
21
+ '%aI',
22
+ '%cI',
23
+ '%s',
24
+ '%b',
25
+ '',
26
+ ].join(GIT_PRETTY_FIELD_SEPARATOR)
27
+
28
+ /**
29
+ * Parse a single line produced by `git log --pretty=format:${GIT_PRETTY_FORMAT}`.
30
+ *
31
+ * The input is expected to contain seven fields separated by
32
+ * `GIT_PRETTY_FIELD_SEPARATOR`:
33
+ * 1. `hash` (commit hash)
34
+ * 2. `parent` (space-separated parent hashes)
35
+ * 3. `authorDate` (ISO 8601)
36
+ * 4. `committerDate` (ISO 8601)
37
+ * 5. `subject`
38
+ * 6. `body`
39
+ * 7. `files` (newline-separated list)
40
+ *
41
+ * The function trims fields, converts parent hashes into an array, normalizes
42
+ * dates to ISO strings, and splits `files` into an array. Missing optional
43
+ * fields are normalized to sensible defaults.
44
+ *
45
+ * @param raw Raw pretty-format string produced by git
46
+ * @returns Parsed `GitLogEntry` with `hash`, `parent`, `authorDate`, `committerDate`, `subject`, `body`, and `files`
47
+ * @throws If required fields (hash, authorDate, committerDate, subject) are missing or malformed
48
+ */
49
+ export function parseGitPrettyFormat(raw: string): GitLogEntry {
50
+ let [hash, parentHashes, authorDate, committerDate, subject, body, files] =
51
+ raw.split(GIT_PRETTY_FIELD_SEPARATOR)
52
+
53
+ hash = hash?.trim()
54
+ parentHashes = parentHashes?.trim()
55
+ authorDate = authorDate?.trim()
56
+ committerDate = committerDate?.trim()
57
+ subject = subject?.trim()
58
+ body = body?.trim()
59
+ files = files?.trim()
60
+
61
+ if (!hash || !authorDate || !committerDate || typeof subject !== 'string') {
62
+ throw new Error(
63
+ `The input "${colors.bold(raw)}" is not in the expected pretty format.`
64
+ )
65
+ }
66
+
67
+ return {
68
+ hash,
69
+ parent: parentHashes ? parentHashes.split(/\s+/) : [],
70
+ authorDate: new Date(authorDate).toISOString(),
71
+ committerDate: new Date(committerDate).toISOString(),
72
+ subject,
73
+ body: body ?? '',
74
+ files: files ? files.split('\n') : [],
75
+ }
76
+ }
@@ -0,0 +1,28 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { execGit } from '#util'
5
+
6
+ /**
7
+ * Count mainline commits reachable from a given reference.
8
+ *
9
+ * Uses `git rev-list --count --first-parent` to calculate the length of the
10
+ * primary history trunk, excluding commits introduced by side-branch merges.
11
+ *
12
+ * @param cwd Repository working directory
13
+ * @param ref Commit reference to count from (defaults to `HEAD`)
14
+ * @returns Number of commits along the primary branch line up to `ref`
15
+ */
16
+ export async function countGitHistory(
17
+ cwd: string,
18
+ ref: string = 'HEAD'
19
+ ): Promise<number> {
20
+ const stdout = await execGit(cwd, [
21
+ 'rev-list',
22
+ '--count',
23
+ '--first-parent',
24
+ ref,
25
+ ])
26
+
27
+ return Number.parseInt(stdout.trim(), 10)
28
+ }
@@ -0,0 +1,108 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { execGit } from '#util'
5
+ import { isGitShallowRepository } from '../repository/isGitShallowRepository'
6
+ import { withShallowLock } from '../repository/withShallowLock'
7
+ import { isGitAncestor } from '../revision/isGitAncestor'
8
+ import { resolveGitRevision } from '../revision/resolveGitRevision'
9
+ import { countGitHistory } from './countGitHistory'
10
+
11
+ export interface ExpandGitHistoryOptions {
12
+ /**
13
+ * Number of commits to deepen the history by when paging
14
+ * @default 50
15
+ */
16
+ pageSize?: number | undefined
17
+
18
+ /**
19
+ * Local reference used to measure current shallow depth (for example HEAD)
20
+ * When provided the function accounts for existing depth before fetching more
21
+ */
22
+ from?: string | null | undefined
23
+
24
+ /**
25
+ * Target boundary reference where history deepening should stop
26
+ * If provided the function will first attempt a single fetch to reach this commit
27
+ */
28
+ until?: string | null | undefined
29
+ }
30
+
31
+ /**
32
+ * Deepen a shallow clone so additional commits become available.
33
+ *
34
+ * Attempts a single `--shallow-since` fetch to reach `until` (if provided),
35
+ * otherwise deepens by `pageSize`, accounting for existing depth when `from`
36
+ * is supplied. Operation is serialized to avoid concurrent deepens.
37
+ *
38
+ * @param cwd Repository working directory
39
+ * @param options Options: { pageSize?, from?, until? }
40
+ * @returns true if history was deepened or already sufficient, otherwise false
41
+ */
42
+ export async function expandGitHistory(
43
+ cwd: string,
44
+ options: ExpandGitHistoryOptions = {}
45
+ ): Promise<boolean> {
46
+ if (!(await isGitShallowRepository(cwd))) {
47
+ return false
48
+ }
49
+
50
+ return withShallowLock(cwd, async () => {
51
+ const { until } = options
52
+ let { from } = options
53
+ let { pageSize = 50 } = options
54
+
55
+ // If an `until` boundary is provided, try a single fetch using the target commit's date
56
+ if (until) {
57
+ if (await isGitAncestor(cwd, until, 'HEAD')) {
58
+ return true
59
+ }
60
+
61
+ const commitDate = (
62
+ await execGit(cwd, ['show', '-s', '--format=%cI', `${until}^{commit}`])
63
+ ).trim()
64
+
65
+ try {
66
+ await execGit(cwd, [
67
+ 'fetch',
68
+ `--shallow-since=${commitDate}`,
69
+ '--update-shallow',
70
+ '--filter=blob:none',
71
+ ])
72
+
73
+ return true
74
+ } catch {}
75
+ }
76
+
77
+ // Incremental deepening: account for existing local depth when `from` is set
78
+ if (from) {
79
+ from = await resolveGitRevision(cwd, from)
80
+
81
+ const depth = await countGitHistory(cwd, from)
82
+
83
+ if (depth > pageSize) {
84
+ return true
85
+ }
86
+
87
+ // Reduce requested deepen amount by existing depth (exclude the `from` commit)
88
+ pageSize -= depth - 1
89
+ }
90
+
91
+ // Perform a deepen fetch; if `from` is not an ancestor of HEAD, fetch that ref
92
+ try {
93
+ await execGit(cwd, [
94
+ 'fetch',
95
+ `--deepen=${pageSize}`,
96
+ '--update-shallow',
97
+ '--filter=blob:none',
98
+ ...(from && !(await isGitAncestor(cwd, from, 'HEAD'))
99
+ ? ['origin', from, 'HEAD']
100
+ : []),
101
+ ])
102
+
103
+ return true
104
+ } catch {
105
+ return false
106
+ }
107
+ })
108
+ }
@@ -0,0 +1,9 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ export * from './countGitHistory'
5
+ export * from './expandGitHistory'
6
+ export * from './mergeGitHistory'
7
+ export * from './restoreFullGitHistory'
8
+ export * from './showGitHistory'
9
+ export * from './showParsedGitHistory'
@@ -0,0 +1,76 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import type { Commit } from '#types'
5
+ import {
6
+ detectGitRelationship,
7
+ GIT_REL_FLAGS,
8
+ } from '../revision/detectGitRelationship'
9
+
10
+ /**
11
+ * Merge two newest-first commit lists into a single deduplicated newest-first list.
12
+ *
13
+ * Compares commits by hash, uses git relationship (ancestor/descendant) when needed,
14
+ * and falls back to committerDate for unrelated commits.
15
+ *
16
+ * @param cwd Repository working directory used for relationship checks
17
+ * @param a Commits from the first source (newest-first)
18
+ * @param b Commits from the second source (newest-first)
19
+ * @returns Merged commits in newest-first order with duplicates removed
20
+ */
21
+ export async function mergeGitHistory(
22
+ cwd: string,
23
+ a: readonly Commit[],
24
+ b: readonly Commit[]
25
+ ): Promise<Commit[]> {
26
+ const merged: Commit[] = []
27
+ const a2 = [...a]
28
+ const b2 = [...b]
29
+ let i = a2.shift()
30
+ let j = b2.shift()
31
+
32
+ while (i && j) {
33
+ const rel =
34
+ i.hash === j.hash
35
+ ? GIT_REL_FLAGS.IS_EQUAL
36
+ : await detectGitRelationship(cwd, i.hash, j.hash)
37
+
38
+ switch (rel) {
39
+ case GIT_REL_FLAGS.IS_EQUAL:
40
+ merged.push(i)
41
+ i = a2.shift()
42
+ j = b2.shift()
43
+ break
44
+
45
+ case GIT_REL_FLAGS.IS_ANCESTOR:
46
+ merged.push(j)
47
+ j = b2.shift()
48
+ break
49
+
50
+ case GIT_REL_FLAGS.IS_DESCENDANT:
51
+ merged.push(i)
52
+ i = a2.shift()
53
+ break
54
+
55
+ case GIT_REL_FLAGS.NONE:
56
+ if (i.committerDate < j.committerDate) {
57
+ merged.push(j)
58
+ j = b2.shift()
59
+ } else {
60
+ merged.push(i)
61
+ i = a2.shift()
62
+ }
63
+ break
64
+ }
65
+ }
66
+
67
+ if (i) {
68
+ merged.push(i)
69
+ }
70
+
71
+ if (j) {
72
+ merged.push(j)
73
+ }
74
+
75
+ return merged.concat(a2).concat(b2)
76
+ }
@@ -0,0 +1,19 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { execGit } from '#util'
5
+
6
+ /**
7
+ * Convert a shallow clone into a full clone by fetching additional history.
8
+ *
9
+ * Runs `git fetch --unshallow`. Returns `true` when the operation succeeds
10
+ * and `false` on error.
11
+ *
12
+ * @param cwd Repository working directory
13
+ * @returns `true` when the operation succeeded, otherwise `false`
14
+ */
15
+ export function restoreFullGitHistory(cwd: string): Promise<boolean> {
16
+ return execGit(cwd, ['fetch', '--unshallow'])
17
+ .then(() => true)
18
+ .catch(() => false)
19
+ }
@@ -0,0 +1,80 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { execGit } from '#util'
5
+ import { resolveGitRevision } from '../revision/resolveGitRevision'
6
+ import {
7
+ expandGitHistory,
8
+ type ExpandGitHistoryOptions,
9
+ } from './expandGitHistory'
10
+
11
+ export interface ShowGitHistoryOptions extends ExpandGitHistoryOptions {
12
+ filePath?: string | readonly string[] | undefined
13
+ }
14
+
15
+ /**
16
+ * Yield commit SHAs from repository history in paginated order.
17
+ *
18
+ * Ensures commits are available via `expandGitHistory`, resolves `until` to a
19
+ * commit id, and pages from `from` toward `until`, yielding each commit SHA.
20
+ * If a git command fails the generator ends silently.
21
+ *
22
+ * @param cwd Repository working directory
23
+ * @param options Supports `from`, `until`, `pageSize`, and `filePath`
24
+ * @yields Commit SHA strings in paginated order
25
+ */
26
+ export async function* showGitHistory(
27
+ cwd: string,
28
+ options: ShowGitHistoryOptions = {}
29
+ ): AsyncGenerator<string> {
30
+ try {
31
+ const { filePath, pageSize = 50 } = options
32
+ let from = options.from || 'HEAD'
33
+ let until = options.until || undefined
34
+
35
+ if (until) {
36
+ until = await resolveGitRevision(cwd, until)
37
+ }
38
+
39
+ while (true) {
40
+ await expandGitHistory(cwd, { from, until, pageSize })
41
+
42
+ const args = ['rev-list', `--max-count=${pageSize}`, from]
43
+
44
+ if (until) {
45
+ args.push(`^${until}`)
46
+ }
47
+
48
+ if (filePath) {
49
+ args.push(
50
+ '--',
51
+ ...(Array.isArray(filePath) ? filePath : [filePath]).filter(Boolean)
52
+ )
53
+ }
54
+
55
+ const stdout = await execGit(cwd, args)
56
+
57
+ const list = stdout.trim().split('\n').filter(Boolean)
58
+
59
+ for (const item of list.slice(0, -1)) {
60
+ yield item
61
+ }
62
+
63
+ const tail = list.at(-1)
64
+
65
+ if (!tail) {
66
+ return
67
+ }
68
+
69
+ if (tail === from) {
70
+ yield tail
71
+
72
+ from = `${tail}^`
73
+
74
+ from = await resolveGitRevision(cwd, from)
75
+ } else {
76
+ from = tail
77
+ }
78
+ }
79
+ } catch {}
80
+ }
@@ -0,0 +1,30 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import type { Commit } from '#types'
5
+ import { getParsedCommit } from '../commit/getParsedCommit'
6
+ import { showGitHistory, type ShowGitHistoryOptions } from './showGitHistory'
7
+
8
+ /**
9
+ * Yield parsed commits sequentially from repository history.
10
+ *
11
+ * Fetches commits using `showGitHistory` and parses each SHA with
12
+ * `getParsedCommit`. Yields successfully parsed `Commit` objects on demand and
13
+ * omits unparsable commits.
14
+ *
15
+ * @param cwd Repository working directory
16
+ * @param options Filter and pagination options passed to `showGitHistory`
17
+ * @yields Parsed `Commit` objects in historical order
18
+ */
19
+ export async function* showParsedGitHistory(
20
+ cwd: string,
21
+ options?: ShowGitHistoryOptions
22
+ ): AsyncGenerator<Commit> {
23
+ for await (const commit of showGitHistory(cwd, options)) {
24
+ const parsed = await getParsedCommit(cwd, commit)
25
+
26
+ if (parsed) {
27
+ yield parsed
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,9 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ export * from './branch'
5
+ export * from './commit'
6
+ export * from './history'
7
+ export * from './repository'
8
+ export * from './revision'
9
+ export * from './tag'
@@ -0,0 +1,31 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { execGit } from '#util'
5
+ import { getGitConfigValue } from './getGitConfigValue'
6
+
7
+ /**
8
+ * Ensure git `user.name` and `user.email` are configured for the repository at `cwd`.
9
+ *
10
+ * No-op when the values are already present.
11
+ *
12
+ * @param cwd Repository working directory
13
+ * @param user Object containing `name` and `email` values to set when missing
14
+ * @returns Promise that resolves when configuration is ensured
15
+ */
16
+ export async function ensureGitUserConfig(
17
+ cwd: string,
18
+ user: { name: string; email: string }
19
+ ): Promise<void> {
20
+ if (
21
+ (await getGitConfigValue(cwd, 'user.name')) &&
22
+ (await getGitConfigValue(cwd, 'user.email'))
23
+ ) {
24
+ return
25
+ }
26
+
27
+ // https://github.com/actions/checkout?tab=readme-ov-file#push-a-commit-using-the-built-in-token
28
+ await execGit(cwd, ['config', 'user.name', user.name])
29
+
30
+ await execGit(cwd, ['config', 'user.email', user.email])
31
+ }
@@ -0,0 +1,27 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { execGit } from '#util'
5
+
6
+ /**
7
+ * Get a local git configuration value for `key` in the repository at `cwd`.
8
+ *
9
+ * Returns the config value when present; otherwise `undefined`.
10
+ *
11
+ * @param cwd Repository working directory
12
+ * @param key Git config key to read
13
+ * @returns The config value or `undefined` if not set
14
+ */
15
+ export async function getGitConfigValue(
16
+ cwd: string,
17
+ key: string
18
+ ): Promise<string | undefined> {
19
+ try {
20
+ const stdout = await execGit(cwd, ['config', '--local', '--get', key])
21
+ const value = stdout.trim()
22
+
23
+ return value ? value : undefined
24
+ } catch {
25
+ return undefined
26
+ }
27
+ }