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

Commit

Permalink
feat: Gitea backend refactoring (#833)
Browse files Browse the repository at this point in the history
  • Loading branch information
denyskon authored Jun 6, 2023
1 parent 77f5a51 commit 6cb1098
Show file tree
Hide file tree
Showing 9 changed files with 267 additions and 147 deletions.
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

0 comments on commit 6cb1098

Please sign in to comment.