mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
commit
453c40553b
7 changed files with 407 additions and 0 deletions
107
src/platforms/Gitea/API.ts
Normal file
107
src/platforms/Gitea/API.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { raiseError } from 'analytics'
|
||||
import { errors } from 'platforms'
|
||||
|
||||
function isEmptyProject(content: any /* safe any */) {
|
||||
return content?.['message'] === 'Git Repository is empty.'
|
||||
}
|
||||
|
||||
function isBlockedProject(content: any /* safe any */) {
|
||||
return content?.['message'] === 'Repository access blocked'
|
||||
}
|
||||
|
||||
async function request(
|
||||
url: string,
|
||||
{
|
||||
accessToken,
|
||||
}: {
|
||||
accessToken?: string
|
||||
} = {},
|
||||
) {
|
||||
const headers = {} as HeadersInit & {
|
||||
Authorization?: string
|
||||
}
|
||||
if (accessToken) {
|
||||
headers.Authorization = `token ${accessToken}`
|
||||
}
|
||||
|
||||
let res: Response
|
||||
try {
|
||||
res = await fetch(url, { headers })
|
||||
} catch (err) {
|
||||
throw new Error(errors.CONNECTION_BLOCKED)
|
||||
}
|
||||
|
||||
const contentType = res.headers.get('Content-Type') || res.headers.get('content-type')
|
||||
if (!contentType) {
|
||||
throw new Error(`Response has no content type`)
|
||||
} else if (!contentType.includes('application/json')) {
|
||||
throw new Error(`Response content type is ${contentType}`)
|
||||
}
|
||||
|
||||
// About res.ok:
|
||||
// True if res.status between 200~299
|
||||
// Ref: https://developer.mozilla.org/en-US/docs/Web/API/Response/ok
|
||||
if (res.ok) {
|
||||
return res.json()
|
||||
} else {
|
||||
if (res.status === 404 || res.status === 401) throw new Error(errors.NOT_FOUND)
|
||||
else if (res.status === 500) throw new Error(errors.SERVER_FAULT)
|
||||
else {
|
||||
const content = await res.json()
|
||||
if (isEmptyProject(content)) throw new Error(errors.EMPTY_PROJECT)
|
||||
if (isBlockedProject(content)) throw new Error(errors.BLOCKED_PROJECT)
|
||||
// Unknown type of error, report it!
|
||||
raiseError(new Error(res.statusText))
|
||||
throw new Error(content && content.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const API_ENDPOINT = `${window.location.host}/api/v1`
|
||||
|
||||
export async function getRepoMeta(
|
||||
userName: string,
|
||||
repoName: string,
|
||||
accessToken?: string,
|
||||
): Promise<GiteaAPI.MetaData> {
|
||||
const url = `https://${API_ENDPOINT}/repos/${userName}/${repoName}`
|
||||
return await request(url, { accessToken })
|
||||
}
|
||||
|
||||
export async function getTreeData(
|
||||
userName: string,
|
||||
repoName: string,
|
||||
branchName: string,
|
||||
recursive?: boolean,
|
||||
accessToken?: string,
|
||||
): Promise<GiteaAPI.TreeData> {
|
||||
const search = new URLSearchParams()
|
||||
if (recursive) search.set('recursive', '1')
|
||||
const url =
|
||||
`https://${API_ENDPOINT}/repos/${userName}/${repoName}/git/trees/${branchName}?` + search
|
||||
return await request(url, { accessToken })
|
||||
}
|
||||
|
||||
export async function getBlobData(
|
||||
userName: string,
|
||||
repoName: string,
|
||||
sha: string,
|
||||
accessToken?: string,
|
||||
): Promise<GitHubAPI.BlobData> {
|
||||
const url = `https://${API_ENDPOINT}/repos/${userName}/${repoName}/git/blobs/${sha}`
|
||||
return await request(url, { accessToken })
|
||||
}
|
||||
|
||||
export async function OAuth(code: string): Promise<string | null> {
|
||||
const endpoint = `https://gitako.now.sh/oauth/gitea?`
|
||||
const res = await fetch(endpoint + new URLSearchParams({ code }).toString(), {
|
||||
method: 'post',
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
const body = await res.json()
|
||||
const accessToken = body?.accessToken
|
||||
if (typeof accessToken === 'string') return accessToken
|
||||
}
|
||||
return null
|
||||
}
|
||||
24
src/platforms/Gitea/DOMHelper.ts
Normal file
24
src/platforms/Gitea/DOMHelper.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { raiseError } from 'analytics'
|
||||
import { $ } from 'utils/DOMHelper'
|
||||
|
||||
export function isInRepoPage() {
|
||||
const repoHeaderSelector = '.repo-header'
|
||||
return Boolean($(repoHeaderSelector))
|
||||
}
|
||||
|
||||
export function isInCodePage() {
|
||||
const branchListSelector = '.reference'
|
||||
return Boolean($(branchListSelector))
|
||||
}
|
||||
|
||||
export function getCurrentBranch() {
|
||||
const branchListSelector = '.reference'
|
||||
const branchButtonElement: HTMLElement = $(branchListSelector)
|
||||
const branchNameElement = branchButtonElement.querySelector('.text > strong')
|
||||
if (branchNameElement) {
|
||||
return branchNameElement.textContent;
|
||||
}
|
||||
|
||||
raiseError(new Error('cannot get current branch'))
|
||||
}
|
||||
|
||||
42
src/platforms/Gitea/Request.d.ts
vendored
Normal file
42
src/platforms/Gitea/Request.d.ts
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
declare namespace GiteaAPI {
|
||||
type TreeItem = {
|
||||
path: string
|
||||
mode: string
|
||||
sha: string
|
||||
size: number
|
||||
url: string
|
||||
type: 'blob' | 'commit' | 'tree'
|
||||
}
|
||||
|
||||
type TreeData = {
|
||||
sha: string
|
||||
truncated: boolean
|
||||
tree: TreeItem[]
|
||||
url: string
|
||||
}
|
||||
|
||||
type MetaData = {
|
||||
name: string
|
||||
default_branch: string
|
||||
html_url: string
|
||||
owner: {
|
||||
login: string
|
||||
html_url: string
|
||||
}
|
||||
}
|
||||
|
||||
type BlobData = {
|
||||
encoding: 'base64' | string
|
||||
sha: string
|
||||
content?: string
|
||||
size: number
|
||||
url: string
|
||||
}
|
||||
|
||||
type OAuth = {
|
||||
access_token?: string
|
||||
scope?: string
|
||||
token_type?: string
|
||||
error_description?: string
|
||||
}
|
||||
}
|
||||
70
src/platforms/Gitea/URLHelper.ts
Normal file
70
src/platforms/Gitea/URLHelper.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { raiseError } from 'analytics'
|
||||
|
||||
export function parse(): Partial<MetaData> & { path: string[] } {
|
||||
const { pathname } = window.location
|
||||
let [
|
||||
,
|
||||
// ignore content before the first '/'
|
||||
userName,
|
||||
repoName,
|
||||
type,
|
||||
...path // should be [...branchName.split('/'), ...filePath.split('/')]
|
||||
] = unescape(decodeURIComponent(pathname)).split('/')
|
||||
return {
|
||||
userName,
|
||||
repoName,
|
||||
branchName: undefined,
|
||||
type,
|
||||
path,
|
||||
}
|
||||
}
|
||||
|
||||
export function parseSHA() {
|
||||
const { type, path } = parse()
|
||||
return type === 'blob' || type === 'tree' ? path[0] : undefined
|
||||
}
|
||||
|
||||
function isCommitPath(path: string[]) {
|
||||
return isCompleteCommitSHA(path[0])
|
||||
}
|
||||
|
||||
function isCompleteCommitSHA(sha?: string) {
|
||||
return typeof sha === 'string' && /^[abcdef0-9]{40}$/i.test(sha)
|
||||
}
|
||||
|
||||
export function getCurrentPath(branchName = '') {
|
||||
const { path, type } = parse()
|
||||
if (type === 'blob' || type === 'tree') {
|
||||
if (isCommitPath(path)) {
|
||||
// path = commit-SHA/path/to/item
|
||||
path.shift()
|
||||
} else {
|
||||
// path = branch/name/path/to/item or HEAD/path/to/item
|
||||
// HEAD is not a valid branch name. Getting HEAD means being detached.
|
||||
if (path[0] === 'HEAD') path.shift()
|
||||
else {
|
||||
const splitBranchName = branchName.split('/')
|
||||
while (splitBranchName.length) {
|
||||
if (
|
||||
splitBranchName[0] === path[0] ||
|
||||
// Keep consuming as their heads are same
|
||||
(splitBranchName.length === 1 && splitBranchName[0].startsWith(path[0]))
|
||||
// This happens when visiting URLs like /blob/{commitSHA}/path/to/file
|
||||
// and {commitSHA} is shorter than we got from DOM
|
||||
) {
|
||||
splitBranchName.shift()
|
||||
path.shift()
|
||||
} else {
|
||||
raiseError(new Error(`branch name and path prefix not match`), {
|
||||
branchName,
|
||||
path: parse().path,
|
||||
})
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return path.map(decodeURIComponent)
|
||||
}
|
||||
return []
|
||||
}
|
||||
160
src/platforms/Gitea/index.ts
Normal file
160
src/platforms/Gitea/index.ts
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import { platform } from 'platforms'
|
||||
import { resolveGitModules } from 'utils/gitSubmodule'
|
||||
import { sortFoldersToFront } from 'utils/treeParser'
|
||||
import * as API from './API'
|
||||
import * as DOMHelper from './DOMHelper'
|
||||
import * as URLHelper from './URLHelper'
|
||||
|
||||
function processTree(tree: TreeNode[]): TreeNode {
|
||||
// nodes are created from items and put onto tree
|
||||
const pathToItem = new Map<string, TreeNode>()
|
||||
tree.forEach(item => pathToItem.set(item.path, item))
|
||||
|
||||
const pathToCreated = new Map<string, TreeNode>()
|
||||
const root: TreeNode = { name: '', path: '', contents: [], type: 'tree' }
|
||||
pathToCreated.set('', root)
|
||||
tree.forEach(item => {
|
||||
// bottom-up search for the deepest node created
|
||||
let path = item.path
|
||||
const itemsToCreateTreeNode: TreeNode[] = []
|
||||
while (path !== '' && !pathToCreated.has(path)) {
|
||||
const item = pathToItem.get(path)
|
||||
if (item) {
|
||||
itemsToCreateTreeNode.push(item)
|
||||
} else {
|
||||
const $item: TreeNode = {
|
||||
name: path.split('/').pop() || '',
|
||||
path,
|
||||
type: 'tree',
|
||||
contents: [],
|
||||
}
|
||||
pathToItem.set(path, $item)
|
||||
itemsToCreateTreeNode.push($item)
|
||||
}
|
||||
// 'a/b' -> 'a'
|
||||
// 'a' -> ''
|
||||
path = path.substring(0, path.lastIndexOf('/'))
|
||||
}
|
||||
|
||||
// top-down create nodes
|
||||
while (itemsToCreateTreeNode.length) {
|
||||
const item = itemsToCreateTreeNode.pop()
|
||||
if (!item) continue
|
||||
const node: TreeNode = item
|
||||
const parentNode = pathToCreated.get(path)
|
||||
if (parentNode) {
|
||||
if (!parentNode.contents) parentNode.contents = []
|
||||
parentNode.contents.push(node)
|
||||
}
|
||||
pathToCreated.set(node.path, node)
|
||||
path = node.path
|
||||
}
|
||||
})
|
||||
|
||||
sortFoldersToFront(root)
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
function getUrlForRedirect(
|
||||
userName: string,
|
||||
repoName: string,
|
||||
branchName: string,
|
||||
type = 'blob',
|
||||
path = '',
|
||||
) {
|
||||
// Modern browsers have great support for handling unsafe URL,
|
||||
// It may be possible to sanitize path with
|
||||
// `path => path.includes('#') ? path.replace(/#/g, '%23') : '...'
|
||||
return `https://${
|
||||
window.location.host
|
||||
}/${userName}/${repoName}/src/branch/${branchName}/${path
|
||||
.split('/')
|
||||
.map(encodeURIComponent)
|
||||
.join('/')}`
|
||||
}
|
||||
|
||||
export const Gitea: Platform = {
|
||||
isEnterprise() {
|
||||
return false;
|
||||
},
|
||||
resolveMeta() {
|
||||
if (!DOMHelper.isInRepoPage()) {
|
||||
return null
|
||||
}
|
||||
|
||||
let detectedBranchName
|
||||
if (DOMHelper.isInCodePage()) {
|
||||
detectedBranchName = DOMHelper.getCurrentBranch() || URLHelper.parseSHA()
|
||||
}
|
||||
|
||||
const metaData = {
|
||||
...URLHelper.parse(),
|
||||
branchName: detectedBranchName,
|
||||
} as MetaData
|
||||
return metaData
|
||||
},
|
||||
async getMetaData(partialMetaData, accessToken) {
|
||||
const { userName, repoName } = partialMetaData
|
||||
const data = await API.getRepoMeta(userName, repoName, accessToken)
|
||||
return {
|
||||
userUrl: data?.owner?.html_url,
|
||||
repoUrl: data?.html_url,
|
||||
defaultBranchName: data.default_branch,
|
||||
}
|
||||
},
|
||||
async getTreeData(metaData, path, recursive, accessToken) {
|
||||
const { userName, repoName, branchName } = metaData
|
||||
const treeData = await API.getTreeData(userName, repoName, branchName, recursive)
|
||||
|
||||
const root = processTree(
|
||||
treeData.tree.map(item => ({
|
||||
path: item.path || '',
|
||||
type: item.type || 'blob',
|
||||
name: item.path?.replace(/^.*\//, '') || '',
|
||||
url:
|
||||
item.url && item.type && item.path
|
||||
? getUrlForRedirect(
|
||||
metaData.userName,
|
||||
metaData.repoName,
|
||||
metaData.branchName,
|
||||
item.type,
|
||||
item.path,
|
||||
)
|
||||
: undefined,
|
||||
contents: item.type === 'tree' ? [] : undefined,
|
||||
sha: item.sha,
|
||||
})),
|
||||
)
|
||||
|
||||
const gitModules = root.contents?.find(item => item.name === '.gitmodules')
|
||||
if (gitModules) {
|
||||
if (metaData.userName && metaData.repoName && gitModules.sha) {
|
||||
const blobData = await API.getBlobData(
|
||||
metaData.repoName,
|
||||
metaData.userName,
|
||||
gitModules.sha,
|
||||
accessToken,
|
||||
)
|
||||
|
||||
if (blobData && blobData.encoding === 'base64' && blobData.content) {
|
||||
await resolveGitModules(root, Base64.decode(blobData.content))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { root }
|
||||
},
|
||||
shouldShow() {
|
||||
return DOMHelper.isInCodePage()
|
||||
},
|
||||
getCurrentPath(branchName) {
|
||||
return URLHelper.getCurrentPath(branchName)
|
||||
},
|
||||
setOAuth(code) {
|
||||
return API.OAuth(code)
|
||||
},
|
||||
getOAuthLink() {
|
||||
return `https://${window.location.host}/api/v1/user/applications/oauth2`
|
||||
},
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { dummyPlatformForTypeSafety } from './dummyPlatformForTypeSafety'
|
||||
import { Gitee } from './Gitee'
|
||||
import { Gitea } from './Gitea'
|
||||
import { GitHub } from './GitHub'
|
||||
|
||||
const platforms: {
|
||||
|
|
@ -7,6 +8,7 @@ const platforms: {
|
|||
} = {
|
||||
GitHub: GitHub,
|
||||
Gitee: Gitee,
|
||||
Gitea: Gitea,
|
||||
}
|
||||
|
||||
function resolvePlatform() {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ const config: Config = {
|
|||
'.repository-content',
|
||||
// gitee
|
||||
'#git-project-content',
|
||||
// gitea
|
||||
'.repository > .ui.container'
|
||||
],
|
||||
update: {
|
||||
css: false,
|
||||
|
|
|
|||
Loading…
Reference in a new issue