diff --git a/src/global.d.ts b/src/global.d.ts index b8b847f..ff135bc 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -5,7 +5,7 @@ type MetaData = { defaultBranchName?: string repoUrl?: string userUrl?: string - type?: 'tree' | 'blob' | string + type?: 'tree' | 'blob' | 'pull' | string } type TreeNode = { diff --git a/src/platforms/GitHub/API.ts b/src/platforms/GitHub/API.ts index 6e0298a..1185994 100644 --- a/src/platforms/GitHub/API.ts +++ b/src/platforms/GitHub/API.ts @@ -80,6 +80,16 @@ export async function getTreeData( return await request(url, { accessToken }) } +export async function getPullTreeData( + userName: string, + repoName: string, + pullId: string, + accessToken?: string, +): Promise { + const url = `https://${API_ENDPOINT}/repos/${userName}/${repoName}/pulls/${pullId}/files` + return await request(url, { accessToken }) +} + export async function getBlobData( userName: string, repoName: string, diff --git a/src/platforms/GitHub/Request.d.ts b/src/platforms/GitHub/Request.d.ts index c413a77..03c84e8 100644 --- a/src/platforms/GitHub/Request.d.ts +++ b/src/platforms/GitHub/Request.d.ts @@ -15,6 +15,21 @@ declare namespace GitHubAPI { url: string } + type PullTreeItem = { + additions: number + blob_url: string + changes: number + contents_url: string + deletions: number + filename: string + patch: string + raw_url: string + sha: string + status: 'modified' | 'added' | 'removed' + } + + type PullTreeData = PullTreeItem[] + type MetaData = { name: string default_branch: string diff --git a/src/platforms/GitHub/URLHelper.ts b/src/platforms/GitHub/URLHelper.ts index fc8140b..2091666 100644 --- a/src/platforms/GitHub/URLHelper.ts +++ b/src/platforms/GitHub/URLHelper.ts @@ -29,6 +29,10 @@ export function isInRepoPage() { return Boolean(document.querySelector(repoHeaderSelector)) } +export function isInPullPage() { + return parse().type === 'pull' +} + function isCommitPath(path: string[]) { return isCompleteCommitSHA(path[0]) } diff --git a/src/platforms/GitHub/index.ts b/src/platforms/GitHub/index.ts index e6a5072..bc6767b 100644 --- a/src/platforms/GitHub/index.ts +++ b/src/platforms/GitHub/index.ts @@ -9,22 +9,19 @@ import * as API from './API' import * as DOMHelper from './DOMHelper' import * as URLHelper from './URLHelper' -function parseTreeData(treeData: GitHubAPI.TreeData, metaData: MetaData) { - const { tree } = treeData - +function processTree(tree: TreeNode[]): TreeNode { // nodes are created from items and put onto tree - const pathToNode = new Map() - const pathToItem = new Map() - - const root: TreeNode = { name: '', path: '', contents: [], type: 'tree' } - pathToNode.set('', root) - + const pathToItem = new Map() tree.forEach(item => pathToItem.set(item.path, item)) + + const pathToCreated = new Map() + 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: GitHubAPI.TreeItem[] = [] - while (path !== '' && !pathToNode.has(path)) { + const itemsToCreateTreeNode: TreeNode[] = [] + while (path !== '' && !pathToCreated.has(path)) { const item = pathToItem.get(path) if (item) { itemsToCreateTreeNode.push(item) @@ -38,28 +35,13 @@ function parseTreeData(treeData: GitHubAPI.TreeData, metaData: MetaData) { while (itemsToCreateTreeNode.length) { const item = itemsToCreateTreeNode.pop() if (!item) continue - const node: TreeNode = { - 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 parentNode = pathToNode.get(path) - if (parentNode && parentNode.contents) { + const node: TreeNode = item + const parentNode = pathToCreated.get(path) + if (parentNode) { + if (!parentNode.contents) parentNode.contents = [] parentNode.contents.push(node) } - pathToNode.set(node.path, node) + pathToCreated.set(node.path, node) path = node.path } }) @@ -114,8 +96,63 @@ export const GitHub: Platform = { }, async getTreeData(metaData, accessToken) { const { userName, repoName, branchName } = metaData + + if (URLHelper.isInPullPage()) { + const treeData = await API.getPullTreeData( + userName, + repoName, + URLHelper.parse().path[0], + accessToken, + ) + + const nodes: TreeNode[] = treeData.map(item => { + const id = document.querySelector(`*[data-path^="${item.filename}"]`)?.parentElement?.id + return { + path: item.filename || '', + type: 'blob', + name: item.filename?.replace(/^.*\//, '') || '', + url: `#${id}`, + contents: undefined, + sha: item.sha, + } + }) + + const missingFolders = findMissingFolders(nodes) + nodes.push( + ...missingFolders.map( + folder => + ({ + name: folder.replace(/^.*\//, ''), + path: folder, + type: 'tree', + } as TreeNode), + ), + ) + + const tree = processTree(nodes) + return tree + } + const treeData = await API.getTreeData(userName, repoName, branchName, accessToken) - const root = parseTreeData(treeData, metaData) + 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) { @@ -136,7 +173,7 @@ export const GitHub: Platform = { return root }, shouldShow() { - return DOMHelper.isInCodePage() + return DOMHelper.isInCodePage() || URLHelper.isInPullPage() }, getCurrentPath(branchName) { return URLHelper.getCurrentPath(branchName) @@ -154,6 +191,33 @@ export const GitHub: Platform = { }, } +function findMissingFolders(nodes: TreeNode[]) { + const folders = new Set() + const foundFolders = new Set() + for (const node of nodes) { + let path = node.path + if (node.type === 'tree') foundFolders.add(path) + else { + while (true) { + // 'a/b' -> 'a' + // 'a' -> '' + path = path.substring(0, path.lastIndexOf('/')) + if (path === '') break + folders.add(path) + } + } + } + + const missingFolders: string[] = [] + for (const folder of folders) { + if (!foundFolders.has(folder)) { + missingFolders.push(folder) + } + } + + return missingFolders +} + export function useGitHubAttachCopySnippetButton(copySnippetButton: boolean) { const attachCopySnippetButton = React.useCallback( function attachCopySnippetButton() {