Skip to content
This repository has been archived by the owner on Sep 9, 2024. It is now read-only.

feat: Gitea backend refactoring #833

Merged
merged 4 commits into from
Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 67 additions & 58 deletions packages/core/src/backends/gitea/API.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { Base64 } from 'js-base64';
import initial from 'lodash/initial';
import last from 'lodash/last';
import partial from 'lodash/partial';
import result from 'lodash/result';
import trim from 'lodash/trim';
import { trimStart, trim, result, partial, last, initial } from 'lodash';

import {
APIError,
Expand All @@ -22,12 +18,12 @@ import type { ApiRequest, FetchError } from '@staticcms/core/lib/util';
import type AssetProxy from '@staticcms/core/valueObjects/AssetProxy';
import type { Semaphore } from 'semaphore';
import type {
FilesResponse,
GitGetBlobResponse,
GitGetTreeResponse,
GiteaUser,
ReposGetResponse,
ReposListCommitsResponse,
ContentsResponse,
} from './types';

export const API_NAME = 'Gitea';
Expand All @@ -40,6 +36,20 @@ export interface Config {
originRepo?: string;
}

enum FileOperation {
CREATE = 'create',
DELETE = 'delete',
UPDATE = 'update',
}

export interface ChangeFileOperation {
content?: string;
from_path?: string;
path: string;
operation: FileOperation;
sha?: string;
}

interface MetaDataObjects {
entry: { path: string; sha: string };
files: MediaFile[];
Expand Down Expand Up @@ -76,13 +86,6 @@ type MediaFile = {
path: string;
};

export type Diff = {
path: string;
newFile: boolean;
sha: string;
binary: boolean;
};

export default class API {
apiRoot: string;
token: string;
Expand Down Expand Up @@ -120,7 +123,7 @@ export default class API {

static DEFAULT_COMMIT_MESSAGE = 'Automatically generated by Static CMS';

user(): Promise<{ full_name: string; login: string }> {
user(): Promise<{ full_name: string; login: string; avatar_url: string }> {
if (!this._userPromise) {
this._userPromise = this.getUser();
}
Expand Down Expand Up @@ -365,50 +368,53 @@ export default class API {
async persistFiles(dataFiles: DataFile[], mediaFiles: AssetProxy[], options: PersistOptions) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const files: (DataFile | AssetProxy)[] = mediaFiles.concat(dataFiles as any);
for (const file of files) {
const item: { raw?: string; sha?: string; toBase64?: () => Promise<string> } = file;
const contentBase64 = await result(
item,
'toBase64',
partial(this.toBase64, item.raw as string),
);
try {
const oldSha = await this.getFileSha(file.path);
await this.updateBlob(contentBase64, file, options, oldSha!);
} catch {
await this.createBlob(contentBase64, file, options);
}
}
const operations = await this.getChangeFileOperations(files, this.branch);
return this.changeFiles(operations, options);
}

async updateBlob(
contentBase64: string,
file: AssetProxy | DataFile,
options: PersistOptions,
oldSha: string,
) {
await this.request(`${this.repoURL}/contents/${file.path}`, {
method: 'PUT',
async changeFiles(operations: ChangeFileOperation[], options: PersistOptions) {
return (await this.request(`${this.repoURL}/contents`, {
method: 'POST',
body: JSON.stringify({
branch: this.branch,
content: contentBase64,
files: operations,
message: options.commitMessage,
sha: oldSha,
signoff: false,
}),
});
})) as FilesResponse;
}

async createBlob(contentBase64: string, file: AssetProxy | DataFile, options: PersistOptions) {
await this.request(`${this.repoURL}/contents/${file.path}`, {
method: 'POST',
body: JSON.stringify({
branch: this.branch,
content: contentBase64,
message: options.commitMessage,
signoff: false,
async getChangeFileOperations(files: { path: string; newPath?: string }[], branch: string) {
const items: ChangeFileOperation[] = await Promise.all(
files.map(async file => {
const content = await result(
file,
'toBase64',
partial(this.toBase64, (file as DataFile).raw),
);
let sha;
let operation;
let from_path;
let path = trimStart(file.path, '/');
try {
sha = await this.getFileSha(file.path, { branch });
operation = FileOperation.UPDATE;
from_path = file.newPath && path;
path = file.newPath ? trimStart(file.newPath, '/') : path;
} catch {
sha = undefined;
operation = FileOperation.CREATE;
}

return {
operation,
content,
path,
from_path,
sha,
} as ChangeFileOperation;
}),
});
);
return items;
}

async getFileSha(path: string, { repoURL = this.repoURL, branch = this.branch } = {}) {
Expand All @@ -434,15 +440,18 @@ export default class API {
}

async deleteFiles(paths: string[], message: string) {
for (const file of paths) {
const meta: ContentsResponse = await this.request(`${this.repoURL}/contents/${file}`, {
method: 'GET',
});
await this.request(`${this.repoURL}/contents/${file}`, {
method: 'DELETE',
body: JSON.stringify({ branch: this.branch, message, sha: meta.sha, signoff: false }),
});
}
const operations: ChangeFileOperation[] = await Promise.all(
paths.map(async path => {
const sha = await this.getFileSha(path);

return {
operation: FileOperation.DELETE,
path,
sha,
} as ChangeFileOperation;
}),
);
return this.changeFiles(operations, { commitMessage: message });
}

toBase64(str: string) {
Expand Down
47 changes: 28 additions & 19 deletions packages/core/src/backends/gitea/AuthenticationPage.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,54 @@
import { Gitea as GiteaIcon } from '@styled-icons/simple-icons/Gitea';
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';

import Login from '@staticcms/core/components/login/Login';
import { NetlifyAuthenticator } from '@staticcms/core/lib/auth';
import { PkceAuthenticator } from '@staticcms/core/lib/auth';

import type { AuthenticationPageProps, TranslatedProps } from '@staticcms/core/interface';
import type { MouseEvent } from 'react';

const GiteaAuthenticationPage = ({
inProgress = false,
config,
base_url,
siteId,
authEndpoint,
clearHash,
onLogin,
t,
}: TranslatedProps<AuthenticationPageProps>) => {
const [loginError, setLoginError] = useState<string | null>(null);

const auth = useMemo(() => {
const { base_url = 'https://try.gitea.io', app_id = '' } = config.backend;

const clientSizeAuth = new PkceAuthenticator({
base_url,
auth_endpoint: 'login/oauth/authorize',
app_id,
auth_token_endpoint: 'login/oauth/access_token',
clearHash,
});

// Complete authentication if we were redirected back to from the provider.
clientSizeAuth.completeAuth((err, data) => {
if (err) {
setLoginError(err.toString());
} else if (data) {
onLogin(data);
}
});
return clientSizeAuth;
}, [clearHash, config.backend, onLogin]);

const handleLogin = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
const cfg = {
base_url,
site_id: document.location.host.split(':')[0] === 'localhost' ? 'cms.netlify.com' : siteId,
auth_endpoint: authEndpoint,
};
const auth = new NetlifyAuthenticator(cfg);

const { auth_scope: authScope = '' } = config.backend;

const scope = authScope || 'repo';
auth.authenticate({ provider: 'gitea', scope }, (err, data) => {
auth.authenticate({ scope: 'repository' }, err => {
if (err) {
setLoginError(err.toString());
} else if (data) {
onLogin(data);
return;
}
});
},
[authEndpoint, base_url, config.backend, onLogin, siteId],
[auth],
);

return (
Expand Down
Loading