mirror of
https://github.com/semantic-release/semantic-release.git
synced 2026-10-25 15:39:35 +00:00
perf(get-tags.js): bulk get for tags notes (#3732)
Co-authored-by: Matt Travi <126441+travi@users.noreply.github.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { escapeRegExp, template } from "lodash-es";
|
||||
import semver from "semver";
|
||||
import pReduce from "p-reduce";
|
||||
import debugTags from "debug";
|
||||
import { getNote, getTags } from "../../lib/git.js";
|
||||
import { getTags, getTagsNotes } from "../../lib/git.js";
|
||||
|
||||
const debug = debugTags("semantic-release:get-tags");
|
||||
|
||||
@@ -13,6 +13,9 @@ export default async ({ cwd, env, options: { tagFormat } }, branches) => {
|
||||
// so it's guaranteed to no be present in the `tagFormat`.
|
||||
const tagRegexp = `^${escapeRegExp(template(tagFormat)({ version: " " })).replace(" ", "(.+)")}`;
|
||||
|
||||
// Get the tags notes for all the tags in the repository
|
||||
const tagsNotesMap = await getTagsNotes({ cwd, env });
|
||||
|
||||
return pReduce(
|
||||
branches,
|
||||
async (branches, branch) => {
|
||||
@@ -20,8 +23,9 @@ export default async ({ cwd, env, options: { tagFormat } }, branches) => {
|
||||
await getTags(branch.name, { cwd, env }),
|
||||
async (branchTags, tag) => {
|
||||
const [, version] = tag.match(tagRegexp) || [];
|
||||
const channels = tagsNotesMap.has(tag) ? tagsNotesMap.get(tag).channels : [null];
|
||||
return version && semver.valid(semver.clean(version))
|
||||
? [...branchTags, { gitTag: tag, version, channels: (await getNote(tag, { cwd, env })).channels || [null] }]
|
||||
? [...branchTags, { gitTag: tag, version, channels }]
|
||||
: branchTags;
|
||||
},
|
||||
[]
|
||||
|
||||
+50
-29
@@ -4,6 +4,7 @@ import { execa } from "execa";
|
||||
import debugGit from "debug";
|
||||
import { merge } from "lodash-es";
|
||||
import { GIT_NOTE_REF } from "./definitions/constants.js";
|
||||
import { extractGitLogTags } from "./utils.js";
|
||||
|
||||
const debug = debugGit("semantic-release:git");
|
||||
|
||||
@@ -296,42 +297,62 @@ export async function isBranchUpToDate(repositoryUrl, branch, execaOptions) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get and parse the JSON note of a given reference.
|
||||
* Retrieves a map of Git tags to their associated notes from the repository.
|
||||
*
|
||||
* @param {String} ref The Git reference for which to retrieve the note.
|
||||
* @param {Object} [execaOpts] Options to pass to `execa`.
|
||||
* Executes a `git log` command to list tags and their notes, then parses the output into a Map
|
||||
* where each key is a tag name (e.g., "v24.2.3") and the value is the parsed JSON note object.
|
||||
*
|
||||
* @return {Object} the parsed JSON note if there is one, an empty object otherwise.
|
||||
* @async
|
||||
* @param {import('execa').Options} execaOptions - Options to pass to `execa`
|
||||
* @returns {Promise<Map<string, Object>>} A promise that resolves to a Map of tag names to their notes.
|
||||
*/
|
||||
export async function getNote(ref, execaOptions) {
|
||||
const handleError = (error) => {
|
||||
if (error.exitCode === 1) {
|
||||
return { stdout: "{}" };
|
||||
export async function getTagsNotes(execaOptions) {
|
||||
/**
|
||||
* git log --tags="*" --decorate-refs="refs/tags/*" --no-walk --format="%d%x09%N" --notes="refs/notes/semantic-release*"
|
||||
*
|
||||
* (tag: v1.2.3)
|
||||
* (tag: v2.0.0) {"channels":[null]}
|
||||
* (tag: v3.0.0, tag: 5833/merge) {"channels":[null]}
|
||||
* ...
|
||||
*/
|
||||
const { stdout } = await execa(
|
||||
"git",
|
||||
[
|
||||
"log",
|
||||
"--tags=*",
|
||||
"--decorate-refs=refs/tags/*", // This filters the refs shown in the %d format specifier to only include tags matching refs/tags/*.
|
||||
"--no-walk", // This ensures that only the commits directly pointed to by the tags are shown, not their historical parents.
|
||||
"--format=%d%x09%N", // <refName><tab><notes> eg. (tag: v24.2.3) {"channels":[null]}
|
||||
`--notes=refs/notes/${GIT_NOTE_REF}*`, // handles both patterns for notes - `semantic-release` (old) and `semantic-release-<version>` (current)
|
||||
],
|
||||
execaOptions
|
||||
);
|
||||
|
||||
// drop empty lines
|
||||
const lines = stdout.split("\n").filter((line) => line.trim() !== "");
|
||||
|
||||
// parse and create a map of tags to notes
|
||||
const tagNotesMap = new Map();
|
||||
|
||||
for (const line of lines) {
|
||||
const [tagPart, notePart] = line.trim().split("\t"); // tab separator is defined in the git command above (%x09)
|
||||
const tags = extractGitLogTags(tagPart);
|
||||
if (tags.length === 0) {
|
||||
debug(`Cannot parse tags from line: ${line}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
debug(error);
|
||||
throw error;
|
||||
};
|
||||
|
||||
try {
|
||||
return merge(
|
||||
JSON.parse(
|
||||
// Used for retro-compatibility
|
||||
(await execa("git", ["notes", "--ref", GIT_NOTE_REF, "show", ref], execaOptions).catch(handleError)).stdout
|
||||
),
|
||||
JSON.parse(
|
||||
(await execa("git", ["notes", "--ref", `${GIT_NOTE_REF}-${ref}`, "show", ref], execaOptions).catch(handleError))
|
||||
.stdout
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.exitCode === 1) {
|
||||
return {};
|
||||
try {
|
||||
const parsed = JSON.parse(notePart);
|
||||
tags.forEach((tag) => {
|
||||
tagNotesMap.set(tag, parsed);
|
||||
});
|
||||
} catch (error) {
|
||||
debug(error);
|
||||
}
|
||||
|
||||
debug(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return tagNotesMap;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+11
-1
@@ -1,4 +1,4 @@
|
||||
import { isFunction, template, union } from "lodash-es";
|
||||
import { template, union } from "lodash-es";
|
||||
import semver from "semver";
|
||||
import hideSensitive from "./hide-sensitive.js";
|
||||
|
||||
@@ -81,3 +81,13 @@ export function makeTag(tagFormat, version) {
|
||||
export function isSameChannel(channel, otherChannel) {
|
||||
return channel === otherChannel || (!channel && !otherChannel);
|
||||
}
|
||||
|
||||
export function extractGitLogTags(tagsString) {
|
||||
const regex = /tag: ([^,)]+)/g;
|
||||
let match;
|
||||
const tags = [];
|
||||
while ((match = regex.exec(tagsString)) !== null) {
|
||||
tags.push(match[1]);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
+19
-11
@@ -6,9 +6,9 @@ import {
|
||||
fetchNotes,
|
||||
getBranches,
|
||||
getGitHead,
|
||||
getNote,
|
||||
getTagHead,
|
||||
getTags,
|
||||
getTagsNotes,
|
||||
isBranchUpToDate,
|
||||
isGitRepo,
|
||||
isRefExists,
|
||||
@@ -332,31 +332,39 @@ test("Get a commit note", async (t) => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const { cwd } = await gitRepo();
|
||||
// Add commits to the master branch
|
||||
const commits = await gitCommits(["First"], { cwd });
|
||||
await gitCommits(["First"], { cwd });
|
||||
await gitTagVersion("v1.0.0", undefined, { cwd });
|
||||
|
||||
await gitAddNote(JSON.stringify({ note: "note" }), commits[0].hash, { cwd });
|
||||
await gitAddNote(JSON.stringify({ note: "note" }), "v1.0.0", { cwd });
|
||||
const tagsNotes = await getTagsNotes({ cwd });
|
||||
|
||||
t.deepEqual(await getNote(commits[0].hash, { cwd }), { note: "note" });
|
||||
t.deepEqual(tagsNotes.get("v1.0.0"), { note: "note" });
|
||||
});
|
||||
|
||||
test("Return empty object if there is no commit note", async (t) => {
|
||||
test("Return undefined if there is no commit note", async (t) => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const { cwd } = await gitRepo();
|
||||
// Add commits to the master branch
|
||||
const commits = await gitCommits(["First"], { cwd });
|
||||
await gitCommits(["First"], { cwd });
|
||||
await gitTagVersion("v1.0.0", undefined, { cwd });
|
||||
|
||||
t.deepEqual(await getNote(commits[0].hash, { cwd }), {});
|
||||
const tagsNotes = await getTagsNotes({ cwd });
|
||||
|
||||
t.deepEqual(tagsNotes.get("v1.0.0"), undefined);
|
||||
});
|
||||
|
||||
test("Throw error if a commit note in invalid", async (t) => {
|
||||
test("Return undefined if a commit note is invalid", async (t) => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const { cwd } = await gitRepo();
|
||||
// Add commits to the master branch
|
||||
const commits = await gitCommits(["First"], { cwd });
|
||||
await gitCommits(["First"], { cwd });
|
||||
await gitTagVersion("v1.0.0", undefined, { cwd });
|
||||
|
||||
await gitAddNote("non-json note", commits[0].hash, { cwd });
|
||||
await gitAddNote("non-json note", "v1.0.0", { cwd });
|
||||
|
||||
await t.throwsAsync(getNote(commits[0].hash, { cwd }));
|
||||
const tagsNotes = await getTagsNotes({ cwd });
|
||||
|
||||
t.deepEqual(tagsNotes.get("v1.0.0"), undefined);
|
||||
});
|
||||
|
||||
test("Add a commit note", async (t) => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
isSameChannel,
|
||||
lowest,
|
||||
makeTag,
|
||||
extractGitLogTags,
|
||||
tagsToVersions,
|
||||
} from "../lib/utils.js";
|
||||
|
||||
@@ -187,3 +188,8 @@ test("isSameChannel", (t) => {
|
||||
|
||||
t.false(isSameChannel("next", false));
|
||||
});
|
||||
|
||||
test("extractGitLogTags", (t) => {
|
||||
t.deepEqual(extractGitLogTags(`(tag: v1.2.3)`), ["v1.2.3"]);
|
||||
t.deepEqual(extractGitLogTags(`(tag: v1.2.3, tag: 5833/merge)`), ["v1.2.3", "5833/merge"]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user