snapshot of https://github.com/go-gitea/gitea.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
cache-seeder / prune (push) Has been skipped
cache-seeder / frontend (push) Failing after 3s
cache-seeder / python (push) Failing after 3s
cache-seeder / gobuild (push) Failing after 4s
cache-seeder / lint (bindata, lint-backend) (push) Failing after 3s
giteabot backport / giteabot (push) Has been skipped
giteabot / giteabot (push) Has been skipped
cache-prune / prune (push) Has been skipped
release-nightly-snapcraft / build-and-publish (ubuntu-24.04) (push) Has been cancelled
release-nightly-snapcraft / build-and-publish (ubuntu-24.04-arm) (push) Has been cancelled
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled
cron-renovate / cron-renovate (push) Has been skipped
Some checks failed
cache-seeder / prune (push) Has been skipped
cache-seeder / frontend (push) Failing after 3s
cache-seeder / python (push) Failing after 3s
cache-seeder / gobuild (push) Failing after 4s
cache-seeder / lint (bindata, lint-backend) (push) Failing after 3s
giteabot backport / giteabot (push) Has been skipped
giteabot / giteabot (push) Has been skipped
cache-prune / prune (push) Has been skipped
release-nightly-snapcraft / build-and-publish (ubuntu-24.04) (push) Has been cancelled
release-nightly-snapcraft / build-and-publish (ubuntu-24.04-arm) (push) Has been cancelled
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled
cron-renovate / cron-renovate (push) Has been skipped
This commit is contained in:
72
tools/build-release.sh
Executable file
72
tools/build-release.sh
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
GO="${GO:-go}"
|
||||
RELEASE_LDFLAGS="-s -w $LDFLAGS"
|
||||
RELEASE_TAGS="bindata $TAGS"
|
||||
RELEASE_PATH_PREFIX="${DIST}/binaries/gitea-${VERSION##*/}"
|
||||
|
||||
RELEASE_PLATFORMS_DEFAULT=(
|
||||
linux/amd64 linux/arm-5 linux/arm-6 linux/arm64 linux/riscv64 \
|
||||
windows/amd64 windows/arm64 \
|
||||
darwin/amd64 darwin/arm64 \
|
||||
freebsd/amd64
|
||||
)
|
||||
|
||||
RELEASE_PLATFORMS_GOGIT=(windows/amd64 windows/arm64)
|
||||
|
||||
build() {
|
||||
echo "building ${*} ..."
|
||||
local target="${1:?}"
|
||||
local variant="${2:-}"
|
||||
|
||||
local goos="${target%%/*}"
|
||||
local goarch="${target##*/}"
|
||||
local goarm=""
|
||||
if [[ "$goarch" == arm-* ]]; then
|
||||
goarm="${goarch#arm-}"
|
||||
goarch="arm"
|
||||
fi
|
||||
|
||||
local tags="${RELEASE_TAGS}"
|
||||
local suffix
|
||||
if [[ "$variant" == "gogit" ]]; then
|
||||
tags="gogit${tags:+ ${tags}}"
|
||||
suffix="-gogit-${target//\//-}"
|
||||
else
|
||||
suffix="-${target//\//-}"
|
||||
fi
|
||||
|
||||
local output="${RELEASE_PATH_PREFIX}${suffix}"
|
||||
local args=()
|
||||
if [[ "$goos" == "windows" ]]; then
|
||||
output="${output}.exe"
|
||||
fi
|
||||
args+=(-tags "$tags")
|
||||
args+=(-ldflags "$RELEASE_LDFLAGS")
|
||||
echo " args: ${args[*]}"
|
||||
echo " output: ${output}"
|
||||
# must disable CGO (host compiler & linker) to get host-independent static builds
|
||||
CGO_ENABLED=0 GOOS="$goos" GOARCH="$goarch" GOARM="$goarm" "$GO" build "${args[@]}" -o "$output" .
|
||||
}
|
||||
|
||||
main() {
|
||||
# When building release binaries, some TAGS (bindata) are needed by the release script,
|
||||
# so here it needs to use the TAGS to generate the assets (embed bindata).
|
||||
echo "generating assets with tags=${RELEASE_TAGS}"
|
||||
"$GO" generate -tags "${RELEASE_TAGS}" ./...
|
||||
local platform="${1:-}"
|
||||
for target in "${RELEASE_PLATFORMS_DEFAULT[@]}"; do
|
||||
if [[ -z "$platform" || "$target" == "$platform/"* ]]; then
|
||||
build "$target"
|
||||
fi
|
||||
done
|
||||
|
||||
for target in "${RELEASE_PLATFORMS_GOGIT[@]}"; do
|
||||
if [[ -z "$platform" || "$target" == "$platform/"* ]]; then
|
||||
build "$target" "gogit"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
main "$@"
|
||||
181
tools/ci-tools.ts
Normal file
181
tools/ci-tools.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env node
|
||||
import {argv, env, exit} from 'node:process';
|
||||
|
||||
const allowedTypes = [
|
||||
'build',
|
||||
'chore',
|
||||
'ci',
|
||||
'docs',
|
||||
'enhance',
|
||||
'feat',
|
||||
'fix',
|
||||
'perf',
|
||||
'refactor',
|
||||
'revert',
|
||||
'style',
|
||||
'test',
|
||||
] as const;
|
||||
type CommitType = typeof allowedTypes[number];
|
||||
|
||||
const allowedTypesList = allowedTypes.join(', ');
|
||||
const titlePattern = new RegExp(`^(${allowedTypes.join('|')})(\\([\\w/.-]+\\))?(!)?: .+$`);
|
||||
|
||||
function parsePrTitle(title: string): {type: CommitType, scope: string, breaking: boolean} | null {
|
||||
const match = titlePattern.exec(title);
|
||||
if (!match) return null;
|
||||
// Keep the first segment, so "webhook/discord" matches "webhook".
|
||||
const scope = match[2] ? match[2].slice(1, -1).toLowerCase().split('/')[0] : '';
|
||||
return {type: match[1] as CommitType, scope, breaking: Boolean(match[3])};
|
||||
}
|
||||
|
||||
const breakingLabel = 'pr/breaking';
|
||||
|
||||
// Mutually exclusive type labels, fully synced with the title type (added and removed).
|
||||
const typeLabels: Partial<Record<CommitType, string>> = {
|
||||
feat: 'type/feature',
|
||||
enhance: 'type/enhancement',
|
||||
fix: 'type/bug',
|
||||
docs: 'type/docs',
|
||||
test: 'type/testing',
|
||||
refactor: 'type/refactoring',
|
||||
};
|
||||
|
||||
// Non-type labels, removed only when the previous title implied them.
|
||||
const extraLabels: Partial<Record<CommitType, string>> = {
|
||||
chore: 'skip-changelog',
|
||||
ci: 'skip-changelog',
|
||||
build: 'topic/build',
|
||||
};
|
||||
|
||||
// Scopes whose label differs from "topic/<scope>".
|
||||
const scopeAliases: Record<string, string> = {
|
||||
actions: 'topic/gitea-actions',
|
||||
auth: 'topic/authentication',
|
||||
issue: 'topic/issues',
|
||||
markup: 'topic/content-rendering',
|
||||
oauth: 'topic/authentication',
|
||||
oauth2: 'topic/authentication',
|
||||
pull: 'topic/pr',
|
||||
pulls: 'topic/pr',
|
||||
webhook: 'topic/webhooks',
|
||||
};
|
||||
|
||||
function labelForScope(scope: string): string | undefined {
|
||||
if (!scope) return undefined;
|
||||
return scopeAliases[scope] ?? `topic/${scope}`;
|
||||
}
|
||||
|
||||
// Labels this tool may remove when the title no longer implies them.
|
||||
const removableLabels = [...Object.values(typeLabels), breakingLabel];
|
||||
|
||||
function labelsForPrTitle(title: string): string[] {
|
||||
const parsed = parsePrTitle(title);
|
||||
if (!parsed) return [];
|
||||
const labels = [
|
||||
typeLabels[parsed.type],
|
||||
extraLabels[parsed.type],
|
||||
labelForScope(parsed.scope),
|
||||
parsed.breaking ? breakingLabel : undefined,
|
||||
].filter((label): label is string => label !== undefined);
|
||||
return [...new Set(labels)];
|
||||
}
|
||||
|
||||
// Command: validate PR_TITLE against the allowed Conventional Commits format.
|
||||
function lintPrTitle(): void {
|
||||
if (!env.PR_TITLE) {
|
||||
console.error('Missing PR_TITLE');
|
||||
exit(1);
|
||||
}
|
||||
if (!parsePrTitle(env.PR_TITLE)) {
|
||||
console.error(`Invalid PR title: ${env.PR_TITLE}`);
|
||||
console.error('Expected format: type(scope): subject (scope optional, append "!" for breaking changes)');
|
||||
console.error(`Allowed types: ${allowedTypesList}`);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Command: sync the title-derived labels onto the PR via the GitHub API.
|
||||
async function setPrLabels(): Promise<void> {
|
||||
if (!env.PR_TITLE || !env.GITHUB_TOKEN || !env.GITHUB_REPOSITORY || !env.PR_NUMBER) {
|
||||
console.error('set-pr-labels requires PR_TITLE, GITHUB_TOKEN, GITHUB_REPOSITORY and PR_NUMBER');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
const labelsUrl = `https://api.github.com/repos/${env.GITHUB_REPOSITORY}/issues/${env.PR_NUMBER}/labels`;
|
||||
|
||||
async function request(url: string, method = 'GET', body?: unknown, ignoreStatus?: number): Promise<Response> {
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
Accept: 'application/vnd.github+json',
|
||||
Authorization: `Bearer ${env.GITHUB_TOKEN}`,
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
...(Boolean(body) && {'Content-Type': 'application/json'}),
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
if (!response.ok && response.status !== ignoreStatus) {
|
||||
throw new Error(`GitHub API ${method} ${url} failed (${response.status}): ${await response.text()}`);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
async function fetchNames(url: string): Promise<string[]> {
|
||||
const names = [];
|
||||
for (let page = 1; page <= 10; page++) {
|
||||
const labels = (await (await request(`${url}?per_page=100&page=${page}`)).json()) as Array<{name: string}>;
|
||||
names.push(...labels.map((label) => label.name));
|
||||
if (labels.length < 100) break;
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
// Labels the repo does not have are dropped instead of failing the job.
|
||||
const candidates = labelsForPrTitle(env.PR_TITLE);
|
||||
const repoLabelsUrl = `https://api.github.com/repos/${env.GITHUB_REPOSITORY}/labels`;
|
||||
const [current, existing]: string[][] = await Promise.all([
|
||||
fetchNames(labelsUrl),
|
||||
candidates.length ? fetchNames(repoLabelsUrl) : [],
|
||||
]);
|
||||
const desired = candidates.filter((name) => {
|
||||
if (existing.includes(name)) return true;
|
||||
console.info(`Skipping label not present on the repo: ${name}`);
|
||||
return false;
|
||||
});
|
||||
|
||||
const stale = new Set([...removableLabels, ...labelsForPrTitle(env.PR_TITLE_BEFORE ?? '')]);
|
||||
const toAdd = desired.filter((name) => !current.includes(name));
|
||||
const toRemove = current.filter((name) => stale.has(name) && !desired.includes(name));
|
||||
|
||||
if (toAdd.length) {
|
||||
await request(labelsUrl, 'POST', {labels: toAdd});
|
||||
console.info(`Added labels: ${toAdd.join(', ')}`);
|
||||
}
|
||||
for (const name of toRemove) {
|
||||
// 404 means the label is already gone.
|
||||
await request(`${labelsUrl}/${encodeURIComponent(name)}`, 'DELETE', undefined, 404);
|
||||
console.info(`Removed label: ${name}`);
|
||||
}
|
||||
if (!toAdd.length && !toRemove.length) {
|
||||
console.info('PR labels already in sync');
|
||||
}
|
||||
}
|
||||
|
||||
const commands: Record<string, () => void | Promise<void>> = {
|
||||
'lint-pr-title': lintPrTitle,
|
||||
'set-pr-labels': setPrLabels,
|
||||
};
|
||||
|
||||
const command = argv[2];
|
||||
const handler = commands[command];
|
||||
if (!handler) {
|
||||
console.error(`Usage: ci-tools.ts <${Object.keys(commands).join('|')}>`);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
await handler();
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
exit(1);
|
||||
}
|
||||
85
tools/eslint-rules/unescaped-html-literal.test.ts
Normal file
85
tools/eslint-rules/unescaped-html-literal.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
// MIT license, Copyright (c) GitHub, Inc.
|
||||
// https://github.com/github/eslint-plugin-github/blob/main/lib/rules/unescaped-html-literal.js
|
||||
/* eslint-disable no-template-curly-in-string -- the fixtures are template literal sources inside plain strings */
|
||||
import rule from './unescaped-html-literal.ts';
|
||||
import {RuleTester} from 'eslint';
|
||||
|
||||
class VitestRuleTester extends RuleTester {
|
||||
static describe = describe;
|
||||
static it = it;
|
||||
static itOnly = it.only;
|
||||
}
|
||||
|
||||
const ruleTester = new VitestRuleTester();
|
||||
|
||||
ruleTester.run('unescaped-html-literal', rule, {
|
||||
valid: [
|
||||
{
|
||||
code: '`Hello World!`;',
|
||||
languageOptions: {ecmaVersion: 2017},
|
||||
},
|
||||
{
|
||||
code: "'Hello World!'",
|
||||
languageOptions: {ecmaVersion: 2017},
|
||||
},
|
||||
{
|
||||
code: '"Hello World!"',
|
||||
languageOptions: {ecmaVersion: 2017},
|
||||
},
|
||||
{
|
||||
code: 'const helloTemplate = () => html`<div>Hello World!</div>`;',
|
||||
languageOptions: {ecmaVersion: 2017},
|
||||
},
|
||||
{
|
||||
code: 'const helloTemplate = (name) => html`<div>Hello ${name}!</div>`;',
|
||||
languageOptions: {ecmaVersion: 2017},
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: "const helloHTML = '<div>Hello, World!</div>'",
|
||||
languageOptions: {ecmaVersion: 2017},
|
||||
errors: [
|
||||
{
|
||||
message: 'Unescaped HTML literal. Use html`` tag template literal for secure escaping.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'const helloHTML = "<h1>Hello, World!</h1>"',
|
||||
languageOptions: {ecmaVersion: 2017},
|
||||
errors: [
|
||||
{
|
||||
message: 'Unescaped HTML literal. Use html`` tag template literal for secure escaping.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'const helloHTML = `<div>Hello ${name}!</div>`',
|
||||
languageOptions: {ecmaVersion: 2017},
|
||||
errors: [
|
||||
{
|
||||
message: 'Unescaped HTML literal. Use html`` tag template literal for secure escaping.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'const helloHTML = ` \n\t<div>Hello ${name}!</div>`',
|
||||
languageOptions: {ecmaVersion: 2017},
|
||||
errors: [
|
||||
{
|
||||
message: 'Unescaped HTML literal. Use html`` tag template literal for secure escaping.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'const helloHTML = foo`<div>Hello ${name}!</div>`',
|
||||
languageOptions: {ecmaVersion: 2017},
|
||||
errors: [
|
||||
{
|
||||
message: 'Unescaped HTML literal. Use html`` tag template literal for secure escaping.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
39
tools/eslint-rules/unescaped-html-literal.ts
Normal file
39
tools/eslint-rules/unescaped-html-literal.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// MIT license, Copyright (c) GitHub, Inc.
|
||||
// https://github.com/github/eslint-plugin-github/blob/main/lib/rules/unescaped-html-literal.js
|
||||
import type {JSRuleDefinition, JSRuleDefinitionTypeOptions} from 'eslint';
|
||||
|
||||
const htmlOpenTag = /^\s*<[a-zA-Z]/;
|
||||
|
||||
const rule: JSRuleDefinition<JSRuleDefinitionTypeOptions> = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
messages: {
|
||||
unescapedHtmlLiteral: 'Unescaped HTML literal. Use html`` tag template literal for secure escaping.',
|
||||
},
|
||||
},
|
||||
|
||||
create: (context) => ({
|
||||
Literal(node) {
|
||||
if (typeof node.value !== 'string' || !htmlOpenTag.test(node.value)) return;
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'unescapedHtmlLiteral',
|
||||
});
|
||||
},
|
||||
TemplateLiteral(node) {
|
||||
const templateStart = node.quasis[0]?.value.raw;
|
||||
if (!templateStart || !htmlOpenTag.test(templateStart)) return;
|
||||
|
||||
const parent = node.parent;
|
||||
if (parent?.type === 'TaggedTemplateExpression' && parent.tag.type === 'Identifier' && parent.tag.name === 'html') return;
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'unescapedHtmlLiteral',
|
||||
});
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export default rule;
|
||||
84
tools/generate-codemirror-languages.ts
Executable file
84
tools/generate-codemirror-languages.ts
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env node
|
||||
import {writeFile} from 'node:fs/promises';
|
||||
import {languages as cmLanguages} from '@codemirror/language-data';
|
||||
import * as linguist from 'linguist-languages';
|
||||
import type {Language} from 'linguist-languages';
|
||||
|
||||
const renames: Record<string, string> = {
|
||||
'Protocol Buffer': 'ProtoBuf',
|
||||
};
|
||||
|
||||
// Languages whose entry is constructed manually in the runtime; skip during generation.
|
||||
const skipNames = new Set(['Dockerfile', 'Markdown']);
|
||||
|
||||
// Extensions claimed by several unrelated languages with no good default; strip globally.
|
||||
const ambiguousExt = new Set(['cgi', 'fcgi', 'inc']);
|
||||
|
||||
// Per-language drops for non-text formats (.frm = binary VB6 forms) or where Linguist's
|
||||
// primary owner conflicts with a more specialised CodeMirror mode (.spec → RPM Spec).
|
||||
const excludeExt: Record<string, string[]> = {
|
||||
'INI': ['frm'],
|
||||
'Python': ['spec'],
|
||||
'Ruby': ['spec'],
|
||||
};
|
||||
|
||||
// Per-CM-language additions for filenames Linguist classifies as separate languages
|
||||
// (.editorconfig, .gitconfig, .npmrc) or omits entirely (Snakefile).
|
||||
const extraFilenames: Record<string, string[]> = {
|
||||
'Properties files': ['.editorconfig', '.gitconfig', '.npmrc'],
|
||||
'Python': ['Snakefile'],
|
||||
};
|
||||
|
||||
// Per-CM-language additions widely used in practice but absent from Linguist's list.
|
||||
const extraExtensions: Record<string, string[]> = {
|
||||
'Properties files': ['conf'],
|
||||
};
|
||||
|
||||
type CmLanguage = {
|
||||
name: string;
|
||||
extensions: string[];
|
||||
filenames: string[];
|
||||
};
|
||||
|
||||
const cmByAlias = new Map<string, string>();
|
||||
// Map of extension -> the CM language that originally owns it. Used to prevent Linguist
|
||||
// from broadening one language's extension claim into another's territory (e.g. Linguist's
|
||||
// PLSQL lists .sql, but CM's SQL is the canonical owner).
|
||||
const cmOriginalExtOwner = new Map<string, string>();
|
||||
for (const lang of cmLanguages) {
|
||||
cmByAlias.set(lang.name.toLowerCase(), lang.name);
|
||||
for (const a of lang.alias) cmByAlias.set(a.toLowerCase(), lang.name);
|
||||
for (const ext of lang.extensions) {
|
||||
if (!cmOriginalExtOwner.has(ext)) cmOriginalExtOwner.set(ext, lang.name);
|
||||
}
|
||||
}
|
||||
|
||||
const out: CmLanguage[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const [linguistName, entry] of Object.entries(linguist as Record<string, Language>)) {
|
||||
const cmName = renames[linguistName] ?? cmByAlias.get(linguistName.toLowerCase());
|
||||
// Multiple Linguist entries can alias to the same CM language (e.g. JSON5 → JSON).
|
||||
if (!cmName || skipNames.has(cmName) || seen.has(cmName)) continue;
|
||||
seen.add(cmName);
|
||||
const exExt = new Set(excludeExt[linguistName]);
|
||||
// CodeMirror's matchFilename uses /\.([^.]+)$/, so multi-dot extensions like
|
||||
// ".cmake.in" can't match as extensions and are dropped here.
|
||||
const extensions = (entry.extensions ?? [])
|
||||
.map((e) => e.replace(/^\./, ''))
|
||||
.filter((e) => {
|
||||
if (e.includes('.') || ambiguousExt.has(e) || exExt.has(e)) return false;
|
||||
const owner = cmOriginalExtOwner.get(e);
|
||||
return !owner || owner === cmName;
|
||||
});
|
||||
out.push({
|
||||
name: cmName,
|
||||
extensions: [...extensions, ...(extraExtensions[cmName] ?? [])],
|
||||
filenames: [...(entry.filenames ?? []), ...(extraFilenames[cmName] ?? [])],
|
||||
});
|
||||
}
|
||||
|
||||
out.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const outPath = new URL('../assets/codemirror-languages.json', import.meta.url);
|
||||
await writeFile(outPath, `${JSON.stringify(out, null, 2)}\n`);
|
||||
console.info(`wrote ${out.length} languages to ${outPath.pathname}`);
|
||||
77
tools/generate-images.ts
Executable file
77
tools/generate-images.ts
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env node
|
||||
import {initWasm, Resvg} from '@resvg/resvg-wasm';
|
||||
import {optimize} from 'svgo';
|
||||
import {mkdir, readFile, writeFile} from 'node:fs/promises';
|
||||
import {argv, exit} from 'node:process';
|
||||
|
||||
async function generateMailIcon(icon: string, name: string, color: string) {
|
||||
await generate(
|
||||
(await readFile(
|
||||
new URL(import.meta.resolve(`${icon.replace('octicon-', '@primer/octicons/build/svg/')}.svg`)),
|
||||
'utf8',
|
||||
)).replace('<svg ', `<svg fill="${color}" `),
|
||||
`../services/mailer/icons/${name}.png`,
|
||||
{size: 48},
|
||||
);
|
||||
}
|
||||
|
||||
async function generate(svg: string, path: string, {size, bg}: {size: number, bg?: boolean}) {
|
||||
const outputFile = new URL(path, import.meta.url);
|
||||
|
||||
if (outputFile.href.endsWith('.svg')) {
|
||||
const {data} = optimize(svg, {
|
||||
plugins: [
|
||||
'preset-default',
|
||||
'removeDimensions',
|
||||
{
|
||||
name: 'addAttributesToSVGElement',
|
||||
params: {
|
||||
attributes: [{width: String(size)}, {height: String(size)}],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
await writeFile(outputFile, data);
|
||||
return;
|
||||
}
|
||||
|
||||
const resvgJS = new Resvg(svg, {
|
||||
fitTo: {
|
||||
mode: 'width',
|
||||
value: size,
|
||||
},
|
||||
...(bg && {background: 'white'}),
|
||||
});
|
||||
const renderedImage = resvgJS.render();
|
||||
const pngBytes = renderedImage.asPng();
|
||||
await writeFile(outputFile, Buffer.from(pngBytes));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const gitea = argv.slice(2).includes('gitea');
|
||||
const logoSvg = await readFile(new URL('../assets/logo.svg', import.meta.url), 'utf8');
|
||||
const faviconSvg = await readFile(new URL('../assets/favicon.svg', import.meta.url), 'utf8');
|
||||
await initWasm(await readFile(new URL(import.meta.resolve('@resvg/resvg-wasm/index_bg.wasm'))));
|
||||
await mkdir(new URL('../services/mailer/icons/', import.meta.url), {recursive: true});
|
||||
|
||||
await Promise.all([
|
||||
generateMailIcon('octicon-check-circle-fill-16', 'status-success', '#2da44e'),
|
||||
generateMailIcon('octicon-x-circle-fill-16', 'status-failure', '#e5534b'),
|
||||
generateMailIcon('octicon-stop-16', 'status-cancelled', '#8c959f'),
|
||||
generateMailIcon('octicon-skip-16', 'status-skipped', '#8c959f'),
|
||||
generate(logoSvg, '../public/assets/img/logo.svg', {size: 32}),
|
||||
generate(logoSvg, '../public/assets/img/logo.png', {size: 512}),
|
||||
generate(faviconSvg, '../public/assets/img/favicon.svg', {size: 32}),
|
||||
generate(faviconSvg, '../public/assets/img/favicon.png', {size: 180}),
|
||||
generate(logoSvg, '../public/assets/img/avatar_default.png', {size: 200}),
|
||||
generate(logoSvg, '../public/assets/img/apple-touch-icon.png', {size: 180, bg: true}),
|
||||
gitea && generate(logoSvg, '../public/assets/img/gitea.svg', {size: 32}),
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
await main();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
exit(1);
|
||||
}
|
||||
570
tools/generate-svg-vscode-extensions.json
Normal file
570
tools/generate-svg-vscode-extensions.json
Normal file
@@ -0,0 +1,570 @@
|
||||
{
|
||||
"pkg:bat": {
|
||||
"bat": [
|
||||
".bat",
|
||||
".cmd"
|
||||
]
|
||||
},
|
||||
"pkg:clojure": {
|
||||
"clojure": [
|
||||
".clj",
|
||||
".cljs",
|
||||
".cljc",
|
||||
".cljx",
|
||||
".clojure",
|
||||
".edn"
|
||||
]
|
||||
},
|
||||
"pkg:coffeescript": {
|
||||
"coffeescript": [
|
||||
".coffee",
|
||||
".cson",
|
||||
".iced"
|
||||
]
|
||||
},
|
||||
"pkg:configuration-editing": {
|
||||
"jsonc": [
|
||||
".code-workspace",
|
||||
"language-configuration.json",
|
||||
"icon-theme.json",
|
||||
"color-theme.json"
|
||||
],
|
||||
"json": [
|
||||
".code-profile"
|
||||
]
|
||||
},
|
||||
"pkg:cpp": {
|
||||
"c": [
|
||||
".c",
|
||||
".i"
|
||||
],
|
||||
"cpp": [
|
||||
".cpp",
|
||||
".cppm",
|
||||
".cc",
|
||||
".ccm",
|
||||
".cxx",
|
||||
".cxxm",
|
||||
".c++",
|
||||
".c++m",
|
||||
".hpp",
|
||||
".hh",
|
||||
".hxx",
|
||||
".h++",
|
||||
".h",
|
||||
".ii",
|
||||
".ino",
|
||||
".inl",
|
||||
".ipp",
|
||||
".ixx",
|
||||
".tpp",
|
||||
".txx",
|
||||
".hpp.in",
|
||||
".h.in"
|
||||
],
|
||||
"cuda-cpp": [
|
||||
".cu",
|
||||
".cuh"
|
||||
]
|
||||
},
|
||||
"pkg:csharp": {
|
||||
"csharp": [
|
||||
".cs",
|
||||
".csx",
|
||||
".cake"
|
||||
]
|
||||
},
|
||||
"pkg:css": {
|
||||
"css": [
|
||||
".css"
|
||||
]
|
||||
},
|
||||
"pkg:dart": {
|
||||
"dart": [
|
||||
".dart"
|
||||
]
|
||||
},
|
||||
"pkg:diff": {
|
||||
"diff": [
|
||||
".diff",
|
||||
".patch",
|
||||
".rej"
|
||||
]
|
||||
},
|
||||
"pkg:docker": {
|
||||
"dockerfile": [
|
||||
".dockerfile",
|
||||
".containerfile"
|
||||
]
|
||||
},
|
||||
"pkg:fsharp": {
|
||||
"fsharp": [
|
||||
".fs",
|
||||
".fsi",
|
||||
".fsx",
|
||||
".fsscript"
|
||||
]
|
||||
},
|
||||
"pkg:git-base": {
|
||||
"ignore": [
|
||||
".gitignore_global",
|
||||
".gitignore",
|
||||
".git-blame-ignore-revs"
|
||||
]
|
||||
},
|
||||
"pkg:go": {
|
||||
"go": [
|
||||
".go"
|
||||
]
|
||||
},
|
||||
"pkg:groovy": {
|
||||
"groovy": [
|
||||
".groovy",
|
||||
".gvy",
|
||||
".gradle",
|
||||
".jenkinsfile",
|
||||
".nf"
|
||||
]
|
||||
},
|
||||
"pkg:handlebars": {
|
||||
"handlebars": [
|
||||
".handlebars",
|
||||
".hbs",
|
||||
".hjs"
|
||||
]
|
||||
},
|
||||
"pkg:hlsl": {
|
||||
"hlsl": [
|
||||
".hlsl",
|
||||
".hlsli",
|
||||
".fx",
|
||||
".fxh",
|
||||
".vsh",
|
||||
".psh",
|
||||
".cginc",
|
||||
".compute"
|
||||
]
|
||||
},
|
||||
"pkg:html": {
|
||||
"html": [
|
||||
".html",
|
||||
".htm",
|
||||
".shtml",
|
||||
".xhtml",
|
||||
".xht",
|
||||
".mdoc",
|
||||
".jsp",
|
||||
".asp",
|
||||
".aspx",
|
||||
".jshtm",
|
||||
".volt",
|
||||
".ejs",
|
||||
".rhtml"
|
||||
]
|
||||
},
|
||||
"pkg:ini": {
|
||||
"ini": [
|
||||
".ini"
|
||||
],
|
||||
"properties": [
|
||||
".conf",
|
||||
".properties",
|
||||
".cfg",
|
||||
".directory",
|
||||
".gitattributes",
|
||||
".gitconfig",
|
||||
".gitmodules",
|
||||
".editorconfig",
|
||||
".repo"
|
||||
]
|
||||
},
|
||||
"pkg:java": {
|
||||
"java": [
|
||||
".java",
|
||||
".jav"
|
||||
]
|
||||
},
|
||||
"pkg:javascript": {
|
||||
"javascriptreact": [
|
||||
".jsx"
|
||||
],
|
||||
"javascript": [
|
||||
".js",
|
||||
".es6",
|
||||
".mjs",
|
||||
".cjs",
|
||||
".pac"
|
||||
]
|
||||
},
|
||||
"pkg:json": {
|
||||
"json": [
|
||||
".json",
|
||||
".bowerrc",
|
||||
".jscsrc",
|
||||
".webmanifest",
|
||||
".js.map",
|
||||
".css.map",
|
||||
".ts.map",
|
||||
".har",
|
||||
".jslintrc",
|
||||
".jsonld",
|
||||
".geojson",
|
||||
".ipynb",
|
||||
".vuerc"
|
||||
],
|
||||
"jsonc": [
|
||||
".jsonc",
|
||||
".eslintrc",
|
||||
".eslintrc.json",
|
||||
".jsfmtrc",
|
||||
".jshintrc",
|
||||
".swcrc",
|
||||
".hintrc",
|
||||
".babelrc"
|
||||
],
|
||||
"jsonl": [
|
||||
".jsonl",
|
||||
".ndjson"
|
||||
],
|
||||
"snippets": [
|
||||
".code-snippets"
|
||||
]
|
||||
},
|
||||
"pkg:julia": {
|
||||
"julia": [
|
||||
".jl"
|
||||
],
|
||||
"juliamarkdown": [
|
||||
".jmd"
|
||||
]
|
||||
},
|
||||
"pkg:latex": {
|
||||
"tex": [
|
||||
".sty",
|
||||
".cls",
|
||||
".bbx",
|
||||
".cbx"
|
||||
],
|
||||
"latex": [
|
||||
".tex",
|
||||
".ltx",
|
||||
".ctx"
|
||||
],
|
||||
"bibtex": [
|
||||
".bib"
|
||||
]
|
||||
},
|
||||
"pkg:less": {
|
||||
"less": [
|
||||
".less"
|
||||
]
|
||||
},
|
||||
"pkg:log": {
|
||||
"log": [
|
||||
".log",
|
||||
"*.log.?"
|
||||
]
|
||||
},
|
||||
"pkg:lua": {
|
||||
"lua": [
|
||||
".lua"
|
||||
]
|
||||
},
|
||||
"pkg:make": {
|
||||
"makefile": [
|
||||
".mak",
|
||||
".mk"
|
||||
]
|
||||
},
|
||||
"pkg:markdown-basics": {
|
||||
"markdown": [
|
||||
".md",
|
||||
".mkd",
|
||||
".mdwn",
|
||||
".mdown",
|
||||
".markdown",
|
||||
".markdn",
|
||||
".mdtxt",
|
||||
".mdtext",
|
||||
".workbook"
|
||||
]
|
||||
},
|
||||
"pkg:ms-vscode.js-debug": {
|
||||
"wat": [
|
||||
".wat",
|
||||
".wasm"
|
||||
]
|
||||
},
|
||||
"pkg:npm": {
|
||||
"ignore": [
|
||||
".npmignore"
|
||||
],
|
||||
"properties": [
|
||||
".npmrc"
|
||||
]
|
||||
},
|
||||
"pkg:objective-c": {
|
||||
"objective-c": [
|
||||
".m"
|
||||
],
|
||||
"objective-cpp": [
|
||||
".mm"
|
||||
]
|
||||
},
|
||||
"pkg:perl": {
|
||||
"perl": [
|
||||
".pl",
|
||||
".pm",
|
||||
".pod",
|
||||
".t",
|
||||
".PL",
|
||||
".psgi"
|
||||
],
|
||||
"raku": [
|
||||
".raku",
|
||||
".rakumod",
|
||||
".rakutest",
|
||||
".rakudoc",
|
||||
".nqp",
|
||||
".p6",
|
||||
".pl6",
|
||||
".pm6"
|
||||
]
|
||||
},
|
||||
"pkg:php": {
|
||||
"php": [
|
||||
".php",
|
||||
".php4",
|
||||
".php5",
|
||||
".phtml",
|
||||
".ctp"
|
||||
]
|
||||
},
|
||||
"pkg:powershell": {
|
||||
"powershell": [
|
||||
".ps1",
|
||||
".psm1",
|
||||
".psd1",
|
||||
".pssc",
|
||||
".psrc"
|
||||
]
|
||||
},
|
||||
"pkg:pug": {
|
||||
"jade": [
|
||||
".pug",
|
||||
".jade"
|
||||
]
|
||||
},
|
||||
"pkg:python": {
|
||||
"python": [
|
||||
".py",
|
||||
".rpy",
|
||||
".pyw",
|
||||
".cpy",
|
||||
".gyp",
|
||||
".gypi",
|
||||
".pyi",
|
||||
".ipy",
|
||||
".pyt"
|
||||
]
|
||||
},
|
||||
"pkg:r": {
|
||||
"r": [
|
||||
".r",
|
||||
".rhistory",
|
||||
".rprofile",
|
||||
".rt"
|
||||
]
|
||||
},
|
||||
"pkg:razor": {
|
||||
"razor": [
|
||||
".cshtml",
|
||||
".razor"
|
||||
]
|
||||
},
|
||||
"pkg:restructuredtext": {
|
||||
"restructuredtext": [
|
||||
".rst"
|
||||
]
|
||||
},
|
||||
"pkg:ruby": {
|
||||
"ruby": [
|
||||
".rb",
|
||||
".rbx",
|
||||
".rjs",
|
||||
".gemspec",
|
||||
".rake",
|
||||
".ru",
|
||||
".erb",
|
||||
".podspec",
|
||||
".rbi"
|
||||
]
|
||||
},
|
||||
"pkg:rust": {
|
||||
"rust": [
|
||||
".rs"
|
||||
]
|
||||
},
|
||||
"pkg:scss": {
|
||||
"scss": [
|
||||
".scss"
|
||||
]
|
||||
},
|
||||
"pkg:search-result": {
|
||||
"search-result": [
|
||||
".code-search"
|
||||
]
|
||||
},
|
||||
"pkg:shaderlab": {
|
||||
"shaderlab": [
|
||||
".shader"
|
||||
]
|
||||
},
|
||||
"pkg:shellscript": {
|
||||
"shellscript": [
|
||||
".sh",
|
||||
".bash",
|
||||
".bashrc",
|
||||
".bash_aliases",
|
||||
".bash_profile",
|
||||
".bash_login",
|
||||
".ebuild",
|
||||
".eclass",
|
||||
".profile",
|
||||
".bash_logout",
|
||||
".xprofile",
|
||||
".xsession",
|
||||
".xsessionrc",
|
||||
".Xsession",
|
||||
".zsh",
|
||||
".zshrc",
|
||||
".zprofile",
|
||||
".zlogin",
|
||||
".zlogout",
|
||||
".zshenv",
|
||||
".zsh-theme",
|
||||
".fish",
|
||||
".ksh",
|
||||
".csh",
|
||||
".cshrc",
|
||||
".tcshrc",
|
||||
".yashrc",
|
||||
".yash_profile"
|
||||
]
|
||||
},
|
||||
"pkg:sql": {
|
||||
"sql": [
|
||||
".sql",
|
||||
".dsql"
|
||||
]
|
||||
},
|
||||
"pkg:swift": {
|
||||
"swift": [
|
||||
".swift"
|
||||
]
|
||||
},
|
||||
"pkg:typescript-basics": {
|
||||
"typescript": [
|
||||
".ts",
|
||||
".cts",
|
||||
".mts"
|
||||
],
|
||||
"typescriptreact": [
|
||||
".tsx"
|
||||
],
|
||||
"json": [
|
||||
".tsbuildinfo"
|
||||
]
|
||||
},
|
||||
"pkg:vb": {
|
||||
"vb": [
|
||||
".vb",
|
||||
".brs",
|
||||
".vbs",
|
||||
".bas",
|
||||
".vba"
|
||||
]
|
||||
},
|
||||
"pkg:xml": {
|
||||
"xml": [
|
||||
".xml",
|
||||
".xsd",
|
||||
".ascx",
|
||||
".atom",
|
||||
".axml",
|
||||
".axaml",
|
||||
".bpmn",
|
||||
".cpt",
|
||||
".csl",
|
||||
".csproj",
|
||||
".csproj.user",
|
||||
".dita",
|
||||
".ditamap",
|
||||
".dtd",
|
||||
".ent",
|
||||
".mod",
|
||||
".dtml",
|
||||
".fsproj",
|
||||
".fxml",
|
||||
".iml",
|
||||
".isml",
|
||||
".jmx",
|
||||
".launch",
|
||||
".menu",
|
||||
".mxml",
|
||||
".nuspec",
|
||||
".opml",
|
||||
".owl",
|
||||
".proj",
|
||||
".props",
|
||||
".pt",
|
||||
".publishsettings",
|
||||
".pubxml",
|
||||
".pubxml.user",
|
||||
".rbxlx",
|
||||
".rbxmx",
|
||||
".rdf",
|
||||
".rng",
|
||||
".rss",
|
||||
".shproj",
|
||||
".storyboard",
|
||||
".svg",
|
||||
".targets",
|
||||
".tld",
|
||||
".tmx",
|
||||
".vbproj",
|
||||
".vbproj.user",
|
||||
".vcxproj",
|
||||
".vcxproj.filters",
|
||||
".wsdl",
|
||||
".wxi",
|
||||
".wxl",
|
||||
".wxs",
|
||||
".xaml",
|
||||
".xbl",
|
||||
".xib",
|
||||
".xlf",
|
||||
".xliff",
|
||||
".xpdl",
|
||||
".xul",
|
||||
".xoml"
|
||||
],
|
||||
"xsl": [
|
||||
".xsl",
|
||||
".xslt"
|
||||
]
|
||||
},
|
||||
"pkg:yaml": {
|
||||
"yaml": [
|
||||
".yaml",
|
||||
".yml",
|
||||
".eyaml",
|
||||
".eyml",
|
||||
".cff",
|
||||
".yaml-tmlanguage",
|
||||
".yaml-tmpreferences",
|
||||
".yaml-tmtheme",
|
||||
".winget"
|
||||
]
|
||||
}
|
||||
}
|
||||
130
tools/generate-svg.ts
Executable file
130
tools/generate-svg.ts
Executable file
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env node
|
||||
import {optimize} from 'svgo';
|
||||
import {dirname, parse} from 'node:path';
|
||||
import {globSync, writeFileSync} from 'node:fs';
|
||||
import {readFile, writeFile, mkdir} from 'node:fs/promises';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {exit} from 'node:process';
|
||||
import type {Manifest} from 'material-icon-theme';
|
||||
|
||||
const glob = (pattern: string) => globSync(pattern, {cwd: dirname(import.meta.dirname)});
|
||||
|
||||
type Opts = {
|
||||
prefix?: string,
|
||||
fullName?: string,
|
||||
};
|
||||
|
||||
async function processAssetsSvgFile(path: string, {prefix, fullName}: Opts = {}) {
|
||||
let name = fullName;
|
||||
if (!name) {
|
||||
name = parse(path).name;
|
||||
if (prefix) name = `${prefix}-${name}`;
|
||||
if (prefix === 'octicon') name = name.replace(/-[0-9]+$/, ''); // chop of '-16' on octicons
|
||||
}
|
||||
// Set the `xmlns` attribute so that the files are displayable in standalone documents
|
||||
// The svg backend module will strip the attribute during startup for inline display
|
||||
const {data} = optimize(await readFile(path, 'utf8'), {
|
||||
plugins: [
|
||||
{name: 'preset-default'},
|
||||
{name: 'removeDimensions'},
|
||||
{name: 'removeTitle'},
|
||||
{name: 'prefixIds', params: {prefix: () => name}},
|
||||
{name: 'addClassesToSVGElement', params: {classNames: ['svg', name]}},
|
||||
{
|
||||
name: 'addAttributesToSVGElement', params: {
|
||||
attributes: [
|
||||
{'xmlns': 'http://www.w3.org/2000/svg'},
|
||||
{'width': '16'}, {'height': '16'}, {'aria-hidden': 'true'},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
await writeFile(fileURLToPath(new URL(`../public/assets/img/svg/${name}.svg`, import.meta.url)), data);
|
||||
}
|
||||
|
||||
function processAssetsSvgFiles(pattern: string, opts: Opts = {}) {
|
||||
return glob(pattern).map((path) => processAssetsSvgFile(path, opts));
|
||||
}
|
||||
|
||||
function lowercaseKeys<T extends Record<string, unknown>>(obj: T): T {
|
||||
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value])) as T;
|
||||
}
|
||||
|
||||
async function processMaterialFileIcons() {
|
||||
const paths = glob('node_modules/material-icon-theme/icons/*.svg');
|
||||
const svgSymbols: Record<string, string> = {};
|
||||
for (const path of paths) {
|
||||
// remove all unnecessary attributes, only keep "viewBox"
|
||||
const {data} = optimize(await readFile(path, 'utf8'), {
|
||||
plugins: [
|
||||
{name: 'preset-default'},
|
||||
{name: 'removeDimensions'},
|
||||
{name: 'removeXMLNS'},
|
||||
{name: 'removeAttrs', params: {attrs: 'xml:space', elemSeparator: ','}},
|
||||
],
|
||||
});
|
||||
const svgName = parse(path).name;
|
||||
// intentionally use single quote here to avoid escaping
|
||||
svgSymbols[svgName] = data.replace(/"/g, `'`);
|
||||
}
|
||||
writeFileSync(fileURLToPath(new URL(`../options/fileicon/material-icon-svgs.json`, import.meta.url)), JSON.stringify(svgSymbols, null, 2));
|
||||
|
||||
const vscodeExtensionsJson = await readFile(fileURLToPath(new URL(`generate-svg-vscode-extensions.json`, import.meta.url)), 'utf8');
|
||||
const vscodeExtensions = JSON.parse(vscodeExtensionsJson) as Record<string, string>;
|
||||
const iconRulesJson = await readFile(fileURLToPath(new URL(`../node_modules/material-icon-theme/dist/material-icons.json`, import.meta.url)), 'utf8');
|
||||
const iconRules = JSON.parse(iconRulesJson) as Manifest;
|
||||
// The rules are from VSCode material-icon-theme, we need to adjust them to our needs
|
||||
// 1. We only use lowercase filenames to match (it should be good enough for most cases and more efficient)
|
||||
// 2. We do not have a "Language ID" system:
|
||||
// * https://code.visualstudio.com/docs/languages/identifiers#_known-language-identifiers
|
||||
// * https://github.com/microsoft/vscode/tree/1.98.0/extensions
|
||||
delete iconRules.iconDefinitions;
|
||||
|
||||
if (iconRules.fileNames) {
|
||||
iconRules.fileNames = lowercaseKeys(iconRules.fileNames);
|
||||
}
|
||||
if (iconRules.folderNames) {
|
||||
iconRules.folderNames = lowercaseKeys(iconRules.folderNames);
|
||||
}
|
||||
if (iconRules.fileExtensions) {
|
||||
iconRules.fileExtensions = lowercaseKeys(iconRules.fileExtensions);
|
||||
}
|
||||
|
||||
// Use VSCode's "Language ID" mapping from its extensions
|
||||
for (const langIdExtMap of Object.values(vscodeExtensions)) {
|
||||
for (const [langId, names] of Object.entries(langIdExtMap)) {
|
||||
for (const name of names) {
|
||||
const nameLower = name.toLowerCase();
|
||||
if (nameLower[0] === '.') {
|
||||
if (iconRules.fileExtensions) {
|
||||
iconRules.fileExtensions[nameLower.substring(1)] ??= langId;
|
||||
}
|
||||
} else {
|
||||
if (iconRules.fileNames) {
|
||||
iconRules.fileNames[nameLower] ??= langId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const iconRulesPretty = JSON.stringify(iconRules, null, 2);
|
||||
writeFileSync(fileURLToPath(new URL(`../options/fileicon/material-icon-rules.json`, import.meta.url)), iconRulesPretty);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await mkdir(fileURLToPath(new URL('../public/assets/img/svg', import.meta.url)), {recursive: true});
|
||||
await Promise.all([
|
||||
...processAssetsSvgFiles('node_modules/@primer/octicons/build/svg/*-16.svg', {prefix: 'octicon'}),
|
||||
...processAssetsSvgFiles('web_src/svg/*.svg'),
|
||||
...processAssetsSvgFiles('public/assets/img/gitea.svg', {fullName: 'gitea-gitea'}),
|
||||
processMaterialFileIcons(),
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
await main();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
exit(1);
|
||||
}
|
||||
118
tools/gocovmerge.go
Normal file
118
tools/gocovmerge.go
Normal file
@@ -0,0 +1,118 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Copyright (c) 2015, Wade Simmons
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// gocovmerge takes the results from multiple `go test -coverprofile` runs and
|
||||
// merges them into one profile
|
||||
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"golang.org/x/tools/cover"
|
||||
)
|
||||
|
||||
func mergeProfiles(p, merge *cover.Profile) {
|
||||
if p.Mode != merge.Mode {
|
||||
log.Fatalf("cannot merge profiles with different modes")
|
||||
}
|
||||
// Since the blocks are sorted, we can keep track of where the last block
|
||||
// was inserted and only look at the blocks after that as targets for merge
|
||||
startIndex := 0
|
||||
for _, b := range merge.Blocks {
|
||||
startIndex = mergeProfileBlock(p, b, startIndex)
|
||||
}
|
||||
}
|
||||
|
||||
func mergeProfileBlock(p *cover.Profile, pb cover.ProfileBlock, startIndex int) int {
|
||||
sortFunc := func(i int) bool {
|
||||
pi := p.Blocks[i+startIndex]
|
||||
return pi.StartLine >= pb.StartLine && (pi.StartLine != pb.StartLine || pi.StartCol >= pb.StartCol)
|
||||
}
|
||||
|
||||
i := 0
|
||||
if sortFunc(i) != true {
|
||||
i = sort.Search(len(p.Blocks)-startIndex, sortFunc)
|
||||
}
|
||||
i += startIndex
|
||||
if i < len(p.Blocks) && p.Blocks[i].StartLine == pb.StartLine && p.Blocks[i].StartCol == pb.StartCol {
|
||||
if p.Blocks[i].EndLine != pb.EndLine || p.Blocks[i].EndCol != pb.EndCol {
|
||||
log.Fatalf("OVERLAP MERGE: %v %v %v", p.FileName, p.Blocks[i], pb)
|
||||
}
|
||||
switch p.Mode {
|
||||
case "set":
|
||||
p.Blocks[i].Count |= pb.Count
|
||||
case "count", "atomic":
|
||||
p.Blocks[i].Count += pb.Count
|
||||
default:
|
||||
log.Fatalf("unsupported covermode: '%s'", p.Mode)
|
||||
}
|
||||
} else {
|
||||
if i > 0 {
|
||||
pa := p.Blocks[i-1]
|
||||
if pa.EndLine >= pb.EndLine && (pa.EndLine != pb.EndLine || pa.EndCol > pb.EndCol) {
|
||||
log.Fatalf("OVERLAP BEFORE: %v %v %v", p.FileName, pa, pb)
|
||||
}
|
||||
}
|
||||
if i < len(p.Blocks)-1 {
|
||||
pa := p.Blocks[i+1]
|
||||
if pa.StartLine <= pb.StartLine && (pa.StartLine != pb.StartLine || pa.StartCol < pb.StartCol) {
|
||||
log.Fatalf("OVERLAP AFTER: %v %v %v", p.FileName, pa, pb)
|
||||
}
|
||||
}
|
||||
p.Blocks = append(p.Blocks, cover.ProfileBlock{})
|
||||
copy(p.Blocks[i+1:], p.Blocks[i:])
|
||||
p.Blocks[i] = pb
|
||||
}
|
||||
return i + 1
|
||||
}
|
||||
|
||||
func addProfile(profiles []*cover.Profile, p *cover.Profile) []*cover.Profile {
|
||||
i := sort.Search(len(profiles), func(i int) bool { return profiles[i].FileName >= p.FileName })
|
||||
if i < len(profiles) && profiles[i].FileName == p.FileName {
|
||||
mergeProfiles(profiles[i], p)
|
||||
} else {
|
||||
profiles = append(profiles, nil)
|
||||
copy(profiles[i+1:], profiles[i:])
|
||||
profiles[i] = p
|
||||
}
|
||||
return profiles
|
||||
}
|
||||
|
||||
func dumpProfiles(profiles []*cover.Profile, out io.Writer) {
|
||||
if len(profiles) == 0 {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(out, "mode: %s\n", profiles[0].Mode)
|
||||
for _, p := range profiles {
|
||||
for _, b := range p.Blocks {
|
||||
fmt.Fprintf(out, "%s:%d.%d,%d.%d %d %d\n", p.FileName, b.StartLine, b.StartCol, b.EndLine, b.EndCol, b.NumStmt, b.Count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var merged []*cover.Profile
|
||||
|
||||
for _, file := range flag.Args() {
|
||||
profiles, err := cover.ParseProfiles(file)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to parse profile '%s': %v", file, err)
|
||||
}
|
||||
for _, p := range profiles {
|
||||
merged = addProfile(merged, p)
|
||||
}
|
||||
}
|
||||
|
||||
dumpProfiles(merged, os.Stdout)
|
||||
}
|
||||
131
tools/lint-go-all.go
Normal file
131
tools/lint-go-all.go
Normal file
@@ -0,0 +1,131 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/mod/modfile"
|
||||
)
|
||||
|
||||
// goModIgnoredDirs returns the go.mod "ignore" directories, which the go tool skips but a filesystem walk does not.
|
||||
func goModIgnoredDirs() (map[string]bool, error) {
|
||||
data, err := os.ReadFile("go.mod")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mod, err := modfile.Parse("go.mod", data, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dirs := make(map[string]bool, len(mod.Ignore))
|
||||
for _, ignore := range mod.Ignore {
|
||||
dirs[filepath.ToSlash(filepath.Clean(ignore.Path))] = true
|
||||
}
|
||||
return dirs, nil
|
||||
}
|
||||
|
||||
func lintGoHeader() bool {
|
||||
headerRE := regexp.MustCompile(`^(// (Copyright [^\n]+|All rights reserved\.)\n)*// Copyright \d{4} (The Gogs Authors|The Gitea Authors|Gitea Authors|Gitea)\.( All rights reserved\.)?\n(// (Copyright [^\n]+|All rights reserved\.)\n)*// SPDX-License-Identifier: [\w.-]+`)
|
||||
generatedRE := regexp.MustCompile(`(?m)^// (Code|This file is) [Gg]enerated.*DO NOT EDIT`)
|
||||
skipDirs, err := goModIgnoredDirs()
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, err)
|
||||
return false
|
||||
}
|
||||
root, bad := ".", 0
|
||||
err = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
if path == root {
|
||||
return nil
|
||||
}
|
||||
if skipDirs[filepath.ToSlash(path)] || strings.HasPrefix(d.Name(), ".") {
|
||||
return fs.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !strings.HasSuffix(path, ".go") {
|
||||
return nil
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := io.ReadAll(io.LimitReader(f, 512))
|
||||
_ = f.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if generatedRE.Match(data) {
|
||||
return nil
|
||||
}
|
||||
if !headerRE.Match(data) {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "%s: missing or invalid copyright header\n", path)
|
||||
bad++
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, err)
|
||||
}
|
||||
return err == nil && bad == 0
|
||||
}
|
||||
|
||||
func runCmd(env []string, name string, args []string) bool {
|
||||
cmd := exec.Command(name, args...)
|
||||
cmd.Env = append(os.Environ(), env...)
|
||||
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 'go run' can not have distinct GOOS/GOARCH for its build and run steps,
|
||||
// so install a pre-compiled binary and run it for different target platforms.
|
||||
_, _ = os.Unsetenv("GOOS"), os.Unsetenv("GOARCH")
|
||||
|
||||
envGolangciLintPackage := os.Getenv("GOLANGCI_LINT_PACKAGE")
|
||||
envGo := os.Getenv("GO")
|
||||
if envGo == "" || envGolangciLintPackage == "" {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "Environment variables GO and GOLANGCI_LINT_PACKAGE must be set")
|
||||
os.Exit(1)
|
||||
}
|
||||
if !runCmd(nil, envGo, []string{"install", envGolangciLintPackage}) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(os.Stdout, "lint go header ...")
|
||||
succeed := lintGoHeader()
|
||||
|
||||
_, _ = fmt.Fprintln(os.Stdout, "lint for linux ...")
|
||||
lintTagsLinux := ""
|
||||
if os.Getenv("CI") != "" || strings.Contains(os.Getenv("TAGS"), "bindata") {
|
||||
// also lint with bindata tag if we are in CI or the "bindata" is explicitly set in the env TAGS
|
||||
lintTagsLinux = "bindata"
|
||||
}
|
||||
succeed = runCmd([]string{"GOOS=linux", "TAGS=" + lintTagsLinux}, "golangci-lint", append([]string{"run", "--build-tags=linux," + lintTagsLinux}, os.Args[1:]...)) && succeed
|
||||
|
||||
if os.Getenv("CI") != "" {
|
||||
// only lint for other platforms when in CI, to keep local lint fast
|
||||
_, _ = fmt.Fprintln(os.Stdout, "lint for windows ...")
|
||||
succeed = runCmd([]string{"GOOS=windows", "TAGS=gogit"}, "golangci-lint", append([]string{"run", "--build-tags=windows,gogit"}, os.Args[1:]...)) && succeed
|
||||
}
|
||||
|
||||
if !succeed {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
11
tools/lint-shell.sh
Executable file
11
tools/lint-shell.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-docker}"
|
||||
VERSION=$(echo "$SHELLCHECK_IMAGE" | sed -E 's/.*:v([0-9.]+)@.*/\1/')
|
||||
|
||||
if hash shellcheck 2>/dev/null && shellcheck --version | grep -qx "version: $VERSION"; then
|
||||
exec shellcheck --color=always "$@"
|
||||
else
|
||||
exec "$CONTAINER_RUNTIME" run --rm -v "$PWD":/mnt -w /mnt "$SHELLCHECK_IMAGE" --color=always "$@"
|
||||
fi
|
||||
25
tools/lint-templates-svg.ts
Executable file
25
tools/lint-templates-svg.ts
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env node
|
||||
import {readdirSync, readFileSync, globSync} from 'node:fs';
|
||||
import {parse, relative} from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {exit} from 'node:process';
|
||||
|
||||
const knownSvgs = new Set<string>();
|
||||
for (const file of readdirSync(new URL('../public/assets/img/svg', import.meta.url))) {
|
||||
knownSvgs.add(parse(file).name);
|
||||
}
|
||||
|
||||
const rootPath = fileURLToPath(new URL('..', import.meta.url));
|
||||
let hadErrors = false;
|
||||
|
||||
for (const file of globSync(fileURLToPath(new URL('../templates/**/*.tmpl', import.meta.url)))) {
|
||||
const content = readFileSync(file, 'utf8');
|
||||
for (const [_, name] of content.matchAll(/svg ["'`]([^"'`]+)["'`]/g)) {
|
||||
if (!knownSvgs.has(name)) {
|
||||
console.info(`SVG "${name}" not found, used in ${relative(rootPath, file)}`);
|
||||
hadErrors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exit(hadErrors ? 1 : 0);
|
||||
9
tools/playwright.sh
Executable file
9
tools/playwright.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# playwright only supports ubuntu/debian officially, and on CI VMs its system deps are pre-installed
|
||||
if [ -z "${GITHUB_ACTIONS:-}" ] && [ "$(uname -s)" = "Linux" ] && grep -qE '^ID(_LIKE)?=.*(ubuntu|debian)' /etc/os-release 2>/dev/null; then
|
||||
pnpm exec playwright install --with-deps "$@"
|
||||
else
|
||||
pnpm exec playwright install "$@"
|
||||
fi
|
||||
32
tools/shared.ts
Normal file
32
tools/shared.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import vuePlugin from '@vitejs/plugin-vue';
|
||||
import {stringPlugin} from 'vite-string-plugin';
|
||||
import type {Plugin} from 'vite';
|
||||
|
||||
// custom elements, vue must render these as-is instead of resolving them as components
|
||||
const webComponents = new Set([
|
||||
// our own, in web_src/js/webcomponents
|
||||
'overflow-menu',
|
||||
'relative-time',
|
||||
// from dependencies
|
||||
'markdown-toolbar',
|
||||
'text-expander',
|
||||
]);
|
||||
|
||||
export const vueDefines = {
|
||||
__VUE_OPTIONS_API__: false,
|
||||
__VUE_PROD_DEVTOOLS__: false,
|
||||
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,
|
||||
};
|
||||
|
||||
export function sharedPlugins(): Plugin[] {
|
||||
return [
|
||||
stringPlugin(),
|
||||
vuePlugin({
|
||||
template: {
|
||||
compilerOptions: {
|
||||
isCustomElement: (tag) => webComponents.has(tag),
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
194
tools/test-e2e.sh
Executable file
194
tools/test-e2e.sh
Executable file
@@ -0,0 +1,194 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-docker}"
|
||||
CONTAINER_NAME="gitea-e2e-runner-$$"
|
||||
|
||||
free_port() {
|
||||
node -e "const s=require('net').createServer();s.listen(0,'127.0.0.1',()=>{process.stdout.write(String(s.address().port));s.close()})"
|
||||
}
|
||||
|
||||
detect_playwright_mode() {
|
||||
if [ "${PLAYWRIGHT_MODE:-auto}" = "local" ] || [ "${PLAYWRIGHT_MODE:-auto}" = "container" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
PLAYWRIGHT_MODE="local"
|
||||
|
||||
if [ "$(uname -s)" = "Linux" ]; then
|
||||
# playwright only supports ubuntu/debian officially
|
||||
if ! grep -qE '^ID(_LIKE)?=.*(ubuntu|debian)' /etc/os-release 2>/dev/null; then
|
||||
PLAYWRIGHT_MODE="container"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
wait_for_container() {
|
||||
local max_wait=30
|
||||
local elapsed=0
|
||||
echo "Waiting for container to start..."
|
||||
while ! (echo > "/dev/tcp/127.0.0.1/$PLAYWRIGHT_SERVER_PORT") 2>/dev/null; do
|
||||
if [ "$("$CONTAINER_RUNTIME" inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null)" != "true" ]; then
|
||||
echo "Error: container exited before becoming ready." >&2
|
||||
"$CONTAINER_RUNTIME" logs "$CONTAINER_NAME" >&2 || true
|
||||
return 1
|
||||
fi
|
||||
if [ "$elapsed" -ge "$max_wait" ]; then
|
||||
echo "Error: container did not become ready after ${max_wait}s." >&2
|
||||
"$CONTAINER_RUNTIME" logs "$CONTAINER_NAME" >&2 || true
|
||||
return 1
|
||||
fi
|
||||
sleep 1
|
||||
elapsed=$((elapsed + 1))
|
||||
done
|
||||
echo "Container is ready."
|
||||
}
|
||||
|
||||
detect_playwright_mode
|
||||
|
||||
if [ "$PLAYWRIGHT_MODE" = "container" ]; then
|
||||
if ! command -v "$CONTAINER_RUNTIME" >/dev/null 2>&1; then
|
||||
echo "error: PLAYWRIGHT_MODE=container but '$CONTAINER_RUNTIME' is not installed." >&2
|
||||
echo "Install docker/podman or set CONTAINER_RUNTIME to an available runtime." >&2
|
||||
exit 1
|
||||
fi
|
||||
PLAYWRIGHT_VERSION=$(sed -n 's/.*"@playwright\/test"[[:space:]]*:[[:space:]]*"[^[:digit:]]*\([^"]*\)".*/\1/p' package.json)
|
||||
if ! [[ "$PLAYWRIGHT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]]; then
|
||||
echo "error: invalid @playwright/test version in package.json: '${PLAYWRIGHT_VERSION}'" >&2
|
||||
exit 1
|
||||
fi
|
||||
PLAYWRIGHT_IMAGE="mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-noble"
|
||||
echo "Running playwright in container"
|
||||
if ! "$CONTAINER_RUNTIME" image inspect "$PLAYWRIGHT_IMAGE" >/dev/null 2>&1; then
|
||||
"$CONTAINER_RUNTIME" pull "$PLAYWRIGHT_IMAGE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create isolated work directory
|
||||
WORK_DIR=$(mktemp -d)
|
||||
|
||||
# Find a random free port
|
||||
FREE_PORT=$(free_port)
|
||||
|
||||
cleanup() {
|
||||
if [ "$PLAYWRIGHT_MODE" = "container" ]; then
|
||||
"$CONTAINER_RUNTIME" stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -n "${SERVER_PID:-}" ]; then
|
||||
kill "$SERVER_PID" 2>/dev/null || true
|
||||
wait "$SERVER_PID" 2>/dev/null || true
|
||||
fi
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [ "$PLAYWRIGHT_MODE" = "container" ]; then
|
||||
PLAYWRIGHT_SERVER_PORT=$(free_port)
|
||||
# --network=host: container needs host loopback to reach gitea.
|
||||
"$CONTAINER_RUNTIME" run --network=host --name "$CONTAINER_NAME" -d --rm --init --workdir /home/pwuser --user pwuser "$PLAYWRIGHT_IMAGE" /bin/sh -c "npx -y playwright@${PLAYWRIGHT_VERSION} run-server --port ${PLAYWRIGHT_SERVER_PORT} --host 0.0.0.0"
|
||||
|
||||
if ! wait_for_container; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Write config file for isolated instance
|
||||
mkdir -p "$WORK_DIR/custom/conf"
|
||||
cat > "$WORK_DIR/custom/conf/app.ini" <<EOF
|
||||
[database]
|
||||
DB_TYPE = sqlite3
|
||||
PATH = $WORK_DIR/data/gitea.db
|
||||
|
||||
[server]
|
||||
HTTP_PORT = $FREE_PORT
|
||||
ROOT_URL = http://localhost:$FREE_PORT
|
||||
STATIC_ROOT_PATH = $(pwd)
|
||||
|
||||
[security]
|
||||
INSTALL_LOCK = true
|
||||
|
||||
[service]
|
||||
ENABLE_CAPTCHA = false
|
||||
|
||||
[ui.notification]
|
||||
EVENT_SOURCE_UPDATE_TIME = 500ms
|
||||
|
||||
[log]
|
||||
MODE = console
|
||||
LEVEL = Warn
|
||||
|
||||
[markup.test-external]
|
||||
ENABLED = true
|
||||
FILE_EXTENSIONS = .external
|
||||
RENDER_COMMAND = cat
|
||||
IS_INPUT_FILE = false
|
||||
RENDER_CONTENT_MODE = iframe
|
||||
EOF
|
||||
|
||||
export GITEA_WORK_DIR="$WORK_DIR"
|
||||
export GITEA_TEST_E2E=true
|
||||
|
||||
# Start Gitea server
|
||||
echo "Starting Gitea server on port $FREE_PORT (workdir: $WORK_DIR)..."
|
||||
if [ -n "${GITEA_TEST_E2E_DEBUG:-}" ]; then
|
||||
"./$EXECUTABLE" web &
|
||||
else
|
||||
"./$EXECUTABLE" web > "$WORK_DIR/server.log" 2>&1 &
|
||||
fi
|
||||
SERVER_PID=$!
|
||||
|
||||
# Wait for server to be reachable
|
||||
E2E_URL="http://localhost:$FREE_PORT"
|
||||
MAX_WAIT=120
|
||||
ELAPSED=0
|
||||
while ! curl -sf --max-time 5 "$E2E_URL" > /dev/null 2>&1; do
|
||||
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||
echo "error: Gitea server process exited unexpectedly. Server log:" >&2
|
||||
cat "$WORK_DIR/server.log" 2>/dev/null >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then
|
||||
echo "error: Gitea server not reachable after ${MAX_WAIT}s. Server log:" >&2
|
||||
cat "$WORK_DIR/server.log" 2>/dev/null >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
ELAPSED=$((ELAPSED + 2))
|
||||
done
|
||||
|
||||
echo "Gitea server is ready at $E2E_URL"
|
||||
|
||||
GITEA_TEST_E2E_DOMAIN="e2e.gitea.com"
|
||||
GITEA_TEST_E2E_USER="e2e-admin"
|
||||
GITEA_TEST_E2E_PASSWORD="password"
|
||||
GITEA_TEST_E2E_EMAIL="$GITEA_TEST_E2E_USER@$GITEA_TEST_E2E_DOMAIN"
|
||||
|
||||
# Create admin test user
|
||||
"./$EXECUTABLE" admin user create \
|
||||
--username "$GITEA_TEST_E2E_USER" \
|
||||
--password "$GITEA_TEST_E2E_PASSWORD" \
|
||||
--email "$GITEA_TEST_E2E_EMAIL" \
|
||||
--must-change-password=false \
|
||||
--admin
|
||||
|
||||
# timeout multiplier to make the tests pass on slow CI runners while using
|
||||
# factor 1 on a fast local machine like a MacBook Pro M1+
|
||||
if [ -z "${GITEA_TEST_E2E_TIMEOUT_FACTOR:-}" ]; then
|
||||
if [ -n "${CI:-}" ]; then
|
||||
GITEA_TEST_E2E_TIMEOUT_FACTOR=4
|
||||
else
|
||||
GITEA_TEST_E2E_TIMEOUT_FACTOR=1
|
||||
fi
|
||||
fi
|
||||
|
||||
export GITEA_TEST_E2E_URL="$E2E_URL"
|
||||
export GITEA_TEST_E2E_DOMAIN
|
||||
export GITEA_TEST_E2E_USER
|
||||
export GITEA_TEST_E2E_PASSWORD
|
||||
export GITEA_TEST_E2E_EMAIL
|
||||
export GITEA_TEST_E2E_TIMEOUT_FACTOR
|
||||
|
||||
if [ "$PLAYWRIGHT_MODE" = "container" ]; then
|
||||
export PW_TEST_CONNECT_WS_ENDPOINT="ws://127.0.0.1:${PLAYWRIGHT_SERVER_PORT}/"
|
||||
fi
|
||||
pnpm exec playwright test "$@"
|
||||
20
tools/test-echo.go
Normal file
20
tools/test-echo.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
_, err := io.Copy(os.Stdout, os.Stdin)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
31
tools/test-integration.sh
Executable file
31
tools/test-integration.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Run a compiled *.test binary. When TEST_SHARD is set, enumerate top-level
|
||||
# tests via -test.list and run only the shard's slice; TestMain skips
|
||||
# environment setup in -test.list mode. Without TEST_SHARD, runs the binary
|
||||
# directly.
|
||||
|
||||
BINARY=${1:?usage: $0 BINARY}
|
||||
|
||||
if [ -z "${TEST_SHARD:-}" ]; then
|
||||
exec "$BINARY"
|
||||
fi
|
||||
|
||||
if ! [[ "${TEST_TOTAL_SHARDS:-}" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "TEST_TOTAL_SHARDS must be a positive integer, got: ${TEST_TOTAL_SHARDS:-}" >&2
|
||||
exit 2
|
||||
fi
|
||||
if ! [[ "$TEST_SHARD" =~ ^[1-9][0-9]*$ ]] || [ "$TEST_SHARD" -gt "$TEST_TOTAL_SHARDS" ]; then
|
||||
echo "TEST_SHARD must be in [1, $TEST_TOTAL_SHARDS], got: $TEST_SHARD" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
NAMES=$("$BINARY" -test.list='^Test' | LC_ALL=C sort -u | awk -v r=$((TEST_SHARD - 1)) -v t="$TEST_TOTAL_SHARDS" '(NR - 1) % t == r')
|
||||
if [ -z "$NAMES" ]; then
|
||||
echo "shard $TEST_SHARD/$TEST_TOTAL_SHARDS has no tests assigned" >&2
|
||||
exit 1
|
||||
fi
|
||||
PATTERN=$(echo "$NAMES" | paste -sd '|' -)
|
||||
echo "Running shard $TEST_SHARD/$TEST_TOTAL_SHARDS ($(echo "$NAMES" | wc -l | tr -d ' ') tests)"
|
||||
exec "$BINARY" -test.run "^($PATTERN)\$"
|
||||
8
tools/watch.sh
Normal file
8
tools/watch.sh
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
make --no-print-directory watch-frontend &
|
||||
make --no-print-directory watch-backend &
|
||||
|
||||
trap 'kill $(jobs -p)' EXIT
|
||||
wait
|
||||
Reference in New Issue
Block a user