feat(backend): Add Gitea proxy backend and LFS support for Gitea backend
Some checks failed
Build / build (push) Waiting to run
Build / lint (push) Waiting to run
Build / test (push) Waiting to run
Build / integration_tests (push) Waiting to run
Cypress Tests / cypress-run (1) (push) Waiting to run
Cypress Tests / cypress-run (2) (push) Waiting to run
CodeQL / Analyze (javascript) (push) Has been cancelled

This commit is contained in:
Denys Konovalov 2024-12-28 11:41:05 +01:00
parent c42e4e9bd8
commit cebf4d0a9d
Signed by: Denys Konovalov
GPG Key ID: 0037E1B0E33BD2C9
17 changed files with 2530 additions and 951 deletions

17
nx.json
View File

@ -1,25 +1,16 @@
{ {
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": [
"build",
"test"
]
}
}
},
"targetDefaults": { "targetDefaults": {
"build": { "build": {
"dependsOn": [ "dependsOn": [
"^build" "^build"
] ],
"cache": true
}, },
"dev": { "dev": {
"dependsOn": [ "dependsOn": [
"^build" "^build"
] ],
"cache": true
} }
}, },
"$schema": "./node_modules/nx/schemas/nx-schema.json", "$schema": "./node_modules/nx/schemas/nx-schema.json",

View File

@ -31,15 +31,15 @@
"@simonsmith/cypress-image-snapshot": "9.0.1", "@simonsmith/cypress-image-snapshot": "9.0.1",
"@types/mocha": "10.0.6", "@types/mocha": "10.0.6",
"all-contributors-cli": "6.26.1", "all-contributors-cli": "6.26.1",
"cypress": "13.17.0",
"cypress-plugin-tab": "1.0.5", "cypress-plugin-tab": "1.0.5",
"cypress-real-events": "1.11.0", "cypress-real-events": "1.11.0",
"cypress": "13.6.2",
"execa": "8.0.1", "execa": "8.0.1",
"husky": "8.0.3", "husky": "8.0.3",
"lerna": "8.0.1", "lerna": "8.1.9",
"lint-staged": "15.2.0", "lint-staged": "15.2.0",
"react-dom": "18.2.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0",
"rehype": "13.0.1", "rehype": "13.0.1",
"unist-util-visit": "5.0.0" "unist-util-visit": "5.0.0"
}, },

View File

@ -48,7 +48,7 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "7.23.4", "@babel/cli": "7.23.4",
"@babel/core": "7.23.7", "@babel/core": "7.26.0",
"@babel/plugin-proposal-class-properties": "7.18.6", "@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-proposal-export-default-from": "7.23.3", "@babel/plugin-proposal-export-default-from": "7.23.3",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6", "@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
@ -57,7 +57,7 @@
"@babel/plugin-proposal-optional-chaining": "7.21.0", "@babel/plugin-proposal-optional-chaining": "7.21.0",
"@babel/plugin-syntax-dynamic-import": "7.8.3", "@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.23.7", "@babel/preset-env": "7.23.7",
"@babel/preset-react": "7.23.3", "@babel/preset-react": "7.26.3",
"@babel/preset-typescript": "7.23.3", "@babel/preset-typescript": "7.23.3",
"@types/node": "18.19.4", "@types/node": "18.19.4",
"@types/react": "18.2.46", "@types/react": "18.2.46",
@ -69,7 +69,7 @@
"babel-loader": "9.1.3", "babel-loader": "9.1.3",
"babel-plugin-inline-json-import": "0.3.2", "babel-plugin-inline-json-import": "0.3.2",
"babel-plugin-inline-react-svg": "2.0.2", "babel-plugin-inline-react-svg": "2.0.2",
"babel-plugin-lodash": "3.3.4", "@sigmacomputing/babel-plugin-lodash": "3.3.5",
"babel-plugin-transform-builtin-extend": "1.1.2", "babel-plugin-transform-builtin-extend": "1.1.2",
"babel-plugin-transform-define": "2.1.4", "babel-plugin-transform-define": "2.1.4",
"babel-plugin-transform-export-extensions": "6.22.0", "babel-plugin-transform-export-extensions": "6.22.0",

View File

@ -8,7 +8,7 @@ const isESM = process.env.NODE_ENV === 'esm';
console.info('Build Package:', path.basename(process.cwd())); console.info('Build Package:', path.basename(process.cwd()));
const defaultPlugins = [ const defaultPlugins = [
'lodash', '@sigmacomputing/babel-plugin-lodash',
[ [
'babel-plugin-transform-builtin-extend', 'babel-plugin-transform-builtin-extend',
{ {

View File

@ -69,9 +69,9 @@
"@mdx-js/mdx": "3.0.0", "@mdx-js/mdx": "3.0.0",
"@mdx-js/react": "3.0.0", "@mdx-js/react": "3.0.0",
"@mui/base": "5.0.0-beta.30", "@mui/base": "5.0.0-beta.30",
"@mui/material": "5.15.3", "@mui/material": "6.3.0",
"@mui/system": "5.15.3", "@mui/system": "6.3.0",
"@mui/x-date-pickers": "7.3.1", "@mui/x-date-pickers": "7.23.3",
"@reduxjs/toolkit": "1.9.7", "@reduxjs/toolkit": "1.9.7",
"@styled-icons/bootstrap": "10.47.0", "@styled-icons/bootstrap": "10.47.0",
"@styled-icons/fa-brands": "10.47.0", "@styled-icons/fa-brands": "10.47.0",
@ -88,7 +88,7 @@
"@udecode/plate-serializer-md": "23.7.4", "@udecode/plate-serializer-md": "23.7.4",
"@uiw/codemirror-extensions-langs": "4.19.16", "@uiw/codemirror-extensions-langs": "4.19.16",
"@uiw/react-codemirror": "4.19.16", "@uiw/react-codemirror": "4.19.16",
"ajv": "8.12.0", "ajv": "8.17.1",
"ajv-errors": "3.0.0", "ajv-errors": "3.0.0",
"ajv-keywords": "5.1.0", "ajv-keywords": "5.1.0",
"buffer": "6.0.3", "buffer": "6.0.3",
@ -166,7 +166,7 @@
"symbol-observable": "4.0.0", "symbol-observable": "4.0.0",
"unified": "11.0.4", "unified": "11.0.4",
"unist-util-visit": "5.0.0", "unist-util-visit": "5.0.0",
"url": "0.11.3", "url": "0.11.4",
"url-join": "5.0.0", "url-join": "5.0.0",
"uuid": "9.0.1", "uuid": "9.0.1",
"validate-color": "2.2.4", "validate-color": "2.2.4",
@ -174,21 +174,23 @@
"vfile-message": "4.0.2", "vfile-message": "4.0.2",
"vfile-statistics": "3.0.0", "vfile-statistics": "3.0.0",
"what-the-diff": "0.6.0", "what-the-diff": "0.6.0",
"yaml": "2.3.4" "yaml": "2.3.4",
"isomorphic-git": "1.27.2",
"@isomorphic-git/lightning-fs": "4.6.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "7.23.4", "@babel/cli": "7.26.4",
"@babel/core": "7.23.7", "@babel/core": "7.26.0",
"@babel/plugin-proposal-class-properties": "7.18.6", "@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-proposal-export-default-from": "7.23.3", "@babel/plugin-proposal-export-default-from": "7.25.9",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6", "@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
"@babel/plugin-proposal-numeric-separator": "7.18.6", "@babel/plugin-proposal-numeric-separator": "7.18.6",
"@babel/plugin-proposal-object-rest-spread": "7.20.7", "@babel/plugin-proposal-object-rest-spread": "7.20.7",
"@babel/plugin-proposal-optional-chaining": "7.21.0", "@babel/plugin-proposal-optional-chaining": "7.21.0",
"@babel/plugin-syntax-dynamic-import": "7.8.3", "@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.23.7", "@babel/preset-env": "7.26.0",
"@babel/preset-react": "7.23.3", "@babel/preset-react": "7.26.3",
"@babel/preset-typescript": "7.23.3", "@babel/preset-typescript": "7.26.0",
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.11", "@pmmmwh/react-refresh-webpack-plugin": "0.5.11",
"@simbathesailor/use-what-changed": "2.0.0", "@simbathesailor/use-what-changed": "2.0.0",
@ -220,7 +222,7 @@
"babel-loader": "9.1.3", "babel-loader": "9.1.3",
"babel-plugin-inline-json-import": "0.3.2", "babel-plugin-inline-json-import": "0.3.2",
"babel-plugin-inline-react-svg": "2.0.2", "babel-plugin-inline-react-svg": "2.0.2",
"babel-plugin-lodash": "3.3.4", "@sigmacomputing/babel-plugin-lodash": "3.3.5",
"babel-plugin-transform-builtin-extend": "1.1.2", "babel-plugin-transform-builtin-extend": "1.1.2",
"babel-plugin-transform-define": "2.1.4", "babel-plugin-transform-define": "2.1.4",
"babel-plugin-transform-export-extensions": "6.22.0", "babel-plugin-transform-export-extensions": "6.22.0",

View File

@ -0,0 +1,220 @@
import FS from '@isomorphic-git/lightning-fs';
import * as git from 'isomorphic-git';
import http from 'isomorphic-git/http/web';
import type { WorkflowStatus } from '@staticcms/core/constants/publishModes';
import type { AssetProxy } from '@staticcms/core/valueObjects';
import type {
Backend,
BackendClass,
BackendEntry,
Config,
ConfigWithDefaults,
Credentials,
Cursor,
DisplayURL,
ImplementationEntry,
ImplementationFile,
PersistOptions,
User,
} from '@staticcms/core';
const dir = '/repo';
let singleton: Promise<string[]>;
function determineRepositoryURL(backend: Backend): string {
const name = backend.name;
if (name.startsWith('github')) {
return `https://github.com/${backend.repo}.git`;
} else if (name.startsWith('gitlab')) {
return `https://gitlab.com/${backend.repo}.git`;
} else if (name.startsWith('gitea')) {
return `${backend.base_url}/${backend.repo}.git`;
} else if (name.startsWith('git-gateway')) {
if (!backend.repo) {
throw new Error(
"Repository URL mandatory for 'git-gateway' configuration (https://[...]/repoName.git)",
);
}
return backend.repo!;
}
throw new Error("Can't determine repository URL");
}
export default function GitProxyBackEndGenerator(
T: new (config: ConfigWithDefaults, options?: Object) => BackendClass,
) {
class GitProxyBackend implements BackendClass {
backend: BackendClass;
config: Config;
fs: FS;
pfs: FS.PromisifiedFS;
repositoryUrl: string;
repository: Promise<string[]>;
constructor(config: ConfigWithDefaults, options = {}) {
this.backend = new T(config, options);
this.config = config;
this.fs = new FS('decapfs');
this.pfs = this.fs.promises;
this.repositoryUrl = determineRepositoryURL(config.backend);
if (!singleton) {
singleton = this.getRepository();
}
this.repository = singleton;
}
async getRepository() {
const branch = this.config.backend.branch || 'main';
try {
await this.pfs.stat(dir);
} catch (e) {
await this.pfs.mkdir(dir);
await git.init({
fs: this.fs,
dir,
defaultBranch: branch,
});
}
await git.addRemote({
fs: this.fs,
dir,
url: this.repositoryUrl,
remote: 'origin',
force: true,
});
await git.fetch({
fs: this.fs,
http,
dir,
remote: 'origin',
ref: branch,
singleBranch: true,
depth: 1,
});
await git.checkout({
fs: this.fs,
dir,
ref: branch,
force: true,
track: false,
});
return this.pfs.readdir(dir);
}
isGitBackend() {
return true;
}
async entriesByFolder(folder: string, extension: string) {
try {
await this.repository;
const files = await this.pfs.readdir(`${dir}/${folder}`);
const relevantFiles = files.filter((name: string) => name.endsWith(extension));
return Promise.all(
relevantFiles.map(async (filename: string) => {
const path = `${folder}/${filename}`;
const fullPath = `${dir}/${path}`;
let data = await this.pfs.readFile(fullPath, 'utf8');
if (data instanceof Uint8Array) {
data = new TextDecoder().decode(data);
}
return {
data,
file: { path, id: path },
};
}),
);
} catch {
return [];
}
}
async getEntry(path: string) {
await this.repository;
let data = await this.pfs.readFile(`${dir}/${path}`, 'utf8');
if (data instanceof Uint8Array) {
data = new TextDecoder().decode(data);
}
return {
file: { path, id: null },
data,
};
}
status() {
return this.backend.status();
}
authComponent() {
return this.backend.authComponent();
}
restoreUser(user: User) {
return this.backend.restoreUser(user);
}
authenticate(credentials: Credentials) {
return this.backend.authenticate(credentials);
}
logout() {
return this.backend.logout();
}
getToken() {
return this.backend.getToken();
}
traverseCursor(cursor: Cursor, action: string) {
return this.backend.traverseCursor!(cursor, action);
}
entriesByFiles(files: ImplementationFile[]) {
return this.backend.entriesByFiles(files);
}
unpublishedEntries() {
return this.backend.unpublishedEntries();
}
unpublishedEntry(args: { id?: string; collection?: string; slug?: string }) {
return this.backend.unpublishedEntry(args);
}
unpublishedEntryDataFile(collection: string, slug: string, path: string, id: string) {
return this.backend.unpublishedEntryDataFile(collection, slug, path, id);
}
unpublishedEntryMediaFile(collection: string, slug: string, path: string, id: string) {
return this.backend.unpublishedEntryMediaFile(collection, slug, path, id);
}
deleteUnpublishedEntry(collection: string, slug: string) {
return this.backend.deleteUnpublishedEntry(collection, slug);
}
persistEntry(entry: BackendEntry, opts: PersistOptions) {
return this.backend.persistEntry(entry, opts);
}
updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: WorkflowStatus) {
return this.backend.updateUnpublishedEntryStatus(collection, slug, newStatus);
}
publishUnpublishedEntry(collection: string, slug: string) {
return this.backend.publishUnpublishedEntry(collection, slug);
}
getMedia(folder?: string, folderSupport?: boolean) {
return this.backend.getMedia(folder, folderSupport);
}
getMediaFile(path: string) {
return this.backend.getMediaFile(path);
}
getMediaDisplayURL(displayURL: DisplayURL): Promise<string> {
return this.backend.getMediaDisplayURL(displayURL);
}
persistMedia(file: AssetProxy, opts: PersistOptions) {
return this.backend.persistMedia(file, opts);
}
deleteFiles(paths: string[], commitMessage: string) {
return this.backend.deleteFiles(paths, commitMessage);
}
getDeployPreview(collectionName: string, slug: string) {
return this.backend.getDeployPreview(collectionName, slug);
}
allEntriesByFolder(
folder: string,
extension: string,
depth: number,
pathRegex?: RegExp,
): Promise<ImplementationEntry[]> {
return this.backend.allEntriesByFolder(folder, extension, depth, pathRegex);
}
}
return GitProxyBackend;
}

View File

@ -0,0 +1,3 @@
import GitProxyBackEndGenerator from './implementation';
export default GitProxyBackEndGenerator;

View File

@ -273,11 +273,13 @@ export default class API {
parseText?: boolean; parseText?: boolean;
} = {}, } = {},
) { ) {
if (!parseText) {
return await this.fetchMediaContent(path, branch, repoURL);
}
if (!sha) { if (!sha) {
sha = await this.getFileSha(path, { repoURL, branch }); sha = await this.getFileSha(path, { repoURL, branch });
} }
const content = await this.fetchBlobContent({ sha: sha as string, repoURL, parseText }); return await this.fetchBlobContent({ sha: sha as string, repoURL, parseText });
return content;
} }
async readFileMetadata(path: string, sha: string | null | undefined) { async readFileMetadata(path: string, sha: string | null | undefined) {
@ -323,6 +325,16 @@ export default class API {
} }
} }
async fetchMediaContent(path: string, branch: string, repoURL: string) {
return await this.request(
`${repoURL}/media/${path}??ref=${branch}`,
{
cache: 'force-cache',
},
response => response.blob(),
);
}
async listFiles( async listFiles(
path: string, path: string,
{ repoURL = this.repoURL, branch = this.branch, depth = 1 } = {}, { repoURL = this.repoURL, branch = this.branch, depth = 1 } = {},

View File

@ -372,11 +372,11 @@ export default class Gitea implements BackendClass {
break; break;
} }
case 'next': { case 'next': {
result = this.getCursorAndFiles(files, (meta?.['page'] as number) + 1 ?? 1); result = this.getCursorAndFiles(files, ((meta?.['page'] as number) ?? 1) + 1);
break; break;
} }
case 'prev': { case 'prev': {
result = this.getCursorAndFiles(files, (meta?.['page'] as number) - 1 ?? 1); result = this.getCursorAndFiles(files, ((meta?.['page'] as number) ?? 1) - 1);
break; break;
} }
default: { default: {

View File

@ -526,11 +526,11 @@ export default class GitHub implements BackendClass {
break; break;
} }
case 'next': { case 'next': {
result = this.getCursorAndFiles(files, (meta?.['page'] as number) + 1 ?? 1); result = this.getCursorAndFiles(files, ((meta?.['page'] as number) ?? 1) + 1);
break; break;
} }
case 'prev': { case 'prev': {
result = this.getCursorAndFiles(files, (meta?.['page'] as number) - 1 ?? 1); result = this.getCursorAndFiles(files, ((meta?.['page'] as number) ?? 1) - 1);
break; break;
} }
default: { default: {

View File

@ -5,3 +5,4 @@ export { GitLabBackend } from './gitlab';
export { GiteaBackend } from './gitea'; export { GiteaBackend } from './gitea';
export { ProxyBackend } from './proxy'; export { ProxyBackend } from './proxy';
export { TestBackend } from './test'; export { TestBackend } from './test';
export { default as GitProxyBackendGenerator } from './git';

View File

@ -247,8 +247,8 @@ const Autocomplete: FC<AutocompleteProps> = ({
return ( return (
<li <li
key={index}
{...getOptionProps({ option: option as Option, index })} {...getOptionProps({ option: option as Option, index })}
key={index}
className={classNames(classes.option, selected && classes['option-selected'])} className={classNames(classes.option, selected && classes['option-selected'])}
data-testid={`autocomplete-option-${optionValue}`} data-testid={`autocomplete-option-${optionValue}`}
> >

View File

@ -4,6 +4,7 @@ import {
GitGatewayBackend, GitGatewayBackend,
GitHubBackend, GitHubBackend,
GitLabBackend, GitLabBackend,
GitProxyBackendGenerator,
ProxyBackend, ProxyBackend,
TestBackend, TestBackend,
} from './backends'; } from './backends';
@ -35,6 +36,7 @@ export default function addExtensions() {
registerBackend('github', GitHubBackend); registerBackend('github', GitHubBackend);
registerBackend('gitlab', GitLabBackend); registerBackend('gitlab', GitLabBackend);
registerBackend('gitea', GiteaBackend); registerBackend('gitea', GiteaBackend);
registerBackend('gitea-large', GitProxyBackendGenerator(GiteaBackend));
registerBackend('bitbucket', BitbucketBackend); registerBackend('bitbucket', BitbucketBackend);
registerBackend('test-repo', TestBackend); registerBackend('test-repo', TestBackend);
registerBackend('proxy', ProxyBackend); registerBackend('proxy', ProxyBackend);

View File

@ -16,7 +16,7 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "7.23.4", "@babel/cli": "7.23.4",
"@babel/core": "7.23.7", "@babel/core": "7.26.0",
"@babel/plugin-syntax-flow": "7.23.3", "@babel/plugin-syntax-flow": "7.23.3",
"@babel/plugin-transform-react-jsx": "7.23.4", "@babel/plugin-transform-react-jsx": "7.23.4",
"@vitejs/plugin-react": "4.2.1", "@vitejs/plugin-react": "4.2.1",

View File

@ -33,7 +33,7 @@
"yaml": "2.3.4" "yaml": "2.3.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.23.7", "@babel/core": "7.26.0",
"@babel/eslint-parser": "7.23.3", "@babel/eslint-parser": "7.23.3",
"@emotion/eslint-plugin": "11.11.0", "@emotion/eslint-plugin": "11.11.0",
"@next/bundle-analyzer": "14.0.4", "@next/bundle-analyzer": "14.0.4",

View File

@ -8,7 +8,7 @@ const isESM = process.env.NODE_ENV === 'esm';
console.info('Build Package:', path.basename(process.cwd())); console.info('Build Package:', path.basename(process.cwd()));
const defaultPlugins = [ const defaultPlugins = [
'lodash', '@sigmacomputing/babel-plugin-lodash',
[ [
'babel-plugin-transform-builtin-extend', 'babel-plugin-transform-builtin-extend',
{ {

3164
yarn.lock

File diff suppressed because it is too large Load Diff