@versatiles/release-tool 2.5.0 → 2.7.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.
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Performance benchmarking utilities for CLI operations.
3
+ *
4
+ * Provides timing measurement and optional logging for tracking
5
+ * execution duration of key operations.
6
+ */
7
+ /**
8
+ * Formats a duration in milliseconds to a human-readable string.
9
+ *
10
+ * @param ms - Duration in milliseconds
11
+ * @returns Formatted duration string (e.g., "1.23s", "456ms")
12
+ */
13
+ export function formatDuration(ms) {
14
+ if (ms >= 1000) {
15
+ return `${(ms / 1000).toFixed(2)}s`;
16
+ }
17
+ return `${Math.round(ms)}ms`;
18
+ }
19
+ /**
20
+ * Measures the execution time of a synchronous function.
21
+ *
22
+ * @param fn - The function to benchmark
23
+ * @param options - Optional benchmark configuration
24
+ * @returns The function result along with timing information
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const { result, durationMs } = benchmarkSync(() => {
29
+ * return heavyComputation();
30
+ * }, { label: 'Heavy computation', log: true });
31
+ * ```
32
+ */
33
+ export function benchmarkSync(fn, options = {}) {
34
+ const { label, log = false, logger = console.log } = options;
35
+ const start = performance.now();
36
+ const result = fn();
37
+ const end = performance.now();
38
+ const durationMs = end - start;
39
+ const durationFormatted = formatDuration(durationMs);
40
+ if (log && label) {
41
+ logger(`[benchmark] ${label}: ${durationFormatted}`);
42
+ }
43
+ return { result, durationMs, durationFormatted };
44
+ }
45
+ /**
46
+ * Measures the execution time of an asynchronous function.
47
+ *
48
+ * @param fn - The async function to benchmark
49
+ * @param options - Optional benchmark configuration
50
+ * @returns Promise resolving to the function result along with timing information
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * const { result, durationMs } = await benchmark(async () => {
55
+ * return await fetchData();
56
+ * }, { label: 'API fetch', log: true });
57
+ * ```
58
+ */
59
+ export async function benchmark(fn, options = {}) {
60
+ const { label, log = false, logger = console.log } = options;
61
+ const start = performance.now();
62
+ const result = await fn();
63
+ const end = performance.now();
64
+ const durationMs = end - start;
65
+ const durationFormatted = formatDuration(durationMs);
66
+ if (log && label) {
67
+ logger(`[benchmark] ${label}: ${durationFormatted}`);
68
+ }
69
+ return { result, durationMs, durationFormatted };
70
+ }
71
+ /**
72
+ * Creates a timer for manual timing control.
73
+ *
74
+ * @returns Timer object with start, stop, and elapsed methods
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * const timer = createTimer();
79
+ * timer.start();
80
+ * // ... do work ...
81
+ * const elapsed = timer.stop();
82
+ * console.log(`Operation took ${elapsed.durationFormatted}`);
83
+ * ```
84
+ */
85
+ export function createTimer() {
86
+ let startTime = null;
87
+ let endTime = null;
88
+ return {
89
+ start() {
90
+ startTime = performance.now();
91
+ endTime = null;
92
+ },
93
+ stop() {
94
+ if (startTime === null) {
95
+ throw new Error('Timer was not started');
96
+ }
97
+ endTime = performance.now();
98
+ const durationMs = endTime - startTime;
99
+ return { durationMs, durationFormatted: formatDuration(durationMs) };
100
+ },
101
+ elapsed() {
102
+ if (startTime === null) {
103
+ return 0;
104
+ }
105
+ const end = endTime ?? performance.now();
106
+ return end - startTime;
107
+ },
108
+ isRunning() {
109
+ return startTime !== null && endTime === null;
110
+ },
111
+ };
112
+ }
113
+ /**
114
+ * Calculates statistics from an array of benchmark durations.
115
+ *
116
+ * @param durations - Array of duration values in milliseconds
117
+ * @returns Statistical summary of the benchmarks
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * const durations = [100, 150, 120, 130, 110];
122
+ * const stats = calculateStats(durations);
123
+ * console.log(`Average: ${stats.avgFormatted}`);
124
+ * ```
125
+ */
126
+ export function calculateStats(durations) {
127
+ if (durations.length === 0) {
128
+ return {
129
+ count: 0,
130
+ totalMs: 0,
131
+ avgMs: 0,
132
+ minMs: 0,
133
+ maxMs: 0,
134
+ avgFormatted: '0ms',
135
+ };
136
+ }
137
+ const totalMs = durations.reduce((sum, d) => sum + d, 0);
138
+ const avgMs = totalMs / durations.length;
139
+ return {
140
+ count: durations.length,
141
+ totalMs,
142
+ avgMs,
143
+ minMs: Math.min(...durations),
144
+ maxMs: Math.max(...durations),
145
+ avgFormatted: formatDuration(avgMs),
146
+ };
147
+ }
148
+ //# sourceMappingURL=benchmark.js.map
@@ -0,0 +1,23 @@
1
+ import { type ParsedCommit } from './git.js';
2
+ /**
3
+ * Generates a changelog entry for a release from parsed commits.
4
+ *
5
+ * @param version - The version being released
6
+ * @param commits - Array of parsed conventional commits
7
+ * @param date - The release date (defaults to today)
8
+ * @returns Formatted changelog entry string
9
+ */
10
+ export declare function generateChangelogEntry(version: string, commits: ParsedCommit[], date?: Date): string;
11
+ /**
12
+ * Updates or creates a CHANGELOG.md file with a new release entry.
13
+ *
14
+ * @param directory - The project directory containing CHANGELOG.md
15
+ * @param version - The version being released
16
+ * @param commits - Array of parsed conventional commits
17
+ * @param date - The release date (defaults to today)
18
+ * @returns Object indicating whether the file was created or updated
19
+ */
20
+ export declare function updateChangelog(directory: string, version: string, commits: ParsedCommit[], date?: Date): {
21
+ created: boolean;
22
+ path: string;
23
+ };
@@ -0,0 +1,117 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
2
+ import { resolve } from 'path';
3
+ import { COMMIT_TYPES, groupCommitsByType } from './git.js';
4
+ /**
5
+ * Generates a changelog entry for a release from parsed commits.
6
+ *
7
+ * @param version - The version being released
8
+ * @param commits - Array of parsed conventional commits
9
+ * @param date - The release date (defaults to today)
10
+ * @returns Formatted changelog entry string
11
+ */
12
+ export function generateChangelogEntry(version, commits, date = new Date()) {
13
+ const dateStr = date.toISOString().split('T')[0];
14
+ const reversed = [...commits].reverse();
15
+ const grouped = groupCommitsByType(reversed);
16
+ let entry = `## [${version}] - ${dateStr}\n\n`;
17
+ // Order: breaking changes first, then features, fixes, and others
18
+ const typeOrder = [
19
+ 'feat',
20
+ 'fix',
21
+ 'perf',
22
+ 'refactor',
23
+ 'docs',
24
+ 'test',
25
+ 'build',
26
+ 'ci',
27
+ 'chore',
28
+ 'style',
29
+ 'revert',
30
+ 'other',
31
+ ];
32
+ // Add breaking changes section if any
33
+ const breakingCommits = reversed.filter((c) => c.breaking);
34
+ if (breakingCommits.length > 0) {
35
+ entry += '### Breaking Changes\n\n';
36
+ for (const commit of breakingCommits) {
37
+ entry += `- ${commit.description.replace(/\s+/g, ' ')}\n`;
38
+ }
39
+ entry += '\n';
40
+ }
41
+ // Add grouped commits
42
+ for (const type of typeOrder) {
43
+ const typeCommits = grouped.get(type);
44
+ if (!typeCommits || typeCommits.length === 0)
45
+ continue;
46
+ // Skip commits already shown in breaking changes
47
+ const nonBreaking = typeCommits.filter((c) => !c.breaking);
48
+ if (nonBreaking.length === 0)
49
+ continue;
50
+ const label = type === 'other' ? 'Other Changes' : COMMIT_TYPES[type];
51
+ entry += `### ${label}\n\n`;
52
+ for (const commit of nonBreaking) {
53
+ const scope = commit.scope ? `**${commit.scope}:** ` : '';
54
+ entry += `- ${scope}${commit.description.replace(/\s+/g, ' ')}\n`;
55
+ }
56
+ entry += '\n';
57
+ }
58
+ return entry;
59
+ }
60
+ /**
61
+ * Default changelog header for new CHANGELOG.md files
62
+ */
63
+ const DEFAULT_CHANGELOG_HEADER = `# Changelog
64
+
65
+ All notable changes to this project will be documented in this file.
66
+
67
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
68
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
69
+
70
+ `;
71
+ /**
72
+ * Updates or creates a CHANGELOG.md file with a new release entry.
73
+ *
74
+ * @param directory - The project directory containing CHANGELOG.md
75
+ * @param version - The version being released
76
+ * @param commits - Array of parsed conventional commits
77
+ * @param date - The release date (defaults to today)
78
+ * @returns Object indicating whether the file was created or updated
79
+ */
80
+ export function updateChangelog(directory, version, commits, date = new Date()) {
81
+ const changelogPath = resolve(directory, 'CHANGELOG.md');
82
+ const entry = generateChangelogEntry(version, commits, date);
83
+ let content;
84
+ let created = false;
85
+ if (existsSync(changelogPath)) {
86
+ const existing = readFileSync(changelogPath, 'utf8');
87
+ // Insert new entry after the header (after the first double newline or at the start if no header)
88
+ const headerEndIndex = findHeaderEnd(existing);
89
+ content = existing.slice(0, headerEndIndex) + entry + existing.slice(headerEndIndex);
90
+ }
91
+ else {
92
+ content = DEFAULT_CHANGELOG_HEADER + entry;
93
+ created = true;
94
+ }
95
+ writeFileSync(changelogPath, content);
96
+ return { created, path: changelogPath };
97
+ }
98
+ /**
99
+ * Finds the end of the changelog header section.
100
+ * Looks for patterns like "# Changelog" followed by description text,
101
+ * ending before the first "## " section header.
102
+ */
103
+ function findHeaderEnd(content) {
104
+ // Look for the first version section (## [x.y.z] or ## x.y.z)
105
+ const versionSectionMatch = content.match(/^## \[?\d+\.\d+\.\d+\]?/m);
106
+ if (versionSectionMatch?.index !== undefined) {
107
+ return versionSectionMatch.index;
108
+ }
109
+ // If no version section found, look for any ## heading
110
+ const anySectionMatch = content.match(/^## /m);
111
+ if (anySectionMatch?.index !== undefined) {
112
+ return anySectionMatch.index;
113
+ }
114
+ // No sections found, append at end (after ensuring trailing newline)
115
+ return content.endsWith('\n') ? content.length : content.length;
116
+ }
117
+ //# sourceMappingURL=changelog.js.map
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Error codes for categorizing different types of errors in the release tool.
3
+ */
4
+ export type VrtErrorCode = 'VALIDATION_ERROR' | 'MARKDOWN_ERROR' | 'GIT_ERROR' | 'RELEASE_ERROR' | 'NOT_IMPLEMENTED';
5
+ /**
6
+ * Custom error class for the VersaTiles Release Tool.
7
+ * Provides consistent error handling with categorization via error codes.
8
+ */
9
+ export declare class VrtError extends Error {
10
+ readonly code: VrtErrorCode;
11
+ constructor(message: string, code?: VrtErrorCode);
12
+ }
13
+ /**
14
+ * Helper function to create a validation error.
15
+ */
16
+ export declare function validationError(message: string): VrtError;
17
+ /**
18
+ * Helper function to create a markdown processing error.
19
+ */
20
+ export declare function markdownError(message: string): VrtError;
21
+ /**
22
+ * Helper function to create a git operation error.
23
+ */
24
+ export declare function gitError(message: string): VrtError;
25
+ /**
26
+ * Helper function to create a release process error.
27
+ */
28
+ export declare function releaseError(message: string): VrtError;
29
+ /**
30
+ * Helper function to create a not implemented error.
31
+ */
32
+ export declare function notImplementedError(feature: string): VrtError;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Custom error class for the VersaTiles Release Tool.
3
+ * Provides consistent error handling with categorization via error codes.
4
+ */
5
+ export class VrtError extends Error {
6
+ code;
7
+ constructor(message, code = 'VALIDATION_ERROR') {
8
+ super(message);
9
+ this.name = 'VrtError';
10
+ this.code = code;
11
+ // Maintains proper stack trace for where our error was thrown (only available on V8)
12
+ if (Error.captureStackTrace) {
13
+ Error.captureStackTrace(this, VrtError);
14
+ }
15
+ }
16
+ }
17
+ /**
18
+ * Helper function to create a validation error.
19
+ */
20
+ export function validationError(message) {
21
+ return new VrtError(message, 'VALIDATION_ERROR');
22
+ }
23
+ /**
24
+ * Helper function to create a markdown processing error.
25
+ */
26
+ export function markdownError(message) {
27
+ return new VrtError(message, 'MARKDOWN_ERROR');
28
+ }
29
+ /**
30
+ * Helper function to create a git operation error.
31
+ */
32
+ export function gitError(message) {
33
+ return new VrtError(message, 'GIT_ERROR');
34
+ }
35
+ /**
36
+ * Helper function to create a release process error.
37
+ */
38
+ export function releaseError(message) {
39
+ return new VrtError(message, 'RELEASE_ERROR');
40
+ }
41
+ /**
42
+ * Helper function to create a not implemented error.
43
+ */
44
+ export function notImplementedError(feature) {
45
+ return new VrtError(`Not implemented yet: "${feature}"`, 'NOT_IMPLEMENTED');
46
+ }
47
+ //# sourceMappingURL=errors.js.map
package/dist/lib/git.d.ts CHANGED
@@ -1,14 +1,106 @@
1
+ /**
2
+ * Represents a Git commit with its SHA, message, and optional tag.
3
+ */
1
4
  export interface Commit {
5
+ /** The full SHA hash of the commit. */
2
6
  sha: string;
7
+ /** The commit message (first line / subject). */
3
8
  message: string;
9
+ /** The tag associated with this commit, if any. */
4
10
  tag?: string;
5
11
  }
12
+ /**
13
+ * Conventional commit types and their display labels
14
+ */
15
+ export declare const COMMIT_TYPES: {
16
+ readonly feat: "Features";
17
+ readonly fix: "Bug Fixes";
18
+ readonly docs: "Documentation";
19
+ readonly style: "Styles";
20
+ readonly refactor: "Code Refactoring";
21
+ readonly perf: "Performance Improvements";
22
+ readonly test: "Tests";
23
+ readonly build: "Build System";
24
+ readonly ci: "CI/CD";
25
+ readonly chore: "Chores";
26
+ readonly revert: "Reverts";
27
+ };
28
+ export type CommitType = keyof typeof COMMIT_TYPES;
29
+ /**
30
+ * Represents a parsed conventional commit
31
+ */
32
+ export interface ParsedCommit extends Commit {
33
+ /** The conventional commit type (feat, fix, etc.) */
34
+ type?: CommitType;
35
+ /** The scope of the change (e.g., "api" in "feat(api): add endpoint") */
36
+ scope?: string;
37
+ /** The commit description without the type/scope prefix */
38
+ description: string;
39
+ /** Whether this is a breaking change */
40
+ breaking: boolean;
41
+ }
42
+ /**
43
+ * Parses a commit message according to the Conventional Commits specification.
44
+ * @see https://www.conventionalcommits.org/
45
+ *
46
+ * @param commit - The commit to parse
47
+ * @returns A ParsedCommit with type, scope, description, and breaking change info
48
+ */
49
+ export declare function parseConventionalCommit(commit: Commit): ParsedCommit;
50
+ /**
51
+ * Determines the suggested semver bump based on parsed commits.
52
+ * - Breaking changes -> major
53
+ * - Features -> minor
54
+ * - Everything else -> patch
55
+ *
56
+ * @param commits - Array of parsed commits
57
+ * @returns The suggested bump type: 'major', 'minor', or 'patch'
58
+ */
59
+ export declare function getSuggestedBump(commits: ParsedCommit[]): 'major' | 'minor' | 'patch';
60
+ /**
61
+ * Groups parsed commits by their type for release notes.
62
+ *
63
+ * @param commits - Array of parsed commits
64
+ * @returns A Map of commit type to array of commits, plus 'other' for non-conventional commits
65
+ */
66
+ export declare function groupCommitsByType(commits: ParsedCommit[]): Map<string, ParsedCommit[]>;
67
+ /**
68
+ * Interface for Git operations used by the release tool.
69
+ */
6
70
  export interface Git {
71
+ /**
72
+ * Gets the most recent semver tag from the repository.
73
+ * @returns The SHA and version string of the last tag, or undefined if no semver tags exist.
74
+ */
7
75
  getLastGitHubTag: () => Promise<{
8
76
  sha: string;
9
77
  version: string;
10
78
  } | undefined>;
79
+ /**
80
+ * Gets the current (most recent) commit.
81
+ * @returns The current commit object.
82
+ */
11
83
  getCurrentGitHubCommit: () => Promise<Commit>;
84
+ /**
85
+ * Gets all commits between two commit SHAs.
86
+ * @param shaLast - The older commit SHA (exclusive).
87
+ * @param shaCurrent - The newer commit SHA (inclusive).
88
+ * @returns Array of commits between the two SHAs.
89
+ */
12
90
  getCommitsBetween: (shaLast?: string, shaCurrent?: string) => Promise<Commit[]>;
13
91
  }
92
+ /**
93
+ * Creates a Git interface for the specified directory.
94
+ * Provides methods to query commit history and tags.
95
+ *
96
+ * @param cwd - The working directory of the Git repository.
97
+ * @returns An object with methods to interact with the Git repository.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * const git = getGit('/path/to/repo');
102
+ * const lastTag = await git.getLastGitHubTag();
103
+ * const commits = await git.getCommitsBetween(lastTag?.sha, 'HEAD');
104
+ * ```
105
+ */
14
106
  export declare function getGit(cwd: string): Git;
package/dist/lib/git.js CHANGED
@@ -1,4 +1,100 @@
1
1
  import { Shell } from './shell.js';
2
+ /**
3
+ * Conventional commit types and their display labels
4
+ */
5
+ export const COMMIT_TYPES = {
6
+ feat: 'Features',
7
+ fix: 'Bug Fixes',
8
+ docs: 'Documentation',
9
+ style: 'Styles',
10
+ refactor: 'Code Refactoring',
11
+ perf: 'Performance Improvements',
12
+ test: 'Tests',
13
+ build: 'Build System',
14
+ ci: 'CI/CD',
15
+ chore: 'Chores',
16
+ revert: 'Reverts',
17
+ };
18
+ /**
19
+ * Parses a commit message according to the Conventional Commits specification.
20
+ * @see https://www.conventionalcommits.org/
21
+ *
22
+ * @param commit - The commit to parse
23
+ * @returns A ParsedCommit with type, scope, description, and breaking change info
24
+ */
25
+ export function parseConventionalCommit(commit) {
26
+ const message = commit.message.trim();
27
+ // Pattern: type(scope)!: description OR type!: description OR type(scope): description OR type: description
28
+ // Note: No space allowed before colon per Conventional Commits spec
29
+ const conventionalPattern = /^(\w+)(?:\(([^)]+)\))?(!)?:\s*(.+)$/;
30
+ const match = message.match(conventionalPattern);
31
+ if (!match) {
32
+ // Not a conventional commit - return with original message as description
33
+ return {
34
+ ...commit,
35
+ description: message,
36
+ breaking: message.toUpperCase().includes('BREAKING CHANGE'),
37
+ };
38
+ }
39
+ const [, typeStr, scope, breakingMark, description] = match;
40
+ const type = typeStr.toLowerCase();
41
+ const isKnownType = type in COMMIT_TYPES;
42
+ return {
43
+ ...commit,
44
+ type: isKnownType ? type : undefined,
45
+ scope: scope || undefined,
46
+ description: description.trim(),
47
+ breaking: breakingMark === '!' || message.toUpperCase().includes('BREAKING CHANGE'),
48
+ };
49
+ }
50
+ /**
51
+ * Determines the suggested semver bump based on parsed commits.
52
+ * - Breaking changes -> major
53
+ * - Features -> minor
54
+ * - Everything else -> patch
55
+ *
56
+ * @param commits - Array of parsed commits
57
+ * @returns The suggested bump type: 'major', 'minor', or 'patch'
58
+ */
59
+ export function getSuggestedBump(commits) {
60
+ const hasBreaking = commits.some((c) => c.breaking);
61
+ if (hasBreaking)
62
+ return 'major';
63
+ const hasFeature = commits.some((c) => c.type === 'feat');
64
+ if (hasFeature)
65
+ return 'minor';
66
+ return 'patch';
67
+ }
68
+ /**
69
+ * Groups parsed commits by their type for release notes.
70
+ *
71
+ * @param commits - Array of parsed commits
72
+ * @returns A Map of commit type to array of commits, plus 'other' for non-conventional commits
73
+ */
74
+ export function groupCommitsByType(commits) {
75
+ const groups = new Map();
76
+ for (const commit of commits) {
77
+ const key = commit.type ?? 'other';
78
+ const existing = groups.get(key) ?? [];
79
+ existing.push(commit);
80
+ groups.set(key, existing);
81
+ }
82
+ return groups;
83
+ }
84
+ /**
85
+ * Creates a Git interface for the specified directory.
86
+ * Provides methods to query commit history and tags.
87
+ *
88
+ * @param cwd - The working directory of the Git repository.
89
+ * @returns An object with methods to interact with the Git repository.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * const git = getGit('/path/to/repo');
94
+ * const lastTag = await git.getLastGitHubTag();
95
+ * const commits = await git.getCommitsBetween(lastTag?.sha, 'HEAD');
96
+ * ```
97
+ */
2
98
  export function getGit(cwd) {
3
99
  const shell = new Shell(cwd);
4
100
  return {
@@ -6,22 +102,30 @@ export function getGit(cwd) {
6
102
  getCurrentGitHubCommit,
7
103
  getCommitsBetween,
8
104
  };
105
+ /**
106
+ * Finds the most recent commit tagged with a semver version (vX.Y.Z format).
107
+ * Supports pre-release and build metadata suffixes.
108
+ */
9
109
  async function getLastGitHubTag() {
10
110
  const commits = await getAllCommits();
11
111
  const result = commits
12
- .map(commit => ({
112
+ .map((commit) => ({
13
113
  sha: commit.sha,
14
- version: commit.tag?.match(/^v(\d+\.\d+\.\d+)$/)?.[1],
114
+ version: commit.tag?.match(/^v(\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?)$/)?.[1],
15
115
  }))
16
- .find(r => r.version);
116
+ .find((r) => r.version);
17
117
  return result;
18
118
  }
119
+ /**
120
+ * Retrieves all commits from the repository's history.
121
+ * Parses the git log output to extract SHA, message, and tag information.
122
+ */
19
123
  async function getAllCommits() {
20
- const result = await shell.stdout('git log --pretty=format:\'⍃%H⍄%s⍄%D⍄\'');
124
+ const result = await shell.stdout("git log --pretty=format:'⍃%H⍄%s⍄%D'");
21
125
  return result
22
126
  .split('⍃')
23
- .filter(line => line.length > 2)
24
- .map(line => {
127
+ .filter((line) => line.length > 2)
128
+ .map((line) => {
25
129
  const obj = line.split('⍄');
26
130
  return {
27
131
  sha: obj[0],
@@ -30,15 +134,23 @@ export function getGit(cwd) {
30
134
  };
31
135
  });
32
136
  }
137
+ /**
138
+ * Returns the most recent commit in the repository.
139
+ */
33
140
  async function getCurrentGitHubCommit() {
34
141
  return (await getAllCommits())[0];
35
142
  }
143
+ /**
144
+ * Gets commits between two SHA hashes.
145
+ * If shaCurrent is provided, starts from that commit.
146
+ * If shaLast is provided, stops before that commit.
147
+ */
36
148
  async function getCommitsBetween(shaLast, shaCurrent) {
37
149
  let commits = await getAllCommits();
38
- const start = commits.findIndex(commit => commit.sha === shaCurrent);
150
+ const start = commits.findIndex((commit) => commit.sha === shaCurrent);
39
151
  if (start >= 0)
40
152
  commits = commits.slice(start);
41
- const end = commits.findIndex(commit => commit.sha === shaLast);
153
+ const end = commits.findIndex((commit) => commit.sha === shaLast);
42
154
  if (end >= 0)
43
155
  commits = commits.slice(0, end);
44
156
  return commits;