mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
301 lines
9.7 KiB
TypeScript
301 lines
9.7 KiB
TypeScript
import { useConfigs } from 'containers/ConfigsContext'
|
|
import { GITHUB_OAUTH } from 'env'
|
|
import { Base64 } from 'js-base64'
|
|
import { Platform } from 'platforms/platform'
|
|
import { $ } from 'utils/$'
|
|
import { configRef } from 'utils/config/helper'
|
|
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'
|
|
import { getCommitTreeData } from './getCommitTreeData'
|
|
import { getPullRequestTreeData } from './getPullRequestTreeData'
|
|
import { useEnterpriseStatBarStyleFix } from './hooks/useEnterpriseStatBarStyleFix'
|
|
import { useGitHubAttachCopySnippetButton } from './hooks/useGitHubAttachCopySnippetButton'
|
|
import { useGitHubCodeFold } from './hooks/useGitHubCodeFold'
|
|
import { useGitHubReviewStatus } from './hooks/useGitHubReviewStatus'
|
|
|
|
export 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
|
|
}
|
|
|
|
export function isEnterprise() {
|
|
if (window.location.host === 'github.com') return false
|
|
|
|
return (
|
|
/**
|
|
* <a class="AppHeader-logo" href="https://host.com/" data-hotkey="g d" aria-label="Homepage Enterprise">
|
|
* <svg></svg>
|
|
* </a>
|
|
*/
|
|
$('a.AppHeader-logo[aria-label="Homepage Enterprise"]') !== null ||
|
|
/**
|
|
* <a class="Header-link " href="https://host.com/" data-hotkey="g d" aria-label="Homepage Enterprise">
|
|
* <span>Enterprise</span>
|
|
* </a>
|
|
*/
|
|
$(
|
|
[
|
|
'a.Header-link[aria-label="Homepage Enterprise"]',
|
|
'a.Header-link[aria-label="Homepage"]', // legacy support
|
|
].join(),
|
|
e => e.textContent?.trim() === 'Enterprise',
|
|
) ||
|
|
false
|
|
)
|
|
}
|
|
|
|
const pathSHAMap = new Map<string, string>()
|
|
|
|
export const GitHub: Platform = {
|
|
shouldActivate() {
|
|
return (
|
|
window.location.host === 'github.com' ||
|
|
// <link rel="fluid-icon" href="https://host.com/fluidicon.png" title="GitHub">
|
|
!!document.querySelector('link[rel="fluid-icon"][title="GitHub"]')
|
|
)
|
|
},
|
|
isEnterprise,
|
|
resolvePartialMetaData() {
|
|
if (!DOMHelper.isInRepoPage()) {
|
|
return null
|
|
}
|
|
|
|
const metaFromDOM = DOMHelper.resolveMeta()
|
|
const metaFromURL = URLHelper.parse()
|
|
const userName: MetaData['userName'] | undefined = metaFromDOM.userName || metaFromURL.userName
|
|
const repoName: MetaData['repoName'] | undefined = metaFromDOM.repoName || metaFromURL.repoName
|
|
if (!userName || !repoName) {
|
|
return null
|
|
}
|
|
|
|
const { type } = metaFromURL
|
|
let branchName = metaFromDOM.branchName
|
|
if (URLHelper.isInPullPage()) {
|
|
branchName = DOMHelper.getIssueTitle()
|
|
} else if (URLHelper.isInCommitPage()) {
|
|
branchName = DOMHelper.getCommitTitle() || metaFromURL.path[0]
|
|
} else if (
|
|
DOMHelper.isInCodePage() &&
|
|
!['releases', 'tags'].includes(type || '') // resolve sentry issue #-CK
|
|
) {
|
|
branchName = DOMHelper.getCurrentBranch() || URLHelper.parseSHA()
|
|
}
|
|
|
|
const metaData = {
|
|
userName,
|
|
repoName,
|
|
type,
|
|
branchName,
|
|
}
|
|
return metaData
|
|
},
|
|
async getDefaultBranchName({ userName, repoName }, accessToken) {
|
|
const dataFromJSON = DOMHelper.resolveMetaFromEmbeddedData()
|
|
if (dataFromJSON?.defaultBranch) return dataFromJSON.defaultBranch
|
|
|
|
return (await API.getRepoMeta(userName, repoName, accessToken)).default_branch
|
|
},
|
|
resolveUrlFromMetaData({ userName, repoName, branchName }) {
|
|
const repoUrl = `${window.location.origin}/${userName}/${repoName}`
|
|
const userUrl = `${window.location.origin}/${userName}`
|
|
const pullId = URLHelper.isInPullPage()
|
|
const commitId = URLHelper.isInCommitPage()
|
|
const branchUrl = pullId
|
|
? `${repoUrl}/pull/${pullId}`
|
|
: commitId && URLHelper.isPossiblyCommitSHA(commitId)
|
|
? `${repoUrl}/tree/${commitId}`
|
|
: `${repoUrl}/tree/${branchName}`
|
|
return {
|
|
repoUrl,
|
|
userUrl,
|
|
branchUrl,
|
|
}
|
|
},
|
|
getTreeData(metaData, path = '/', recursive, accessToken) {
|
|
const pullId = URLHelper.isInPullPage()
|
|
if (pullId) return getPullRequestTreeData(metaData, pullId, accessToken)
|
|
|
|
const commitId = URLHelper.isInCommitPage()
|
|
if (commitId) return getCommitTreeData(metaData, commitId, accessToken)
|
|
|
|
return getRepositoryTreeData(metaData, path, recursive, accessToken)
|
|
},
|
|
shouldExpandSideBar() {
|
|
return Boolean(
|
|
(DOMHelper.isInCodePage() || URLHelper.isInCommitPage() || URLHelper.isInPullPage()) &&
|
|
!DOMHelper.isNativeFileTreeShown() &&
|
|
!DOMHelper.isNativePRFileTreeShown(),
|
|
)
|
|
},
|
|
shouldExpandAll() {
|
|
return Boolean(URLHelper.isInPullPage() || URLHelper.isInCommitPage())
|
|
},
|
|
getCurrentPath(branchName) {
|
|
const pathFromURL = URLHelper.parse().path.join('/')
|
|
if (pathFromURL.length) {
|
|
if (branchName && pathFromURL.startsWith(branchName + '/')) {
|
|
return pathFromURL.replace(branchName + '/', '').split('/')
|
|
} else {
|
|
// This would fail before PJAX replace, but works well when no branchName, e.g. first load
|
|
return DOMHelper.getPath()
|
|
}
|
|
} else {
|
|
return []
|
|
}
|
|
},
|
|
setOAuth(code) {
|
|
return API.OAuth(code)
|
|
},
|
|
getOAuthLink() {
|
|
const params = new URLSearchParams({
|
|
client_id: GITHUB_OAUTH.clientId,
|
|
scope: 'repo',
|
|
redirect_uri: window.location.href,
|
|
})
|
|
return `https://github.com/login/oauth/authorize?${params}`
|
|
},
|
|
usePlatformHooks() {
|
|
const { copySnippetButton, codeFolding } = useConfigs().value
|
|
useGitHubAttachCopySnippetButton(copySnippetButton)
|
|
useGitHubCodeFold(codeFolding)
|
|
useEnterpriseStatBarStyleFix()
|
|
},
|
|
usePlatformFileTreeHooks({ visibleNodesGenerator = null }) {
|
|
useGitHubReviewStatus(visibleNodesGenerator)
|
|
},
|
|
delegateFastRedirectAnchorProps() {
|
|
if (configRef.pjaxMode !== 'native') return
|
|
|
|
const pjaxContainerSelector = 'main'
|
|
const turboContainerId = 'repo-content-turbo-frame'
|
|
|
|
return {
|
|
'data-pjax': pjaxContainerSelector,
|
|
'data-turbo-frame':
|
|
URLHelper.isInPullPage() || URLHelper.isInCommitPage() ? undefined : turboContainerId,
|
|
onClick() {
|
|
/* Overwriting default onClick */
|
|
},
|
|
}
|
|
},
|
|
loadWithFastRedirect: (url, element) => {
|
|
if (configRef.pjaxMode === 'native') {
|
|
element.click()
|
|
return true
|
|
}
|
|
},
|
|
}
|
|
|
|
function sanitizePath(path: string) {
|
|
return path.replace(/\/\/+/g, '/').replace(/^\/|\/$/g, '') || '/'
|
|
}
|
|
|
|
async function getRepositoryTreeData(
|
|
{ userName, repoName, branchName }: Pick<MetaData, 'userName' | 'repoName' | 'branchName'>,
|
|
path: string,
|
|
recursive: boolean | undefined,
|
|
accessToken: string | undefined,
|
|
) {
|
|
const sha = path === '/' ? branchName : pathSHAMap.get(path)
|
|
if (!sha) throw new Error(`No sha for path "${path}"`)
|
|
const treeData = await API.getTreeData(userName, repoName, sha, recursive, accessToken)
|
|
|
|
// remove deep items
|
|
if (treeData.truncated) {
|
|
if (treeData.tree.some(item => item.path.includes('/')))
|
|
treeData.tree = treeData.tree.filter(item => !item.path.includes('/'))
|
|
}
|
|
|
|
// update map
|
|
if (path !== '/' || treeData.truncated) {
|
|
if (path !== '/') {
|
|
treeData.tree.forEach(item => {
|
|
item.path = sanitizePath(`${path}/${item.path}`)
|
|
})
|
|
}
|
|
treeData.tree.forEach(item => {
|
|
pathSHAMap.set(item.path, item.sha)
|
|
})
|
|
}
|
|
|
|
const root = processTree(
|
|
treeData.tree.map(item => ({
|
|
path: item.path || '',
|
|
type: item.type || 'blob',
|
|
name: item.path?.split('/').pop() || '',
|
|
url:
|
|
item.url && item.type && item.path
|
|
? URLHelper.getItemUrl(userName, repoName, branchName, item.type, item.path)
|
|
: undefined,
|
|
permalink: URLHelper.getItemUrl(userName, repoName, treeData.sha, item.type, item.path),
|
|
rawLink:
|
|
item.url && item.type === 'blob' && item.path
|
|
? URLHelper.getItemUrl(userName, repoName, branchName, 'raw', item.path)
|
|
: undefined,
|
|
contents: item.type === 'tree' ? [] : undefined,
|
|
sha: item.sha,
|
|
})),
|
|
)
|
|
|
|
const gitModules = root.contents?.find(
|
|
item => item.type === 'blob' && item.name === '.gitmodules',
|
|
)
|
|
if (gitModules?.sha) {
|
|
const blobData = await API.getBlobData(userName, repoName, gitModules.sha, accessToken)
|
|
|
|
if (blobData && blobData.encoding === 'base64' && blobData.content) {
|
|
await resolveGitModules(root, Base64.decode(blobData.content))
|
|
}
|
|
}
|
|
|
|
return { root, defer: treeData.truncated }
|
|
}
|