@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,20 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { execGit } from '#util'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get the URL for the named remote (defaults to `origin`).
|
|
8
|
+
*
|
|
9
|
+
* @param cwd Repository working directory
|
|
10
|
+
* @param remoteName Remote name (defaults to `'origin'`)
|
|
11
|
+
* @returns Remote URL string or `null` when unavailable
|
|
12
|
+
*/
|
|
13
|
+
export function getGitRemote(
|
|
14
|
+
cwd: string,
|
|
15
|
+
remoteName = 'origin'
|
|
16
|
+
): Promise<string | null> {
|
|
17
|
+
return execGit(cwd, ['remote', 'get-url', remoteName])
|
|
18
|
+
.then((stdout) => stdout.trim())
|
|
19
|
+
.catch(() => null)
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
export * from './ensureGitUserConfig'
|
|
5
|
+
export * from './getGitConfigValue'
|
|
6
|
+
export * from './getGitRemote'
|
|
7
|
+
export * from './isGitShallowRepository'
|
|
8
|
+
export * from './pushGitChanges'
|
|
9
|
+
export * from './resolveGitRepository'
|
|
10
|
+
export * from './withShallowLock'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { execGit } from '#util'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Determine whether the repository at `cwd` is a shallow clone.
|
|
8
|
+
*
|
|
9
|
+
* Runs `git rev-parse --is-shallow-repository` to detect shallow clones.
|
|
10
|
+
* Useful before operations that require full history.
|
|
11
|
+
*
|
|
12
|
+
* @param cwd Repository working directory
|
|
13
|
+
* @returns `true` if shallow, `false` if full clone, or `null` on error
|
|
14
|
+
*/
|
|
15
|
+
export async function isGitShallowRepository(cwd: string): Promise<boolean> {
|
|
16
|
+
const stdout = (
|
|
17
|
+
await execGit(cwd, ['rev-parse', '--is-shallow-repository'])
|
|
18
|
+
).trim()
|
|
19
|
+
|
|
20
|
+
return stdout === 'true'
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { compact, execGit } from '#util'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Push a branch and optional tags to the `origin` remote using an atomic push.
|
|
8
|
+
*
|
|
9
|
+
* @param cwd Repository working directory
|
|
10
|
+
* @param branch Branch name to push
|
|
11
|
+
* @param additionalTags Optional list of tag names to push alongside the branch
|
|
12
|
+
* @returns Promise that resolves when the push completes
|
|
13
|
+
*/
|
|
14
|
+
export async function pushGitChanges(
|
|
15
|
+
cwd: string,
|
|
16
|
+
branch: string | null,
|
|
17
|
+
...additionalTags: readonly string[]
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
await execGit(cwd, [
|
|
20
|
+
'push',
|
|
21
|
+
'origin',
|
|
22
|
+
...compact([branch, ...additionalTags]),
|
|
23
|
+
'--atomic',
|
|
24
|
+
])
|
|
25
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import type { GitRepository } from '#types'
|
|
5
|
+
import GitHost from 'hosted-git-info'
|
|
6
|
+
import { getGitRemote } from './getGitRemote'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolve Git repository host, owner, and project metadata from the `origin` remote URL.
|
|
10
|
+
*
|
|
11
|
+
* @param cwd Repository working directory
|
|
12
|
+
* @returns Promise that resolves to a `GitRepository` object, or `null` when unavailable
|
|
13
|
+
*/
|
|
14
|
+
export async function resolveGitRepository(
|
|
15
|
+
cwd: string
|
|
16
|
+
): Promise<GitRepository | null> {
|
|
17
|
+
const origin = await getGitRemote(cwd)
|
|
18
|
+
|
|
19
|
+
if (!origin) {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const info = GitHost.fromUrl(origin)
|
|
24
|
+
|
|
25
|
+
if (
|
|
26
|
+
info &&
|
|
27
|
+
typeof info.type === 'string' &&
|
|
28
|
+
typeof info.user === 'string' &&
|
|
29
|
+
typeof info.project === 'string'
|
|
30
|
+
) {
|
|
31
|
+
return {
|
|
32
|
+
host: info.type,
|
|
33
|
+
owner: info.user,
|
|
34
|
+
repo: info.project,
|
|
35
|
+
url: info.browse(),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
5
|
+
import { isGitShallowRepository } from './isGitShallowRepository'
|
|
6
|
+
|
|
7
|
+
const lockQueue = new Map<string, Promise<void>>()
|
|
8
|
+
|
|
9
|
+
const context = new AsyncLocalStorage<Set<string>>()
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Serialize access to a shallow Git repository while running a callback.
|
|
13
|
+
*
|
|
14
|
+
* If `cwd` is a shallow clone, callers from different async contexts are
|
|
15
|
+
* executed one at a time; nested calls within the same async context run
|
|
16
|
+
* immediately without waiting. If the repository becomes non-shallow before
|
|
17
|
+
* a queued turn runs, downstream waiters are released early.
|
|
18
|
+
*
|
|
19
|
+
* @param cwd Repository working directory path string
|
|
20
|
+
* @param callback Function to execute while the lock is held
|
|
21
|
+
* @returns Promise resolving to the result of `callback`
|
|
22
|
+
*/
|
|
23
|
+
export async function withShallowLock<T>(
|
|
24
|
+
cwd: string,
|
|
25
|
+
callback: () => T | Promise<T>
|
|
26
|
+
): Promise<T> {
|
|
27
|
+
if (!(await isGitShallowRepository(cwd))) {
|
|
28
|
+
return callback()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const active = context.getStore()
|
|
32
|
+
|
|
33
|
+
if (active?.has(cwd)) {
|
|
34
|
+
return callback()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { promise, resolve } = Promise.withResolvers<void>()
|
|
38
|
+
const previous = lockQueue.get(cwd) ?? Promise.resolve()
|
|
39
|
+
const queued = previous.then(() => promise)
|
|
40
|
+
|
|
41
|
+
lockQueue.set(cwd, queued)
|
|
42
|
+
|
|
43
|
+
// Wait until previous tasks complete.
|
|
44
|
+
await previous
|
|
45
|
+
|
|
46
|
+
// If an earlier task made the repo non-shallow, release downstream waiters now.
|
|
47
|
+
if (!(await isGitShallowRepository(cwd))) {
|
|
48
|
+
resolve()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
return await context.run(new Set(active).add(cwd), callback)
|
|
53
|
+
} finally {
|
|
54
|
+
// Ensure downstream waiters are unblocked even if callback throws.
|
|
55
|
+
resolve()
|
|
56
|
+
|
|
57
|
+
// Remove the queue entry only if no newer task has replaced it.
|
|
58
|
+
if (lockQueue.get(cwd) === queued) {
|
|
59
|
+
lockQueue.delete(cwd)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { isGitAncestor } from './isGitAncestor'
|
|
5
|
+
|
|
6
|
+
export const GIT_REL_FLAGS = {
|
|
7
|
+
NONE: 0,
|
|
8
|
+
IS_ANCESTOR: 1,
|
|
9
|
+
IS_DESCENDANT: 2,
|
|
10
|
+
IS_EQUAL: 3,
|
|
11
|
+
} as const
|
|
12
|
+
|
|
13
|
+
export type GitRelFlags = (typeof GIT_REL_FLAGS)[keyof typeof GIT_REL_FLAGS]
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Determine the relationship between two git refs.
|
|
17
|
+
*
|
|
18
|
+
* Runs `git merge-base --is-ancestor` in both directions and returns a
|
|
19
|
+
* numeric bitmask using `GIT_REL_FLAGS`. A successful check (exit code 0)
|
|
20
|
+
* indicates an ancestor relationship; non-zero exit is treated as "not ancestor".
|
|
21
|
+
*
|
|
22
|
+
* @param cwd Repository working directory
|
|
23
|
+
* @param refA First ref (candidate ancestor)
|
|
24
|
+
* @param refB Second ref (candidate descendant)
|
|
25
|
+
* @returns Bitmask of type `GitRelFlags`
|
|
26
|
+
*/
|
|
27
|
+
export async function detectGitRelationship(
|
|
28
|
+
cwd: string,
|
|
29
|
+
refA: string,
|
|
30
|
+
refB: string
|
|
31
|
+
): Promise<GitRelFlags> {
|
|
32
|
+
let flags = GIT_REL_FLAGS.NONE
|
|
33
|
+
|
|
34
|
+
const [aIsAncestor, bIsAncestor] = await Promise.all([
|
|
35
|
+
isGitAncestor(cwd, refA, refB),
|
|
36
|
+
isGitAncestor(cwd, refB, refA),
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
if (aIsAncestor) flags |= GIT_REL_FLAGS.IS_ANCESTOR
|
|
40
|
+
if (bIsAncestor) flags |= GIT_REL_FLAGS.IS_DESCENDANT
|
|
41
|
+
|
|
42
|
+
return flags as GitRelFlags
|
|
43
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { execGit } from '#util'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Determine whether `ancestor` is an ancestor of `descendant`.
|
|
8
|
+
*
|
|
9
|
+
* Runs `git merge-base --is-ancestor <ancestor> <descendant>` in the repository at `cwd`.
|
|
10
|
+
* Resolves to `true` if `ancestor` is an ancestor, `false` if not.
|
|
11
|
+
* Rethrows errors for invalid refs or other failures.
|
|
12
|
+
*
|
|
13
|
+
* @param cwd Repository working directory
|
|
14
|
+
* @param ancestor Candidate ancestor ref
|
|
15
|
+
* @param descendant Candidate descendant ref
|
|
16
|
+
* @returns Resolves to `true` if `ancestor` is an ancestor of `descendant`, `false` otherwise
|
|
17
|
+
* @throws If a ref is invalid or the Git command fails
|
|
18
|
+
*/
|
|
19
|
+
export function isGitAncestor(
|
|
20
|
+
cwd: string,
|
|
21
|
+
ancestor: string,
|
|
22
|
+
descendant: string
|
|
23
|
+
): Promise<boolean> {
|
|
24
|
+
return execGit(cwd, ['merge-base', '--is-ancestor', ancestor, descendant])
|
|
25
|
+
.then(() => true)
|
|
26
|
+
.catch((err) => {
|
|
27
|
+
if ((err as { code?: number })?.code === 1) {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
throw err
|
|
32
|
+
})
|
|
33
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { execGit } from '#util'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Read a file's contents from a specific Git revision.
|
|
8
|
+
*
|
|
9
|
+
* Runs `git show <ref>:<filePath>` and returns the command output as a
|
|
10
|
+
* string. If the file does not exist at the given revision or the Git
|
|
11
|
+
* command fails for any reason, the function returns `null`.
|
|
12
|
+
*
|
|
13
|
+
* The returned string is the raw file contents as emitted by Git; callers
|
|
14
|
+
* that need binary-safe handling should decode or process the bytes
|
|
15
|
+
* accordingly. This function intentionally swallows Git errors and signals
|
|
16
|
+
* absence via a `null` result rather than throwing.
|
|
17
|
+
*
|
|
18
|
+
* @param cwd Repository working directory
|
|
19
|
+
* @param ref Commit-ish reference (commit SHA, tag, or branch) to read from
|
|
20
|
+
* @param filePath Path to the file relative to the repository root
|
|
21
|
+
* @returns File contents as a string, or `null` if the file is unavailable at the ref
|
|
22
|
+
*/
|
|
23
|
+
export function readGitFile(
|
|
24
|
+
cwd: string,
|
|
25
|
+
ref: string,
|
|
26
|
+
filePath: string
|
|
27
|
+
): Promise<string | null> {
|
|
28
|
+
return execGit(cwd, ['show', `${ref}:${filePath}`]).catch(() => null)
|
|
29
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { execGit } from '#util'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a Git revision or commit-ish expression to its full commit hash.
|
|
8
|
+
*
|
|
9
|
+
* Runs `git rev-parse --verify <revision>^{commit}` under the hood. Peels annotated
|
|
10
|
+
* tags, branches, short SHAs, and relative expressions down to a verified commit object.
|
|
11
|
+
*
|
|
12
|
+
* @param cwd The repository working directory
|
|
13
|
+
* @param revision The Git revision or expression to resolve
|
|
14
|
+
* @returns A promise that resolves to the full, trimmed commit SHA-1 string
|
|
15
|
+
*/
|
|
16
|
+
export async function resolveGitRevision(
|
|
17
|
+
cwd: string,
|
|
18
|
+
revision: string
|
|
19
|
+
): Promise<string> {
|
|
20
|
+
return (
|
|
21
|
+
await execGit(cwd, ['rev-parse', '--verify', `${revision}^{commit}`])
|
|
22
|
+
).trim()
|
|
23
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { execGit } from '#util'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a git tag in the repository at `cwd`.
|
|
8
|
+
*
|
|
9
|
+
* Creates an annotated tag when `message` is provided.
|
|
10
|
+
*
|
|
11
|
+
* @param cwd Repository working directory
|
|
12
|
+
* @param tag Tag name to create
|
|
13
|
+
* @param message Optional tag message (creates an annotated tag when present)
|
|
14
|
+
* @returns Promise that resolves when the tag is created
|
|
15
|
+
*/
|
|
16
|
+
export async function createGitTag(
|
|
17
|
+
cwd: string,
|
|
18
|
+
tag: string,
|
|
19
|
+
message?: string
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
await execGit(cwd, [
|
|
22
|
+
'tag',
|
|
23
|
+
...(message ? ['-a', tag, '-m', message, '--cleanup=whitespace'] : [tag]),
|
|
24
|
+
])
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { execGit } from '#util'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Delete a local git tag in the repository at `cwd`.
|
|
8
|
+
*
|
|
9
|
+
* @param cwd Repository working directory
|
|
10
|
+
* @param tag Tag name to delete
|
|
11
|
+
* @returns `true` when deletion succeeded, otherwise `false`
|
|
12
|
+
*/
|
|
13
|
+
export function deleteGitTag(cwd: string, tag: string): Promise<boolean> {
|
|
14
|
+
return execGit(cwd, ['tag', '-d', tag])
|
|
15
|
+
.then(() => true)
|
|
16
|
+
.catch(() => false)
|
|
17
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { execGit } from '#util'
|
|
5
|
+
import { countGitHistory } from '../history/countGitHistory'
|
|
6
|
+
import { withShallowLock } from '../repository/withShallowLock'
|
|
7
|
+
import { deleteGitTag } from './deleteGitTag'
|
|
8
|
+
import { isGitTagPresent } from './isGitTagPresent'
|
|
9
|
+
|
|
10
|
+
const MIN_HISTORY_DEPTH = 2
|
|
11
|
+
const ORIGIN_NOT_REPO = `fatal: 'origin' does not appear to be a git repository`
|
|
12
|
+
const REMOTE_REF_NOT_FOUND = `fatal: couldn't find remote ref`
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Ensure a tag exists locally, fetching it shallowly from `origin` if needed.
|
|
16
|
+
*
|
|
17
|
+
* Acquires a shallow-fetch lock, verifies local presence and history depth,
|
|
18
|
+
* attempts a shallow fetch of `refs/tags/<tag>` when required, and removes a
|
|
19
|
+
* local tag if the remote reports the tag is missing.
|
|
20
|
+
*
|
|
21
|
+
* @param cwd Working directory
|
|
22
|
+
* @param tag Tag name (for example `v1.2.0`)
|
|
23
|
+
* @returns `true` if the tag is present or was fetched; otherwise `false`
|
|
24
|
+
*/
|
|
25
|
+
export function ensureGitTag(cwd: string, tag: string): Promise<boolean> {
|
|
26
|
+
return withShallowLock(cwd, async () => {
|
|
27
|
+
const tagRef = `refs/tags/${tag}`
|
|
28
|
+
|
|
29
|
+
if (
|
|
30
|
+
(await isGitTagPresent(cwd, tag)) &&
|
|
31
|
+
(await countGitHistory(cwd, tagRef)) >= MIN_HISTORY_DEPTH
|
|
32
|
+
) {
|
|
33
|
+
return true
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
await execGit(cwd, [
|
|
38
|
+
'fetch',
|
|
39
|
+
'origin',
|
|
40
|
+
`${tagRef}:${tagRef}`,
|
|
41
|
+
`--depth=${MIN_HISTORY_DEPTH}`,
|
|
42
|
+
'--update-shallow',
|
|
43
|
+
'--force',
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
return true
|
|
47
|
+
} catch (err) {
|
|
48
|
+
const errorMessage = (err as Error | undefined)?.message
|
|
49
|
+
|
|
50
|
+
if (errorMessage?.includes(ORIGIN_NOT_REPO)) {
|
|
51
|
+
return await isGitTagPresent(cwd, tag)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (errorMessage?.includes(REMOTE_REF_NOT_FOUND)) {
|
|
55
|
+
if (await isGitTagPresent(cwd, tag)) {
|
|
56
|
+
await deleteGitTag(cwd, tag)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return false
|
|
62
|
+
})
|
|
63
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate a git tag name for a package.
|
|
6
|
+
*
|
|
7
|
+
* For entry packages the tag is `v<version>`. For non-entry packages the
|
|
8
|
+
* tag is `<name>@<version>`. Returns `null` when `version` is not provided.
|
|
9
|
+
*
|
|
10
|
+
* @param entry Whether the package is the repository entry package
|
|
11
|
+
* @param name Package name (required for non-entry packages)
|
|
12
|
+
* @param version Package version
|
|
13
|
+
* @returns Tag string, or `null` when `version` is absent
|
|
14
|
+
*/
|
|
15
|
+
export function generateGitTagName(
|
|
16
|
+
entry: boolean,
|
|
17
|
+
name: string | null | undefined,
|
|
18
|
+
version: string
|
|
19
|
+
): string
|
|
20
|
+
export function generateGitTagName(
|
|
21
|
+
entry: boolean,
|
|
22
|
+
name: string | null | undefined,
|
|
23
|
+
version: string | null | undefined
|
|
24
|
+
): string | null
|
|
25
|
+
export function generateGitTagName(
|
|
26
|
+
entry: boolean,
|
|
27
|
+
name: string | null | undefined,
|
|
28
|
+
version: string | null | undefined
|
|
29
|
+
): string | null {
|
|
30
|
+
if (!version) {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (entry) {
|
|
35
|
+
return `v${version}`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!name) {
|
|
39
|
+
throw new Error('Package name is required to build a tag name.')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return `${name}@${version}`
|
|
43
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { execGit } from '#util'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get the tag name that exactly matches the current `HEAD`, if any.
|
|
8
|
+
*
|
|
9
|
+
* @param cwd Repository working directory
|
|
10
|
+
* @returns Tag name or `null` when no exact tag matches HEAD
|
|
11
|
+
*/
|
|
12
|
+
export async function getCurrentGitTag(cwd: string): Promise<string | null> {
|
|
13
|
+
try {
|
|
14
|
+
const stdout = await execGit(cwd, ['describe', '--tags', '--exact-match'])
|
|
15
|
+
|
|
16
|
+
const tag = stdout.trim()
|
|
17
|
+
|
|
18
|
+
/* istanbul ignore next */
|
|
19
|
+
return tag ? tag : null
|
|
20
|
+
} catch {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { execGit } from '#util'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get the message of an annotated git tag in the repository at `cwd`.
|
|
8
|
+
*
|
|
9
|
+
* @param cwd Repository working directory
|
|
10
|
+
* @param tag Tag name to query
|
|
11
|
+
* @returns The tag message string when found, otherwise `undefined`
|
|
12
|
+
*/
|
|
13
|
+
export async function getGitTagMessage(
|
|
14
|
+
cwd: string,
|
|
15
|
+
tag: string
|
|
16
|
+
): Promise<string | undefined> {
|
|
17
|
+
try {
|
|
18
|
+
const stdout = await execGit(cwd, [
|
|
19
|
+
'tag',
|
|
20
|
+
'-l',
|
|
21
|
+
'--format=%(objecttype)%00%(contents)',
|
|
22
|
+
tag,
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
const [objectType, message] = stdout.split('\0')
|
|
26
|
+
|
|
27
|
+
if (objectType?.trim() !== 'tag') {
|
|
28
|
+
return undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return message?.trim() || undefined
|
|
32
|
+
} catch {
|
|
33
|
+
return undefined
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { execGit } from '#util'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get the most recent tag reachable from `HEAD`.
|
|
8
|
+
*
|
|
9
|
+
* @param cwd Repository working directory
|
|
10
|
+
* @returns Latest tag string or `null` when none is found
|
|
11
|
+
*/
|
|
12
|
+
export async function getLatestGitTag(cwd: string): Promise<string | null> {
|
|
13
|
+
try {
|
|
14
|
+
const stdout = await execGit(cwd, ['describe', '--tags', '--abbrev=0'])
|
|
15
|
+
const tag = stdout.trim()
|
|
16
|
+
|
|
17
|
+
/* istanbul ignore next */
|
|
18
|
+
return tag ? tag : null
|
|
19
|
+
} catch {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
export * from './createGitTag'
|
|
5
|
+
export * from './deleteGitTag'
|
|
6
|
+
export * from './ensureGitTag'
|
|
7
|
+
export * from './generateGitTagName'
|
|
8
|
+
export * from './getCurrentGitTag'
|
|
9
|
+
export * from './getGitTagMessage'
|
|
10
|
+
export * from './getLatestGitTag'
|
|
11
|
+
export * from './isGitTagConnected'
|
|
12
|
+
export * from './isGitTagPointed'
|
|
13
|
+
export * from './isGitTagPresent'
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { execGit } from '#util'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Determine whether a specific git tag commit is reachable from the current HEAD.
|
|
8
|
+
*
|
|
9
|
+
* Uses `git merge-base --is-ancestor refs/tags/<tag>^{commit} HEAD` to verify
|
|
10
|
+
* that the tag's commit is an ancestor of the current reference. Any non-zero
|
|
11
|
+
* exit status or execution error is treated as "not reachable".
|
|
12
|
+
*
|
|
13
|
+
* @param cwd Working directory where the git command will run
|
|
14
|
+
* @param tag Tag name to evaluate (for example, "v1.2.0")
|
|
15
|
+
* @returns True if the tag commit is reachable from HEAD, false otherwise
|
|
16
|
+
*/
|
|
17
|
+
export async function isGitTagConnected(
|
|
18
|
+
cwd: string,
|
|
19
|
+
tag: string
|
|
20
|
+
): Promise<boolean> {
|
|
21
|
+
try {
|
|
22
|
+
await execGit(cwd, [
|
|
23
|
+
'merge-base',
|
|
24
|
+
'--is-ancestor',
|
|
25
|
+
`refs/tags/${tag}^{commit}`,
|
|
26
|
+
'HEAD',
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
return true
|
|
30
|
+
} catch {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { execGit } from '#util'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check whether a given tag points to the provided commit.
|
|
8
|
+
*
|
|
9
|
+
* @param cwd Repository working directory
|
|
10
|
+
* @param tag Tag name to resolve
|
|
11
|
+
* @param commit Commit hash to compare against
|
|
12
|
+
* @returns `true` when the tag points at `commit`, otherwise `false`
|
|
13
|
+
*/
|
|
14
|
+
export async function isGitTagPointed(
|
|
15
|
+
cwd: string,
|
|
16
|
+
tag: string,
|
|
17
|
+
commit: string
|
|
18
|
+
): Promise<boolean> {
|
|
19
|
+
try {
|
|
20
|
+
await execGit(cwd, [
|
|
21
|
+
'merge-base',
|
|
22
|
+
'--is-ancestor',
|
|
23
|
+
`refs/tags/${tag}^{commit}`,
|
|
24
|
+
commit,
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
await execGit(cwd, [
|
|
28
|
+
'merge-base',
|
|
29
|
+
'--is-ancestor',
|
|
30
|
+
commit,
|
|
31
|
+
`refs/tags/${tag}^{commit}`,
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
return true
|
|
35
|
+
} catch {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { resolveGitRevision } from '../revision/resolveGitRevision'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Determine whether a specific git tag exists locally in the repository.
|
|
8
|
+
*
|
|
9
|
+
* @param cwd Working directory for the git command
|
|
10
|
+
* @param tag Name of the tag to evaluate
|
|
11
|
+
* @returns True if the tag reference exists locally, false otherwise
|
|
12
|
+
*/
|
|
13
|
+
export function isGitTagPresent(cwd: string, tag: string): Promise<boolean> {
|
|
14
|
+
return resolveGitRevision(cwd, `refs/tags/${tag}`)
|
|
15
|
+
.then(() => true)
|
|
16
|
+
.catch(() => false)
|
|
17
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
//
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
+
export * from './cache'
|
|
5
|
+
export * from './config'
|
|
6
|
+
export * from './git'
|
|
7
|
+
export * from './types'
|
|
8
|
+
export * from './util'
|
|
9
|
+
export * from './workflow'
|