@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.
- package/dist/analyzer.js +38 -0
- package/dist/analyzer.js.map +1 -0
- package/dist/builder.js +52 -0
- package/dist/builder.js.map +1 -0
- package/dist/changelog.js +58 -0
- package/dist/changelog.js.map +1 -0
- package/dist/formatError-BXNdyRfu.js +23 -0
- package/dist/formatError-BXNdyRfu.js.map +1 -0
- package/dist/index.js +175 -1
- package/dist/index.js.map +1 -0
- package/dist/types-la_KkjCS.js +1 -0
- package/dist/types.js +2 -0
- package/dist/workflow-BoVmbvH6.js +2342 -0
- package/dist/workflow-BoVmbvH6.js.map +1 -0
- package/package.json +27 -3
- package/src/analyzer.ts +63 -0
- package/src/builder.ts +104 -0
- package/src/cache/TTLCache.ts +100 -0
- package/src/cache/index.ts +4 -0
- package/src/changelog.ts +97 -0
- package/src/config/const.ts +97 -0
- package/src/config/index.ts +11 -0
- package/src/config/isSemVerGitTag.ts +25 -0
- package/src/config/normalizeCommitFilter.ts +37 -0
- package/src/config/normalizeReleaseRule.ts +42 -0
- package/src/config/normalizeWorkspace.ts +34 -0
- package/src/config/resolvePlugin.ts +56 -0
- package/src/config/resolveReleaseTarget.ts +71 -0
- package/src/config/sanitizePrereleaseLabel.ts +18 -0
- package/src/git/branch/getCurrentGitBranch.ts +18 -0
- package/src/git/branch/index.ts +4 -0
- package/src/git/commit/createGitCommit.ts +29 -0
- package/src/git/commit/extractMentions.ts +34 -0
- package/src/git/commit/getCurrentGitCommit.ts +14 -0
- package/src/git/commit/getParsedCommit.ts +53 -0
- package/src/git/commit/index.ts +9 -0
- package/src/git/commit/parseCommit.ts +99 -0
- package/src/git/commit/parseGitPrettyFormat.ts +76 -0
- package/src/git/history/countGitHistory.ts +28 -0
- package/src/git/history/expandGitHistory.ts +108 -0
- package/src/git/history/index.ts +9 -0
- package/src/git/history/mergeGitHistory.ts +76 -0
- package/src/git/history/restoreFullGitHistory.ts +19 -0
- package/src/git/history/showGitHistory.ts +80 -0
- package/src/git/history/showParsedGitHistory.ts +30 -0
- package/src/git/index.ts +9 -0
- package/src/git/repository/ensureGitUserConfig.ts +31 -0
- package/src/git/repository/getGitConfigValue.ts +27 -0
- package/src/git/repository/getGitRemote.ts +20 -0
- package/src/git/repository/index.ts +10 -0
- package/src/git/repository/isGitShallowRepository.ts +21 -0
- package/src/git/repository/pushGitChanges.ts +25 -0
- package/src/git/repository/resolveGitRepository.ts +40 -0
- package/src/git/repository/withShallowLock.ts +62 -0
- package/src/git/revision/detectGitRelationship.ts +43 -0
- package/src/git/revision/index.ts +7 -0
- package/src/git/revision/isGitAncestor.ts +33 -0
- package/src/git/revision/readGitFile.ts +29 -0
- package/src/git/revision/resolveGitRevision.ts +23 -0
- package/src/git/tag/createGitTag.ts +25 -0
- package/src/git/tag/deleteGitTag.ts +17 -0
- package/src/git/tag/ensureGitTag.ts +63 -0
- package/src/git/tag/generateGitTagName.ts +43 -0
- package/src/git/tag/getCurrentGitTag.ts +23 -0
- package/src/git/tag/getGitTagMessage.ts +35 -0
- package/src/git/tag/getLatestGitTag.ts +22 -0
- package/src/git/tag/index.ts +13 -0
- package/src/git/tag/isGitTagConnected.ts +33 -0
- package/src/git/tag/isGitTagPointed.ts +38 -0
- package/src/git/tag/isGitTagPresent.ts +17 -0
- package/src/index.ts +8 -2
- package/src/types.ts +217 -0
- package/src/util/buildTrailerPattern.ts +20 -0
- package/src/util/compact.ts +14 -0
- package/src/util/execCommand.ts +130 -0
- package/src/util/execGit.ts +18 -0
- package/src/util/formatError.ts +21 -0
- package/src/util/formatFile.ts +41 -0
- package/src/util/index.ts +16 -0
- package/src/util/interpolate.ts +141 -0
- package/src/util/isCI.ts +23 -0
- package/src/util/isDefined.ts +12 -0
- package/src/util/isMatch.ts +43 -0
- package/src/util/readJson.ts +47 -0
- package/src/util/unique.ts +26 -0
- package/src/util/writeJson.ts +28 -0
- package/src/workflow/analysis/analyzePackageChanges.ts +54 -0
- package/src/workflow/analysis/index.ts +9 -0
- package/src/workflow/analysis/isCommitForPackage.ts +23 -0
- package/src/workflow/analysis/promotePrereleaseVersions.ts +50 -0
- package/src/workflow/analysis/propagateDependencyUpdates.ts +71 -0
- package/src/workflow/analysis/sliceUnreleased.ts +36 -0
- package/src/workflow/analysis/streamPackageCommits.ts +34 -0
- package/src/workflow/changelog/buildChangelog.ts +79 -0
- package/src/workflow/changelog/buildReleaseNotes.ts +50 -0
- package/src/workflow/changelog/extractLatestVersion.ts +22 -0
- package/src/workflow/changelog/extractPlainText.ts +38 -0
- package/src/workflow/changelog/extractPreviousVersion.ts +77 -0
- package/src/workflow/changelog/extractSemVer.ts +33 -0
- package/src/workflow/changelog/index.ts +12 -0
- package/src/workflow/changelog/iterateChangelogVersions.ts +70 -0
- package/src/workflow/changelog/prependChangelog.ts +59 -0
- package/src/workflow/changelog/writeChangelog.ts +27 -0
- package/src/workflow/committing/generateReleaseCommitMessage.ts +96 -0
- package/src/workflow/committing/index.ts +5 -0
- package/src/workflow/committing/isReleaseCommit.ts +52 -0
- package/src/workflow/discovery/buildDependencyGraph.ts +43 -0
- package/src/workflow/discovery/discoverPackageTag.ts +98 -0
- package/src/workflow/discovery/index.ts +9 -0
- package/src/workflow/discovery/resolvePackageStableTag.ts +88 -0
- package/src/workflow/discovery/resolvePackageTag.ts +45 -0
- package/src/workflow/discovery/sortPackages.ts +73 -0
- package/src/workflow/discovery/verifyPackageTag.ts +43 -0
- package/src/workflow/index.ts +10 -0
- package/src/workflow/report/generateReleaseReport.ts +76 -0
- package/src/workflow/report/index.ts +5 -0
- package/src/workflow/report/printReleaseReport.ts +97 -0
- package/src/workflow/runner/index.ts +4 -0
- package/src/workflow/runner/runWorkflow.ts +120 -0
- package/src/workflow/versioning/bumpVersion.ts +67 -0
- package/src/workflow/versioning/deriveReleaseType.ts +33 -0
- package/src/workflow/versioning/determineReleaseType.ts +35 -0
- package/src/workflow/versioning/index.ts +8 -0
- package/src/workflow/versioning/isStableVersion.ts +31 -0
- 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
|
+
}
|
package/src/git/index.ts
ADDED
|
@@ -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
|
+
}
|