初始提交: Gitea 项目代码
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
#!/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; breaking: boolean} | null {
|
||||
const match = titlePattern.exec(title);
|
||||
return match ? {type: match[1] as CommitType, breaking: Boolean(match[3])} : null;
|
||||
}
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
// Non-type labels, only added, never auto-removed, so manual labeling is not clobbered.
|
||||
const extraLabels: Partial<Record<CommitType, string>> = {
|
||||
chore: 'skip-changelog',
|
||||
ci: 'skip-changelog',
|
||||
build: 'topic/build',
|
||||
};
|
||||
|
||||
// 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 [];
|
||||
return [typeLabels[parsed.type], extraLabels[parsed.type], parsed.breaking ? breakingLabel : undefined]
|
||||
.filter((label): label is string => label !== undefined);
|
||||
}
|
||||
|
||||
// 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): 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',
|
||||
...(body ? {'Content-Type': 'application/json'} : {}),
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API ${method} ${url} failed (${response.status}): ${await response.text()}`);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
const desired = labelsForPrTitle(env.PR_TITLE);
|
||||
const response = await request(`${labelsUrl}?per_page=100`);
|
||||
const current = ((await response.json()) as Array<{name: string}>).map((label) => label.name);
|
||||
|
||||
const toAdd = desired.filter((name) => !current.includes(name));
|
||||
const toRemove = removableLabels.filter((name) => current.includes(name) && !desired.includes(name));
|
||||
|
||||
if (toAdd.length) {
|
||||
await request(labelsUrl, 'POST', {labels: toAdd});
|
||||
console.info(`Added labels: ${toAdd.join(', ')}`);
|
||||
}
|
||||
for (const name of toRemove) {
|
||||
await request(`${labelsUrl}/${encodeURIComponent(name)}`, 'DELETE');
|
||||
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);
|
||||
}
|
||||
Executable
+95
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env node
|
||||
import {load as parseYaml} from 'js-yaml';
|
||||
import {writeFile} from 'node:fs/promises';
|
||||
import {languages as cmLanguages} from '@codemirror/language-data';
|
||||
|
||||
const linguistUrl = 'https://raw.githubusercontent.com/github-linguist/linguist/main/lib/linguist/languages.yml';
|
||||
|
||||
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 LinguistEntry = {
|
||||
type: string;
|
||||
extensions?: string[];
|
||||
filenames?: string[];
|
||||
};
|
||||
|
||||
type CmLanguage = {
|
||||
name: string;
|
||||
extensions: string[];
|
||||
filenames: string[];
|
||||
};
|
||||
|
||||
const res = await fetch(linguistUrl);
|
||||
if (!res.ok) throw new Error(`fetch ${linguistUrl} failed: ${res.status}`);
|
||||
const linguist = parseYaml(await res.text()) as Record<string, LinguistEntry>;
|
||||
|
||||
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)) {
|
||||
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}`);
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env node
|
||||
import {initWasm, Resvg} from '@resvg/resvg-wasm';
|
||||
import {optimize} from 'svgo';
|
||||
import {readFile, writeFile} from 'node:fs/promises';
|
||||
import {argv, exit} from 'node:process';
|
||||
|
||||
async function generate(svg: string, path: string, {size, bg}: {size: number, bg?: boolean}) {
|
||||
const outputFile = new URL(path, import.meta.url);
|
||||
|
||||
if (String(outputFile).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 Promise.all([
|
||||
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);
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
Executable
+130
@@ -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(obj: Record<string, any>) {
|
||||
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value]));
|
||||
}
|
||||
|
||||
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.entries(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);
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
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 := map[string]bool{
|
||||
".git": true,
|
||||
".venv": true,
|
||||
"node_modules": true,
|
||||
"public": true,
|
||||
"vendor": true,
|
||||
"web_src": true,
|
||||
}
|
||||
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 rel, _ := filepath.Rel(root, path); skipDirs[filepath.ToSlash(rel)] {
|
||||
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 ...")
|
||||
succeed = runCmd([]string{"GOOS=linux", "TAGS=bindata"}, "golangci-lint", append([]string{"run", "--build-tags=linux,bindata"}, 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)
|
||||
}
|
||||
}
|
||||
Executable
+11
@@ -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
|
||||
Executable
+25
@@ -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);
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
// nolyfill writes overrides to package.json#pnpm.overrides which pnpm v11 ignores.
|
||||
// This moves them to pnpm-workspace.yaml until SukkaW/nolyfill#119 is fixed.
|
||||
import {readFileSync, writeFileSync} from 'node:fs';
|
||||
import {exit} from 'node:process';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {dump} from 'js-yaml';
|
||||
|
||||
const packagePath = fileURLToPath(new URL('../package.json', import.meta.url));
|
||||
const workspacePath = fileURLToPath(new URL('../pnpm-workspace.yaml', import.meta.url));
|
||||
|
||||
const packageJson: {pnpm?: {overrides?: Record<string, string>}} = JSON.parse(readFileSync(packagePath, 'utf8'));
|
||||
const overrides = packageJson.pnpm?.overrides;
|
||||
|
||||
if (!overrides || !Object.keys(overrides).length) {
|
||||
exit(0);
|
||||
}
|
||||
|
||||
const block = dump({overrides}, {lineWidth: -1, quotingType: "'"});
|
||||
const workspace = readFileSync(workspacePath, 'utf8');
|
||||
const overridesRegex = /^overrides:[^\n]*(?:\n(?:[ \t][^\n]*|[ \t]*(?=\n[ \t])))*\n?/m;
|
||||
|
||||
if (!overridesRegex.test(workspace)) {
|
||||
console.error(`No 'overrides:' block found in pnpm-workspace.yaml`);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
writeFileSync(workspacePath, workspace.replace(overridesRegex, block));
|
||||
|
||||
const pnpm = packageJson.pnpm!;
|
||||
delete pnpm.overrides;
|
||||
if (!Object.keys(pnpm).length) delete packageJson.pnpm;
|
||||
writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
||||
Executable
+216
@@ -0,0 +1,216 @@
|
||||
#!/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."
|
||||
}
|
||||
|
||||
CMD="${1:-run}"
|
||||
if [ "$CMD" = "install" ] || [ "$CMD" = "run" ]; then
|
||||
[ $# -gt 0 ] && shift
|
||||
else
|
||||
CMD="run"
|
||||
fi
|
||||
|
||||
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"
|
||||
fi
|
||||
|
||||
if [ "$CMD" = "install" ]; then
|
||||
if [ "$PLAYWRIGHT_MODE" = "local" ]; then
|
||||
# on GitHub Actions VMs, playwright's system deps are pre-installed
|
||||
if [ -z "${GITHUB_ACTIONS:-}" ]; then
|
||||
# shellcheck disable=SC2086 # flag string
|
||||
pnpm exec playwright install --with-deps chromium firefox ${PLAYWRIGHT_FLAGS:-}
|
||||
else
|
||||
# shellcheck disable=SC2086 # flag string
|
||||
pnpm exec playwright install chromium firefox ${PLAYWRIGHT_FLAGS:-}
|
||||
fi
|
||||
else
|
||||
echo "Running playwright in container as host distro is not supported by playwright directly"
|
||||
if ! "$CONTAINER_RUNTIME" image inspect "$PLAYWRIGHT_IMAGE" >/dev/null 2>&1; then
|
||||
"$CONTAINER_RUNTIME" pull "$PLAYWRIGHT_IMAGE"
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
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 "$@"
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Executable
+31
@@ -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)\$"
|
||||
@@ -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